aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/frontend/tubo/components/player.cljs94
-rw-r--r--src/frontend/tubo/events.cljs11
-rw-r--r--src/frontend/tubo/player/events.cljs218
-rw-r--r--src/frontend/tubo/player/subs.cljs28
-rw-r--r--src/frontend/tubo/player/views.cljs166
-rw-r--r--src/frontend/tubo/queue/events.cljs8
-rw-r--r--src/frontend/tubo/queue/views.cljs61
-rw-r--r--src/frontend/tubo/stream/views.cljs57
-rw-r--r--src/frontend/tubo/utils.cljs3
-rw-r--r--src/frontend/tubo/views.cljs1
10 files changed, 392 insertions, 255 deletions
diff --git a/src/frontend/tubo/components/player.cljs b/src/frontend/tubo/components/player.cljs
index 3d72e48..5344d76 100644
--- a/src/frontend/tubo/components/player.cljs
+++ b/src/frontend/tubo/components/player.cljs
@@ -1,7 +1,89 @@
(ns tubo.components.player
(:require
[reagent.core :as r]
- [re-frame.core :as rf]))
+ [re-frame.core :as rf]
+ [reagent.core :as r]
+ [reagent.dom :as rdom]
+ ["@vidstack/react" :refer (MediaPlayer MediaProvider Poster)]
+ ["@vidstack/react/player/layouts/default" :refer (defaultLayoutIcons DefaultVideoLayout DefaultAudioLayout)]))
+
+(defn get-player-sources
+ [available-streams]
+ (map (fn [{:keys [content]}] {:src content :type "video/mp4"}) available-streams))
+
+(defn video-player
+ [stream !player]
+ (let [!elapsed-time @(rf/subscribe [:elapsed-time])
+ !main-player-first? (r/atom true)]
+ (r/create-class
+ {:component-will-unmount #(rf/dispatch [:main-player/ready false])
+ :reagent-render
+ (fn [{:keys [name video-streams audio-streams thumbnail-url]} !player]
+ (let [show-main-player? @(rf/subscribe [:main-player/show])]
+ [:> MediaPlayer
+ {:title name
+ :src (get-player-sources (into video-streams audio-streams))
+ :poster thumbnail-url
+ :class "h-[500px] lg:h-[600px] w-full xl:w-3/5 overflow-x-hidden"
+ :ref #(reset! !player %)
+ :loop (when show-main-player? (= @(rf/subscribe [:loop-playback]) :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-pos]))])
+ (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}]]))})))
+
+(defn audio-player
+ [stream !player]
+ (let [!elapsed-time @(rf/subscribe [:elapsed-time])
+ !bg-player-first? (r/atom nil)]
+ (r/create-class
+ {:component-will-unmount #(rf/dispatch [:background-player/ready false])
+ :reagent-render
+ (fn [{:keys [name video-streams audio-streams thumbnail-url]} !player]
+ [:> MediaPlayer
+ {:title name
+ :class "invisible fixed"
+ :controls []
+ :src (get-player-sources audio-streams)
+ :viewType "audio"
+ :ref #(reset! !player %)
+ :loop (= @(rf/subscribe [:loop-playback]) :stream)
+ :onCanPlay #(rf/dispatch [:background-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-pos]))])
+ (reset! !elapsed-time 0))
+ :onPlay #(rf/dispatch [:background-player/play])
+ :onReplay (fn []
+ (rf/dispatch [:background-player/set-paused false])
+ (reset! !elapsed-time 0))
+ :onPause #(rf/dispatch [:background-player/set-paused true])
+ :onLoadedData (fn []
+ (rf/dispatch [:background-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}]])})))
(defonce base-slider-classes
["h-2" "cursor-pointer" "appearance-none" "bg-neutral-300" "dark:bg-neutral-600"
@@ -41,14 +123,14 @@
(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))]
+ (get-slider-shadow-classes service-color))
+ bg-player-ready? @(rf/subscribe [:background-player/ready])]
[:input.w-full
{:class styles
:type "range"
:on-input #(reset! !elapsed-time (.. % -target -value))
- :on-change #(and @!player (> (.-readyState @!player) 0)
- (set! (.-currentTime @!player) @!elapsed-time))
- :max (if (and @!player (> (.-readyState @!player) 0))
+ :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)
:value @!elapsed-time}]))
@@ -90,7 +172,7 @@
: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 [:player/mute (not muted?) player])
+ :on-click #(rf/dispatch [:background-player/mute (not muted?) player])
:extra-classes [:pl-3 :pr-2]]
(when @show-slider?
[:input.absolute.w-24.ml-2.m-1.bottom-16
diff --git a/src/frontend/tubo/events.cljs b/src/frontend/tubo/events.cljs
index 20e350d..d9740f9 100644
--- a/src/frontend/tubo/events.cljs
+++ b/src/frontend/tubo/events.cljs
@@ -37,7 +37,7 @@
:loop-playback (if-nil (:loop-playback store) :playlist)
:queue-pos (if-nil (:queue-pos store) 0)
:volume-level (if-nil (:volume-level store) 100)
- :show-background-player (:show-background-player store)
+ :background-player/show (:background-player/show store)
:bookmarks
(if-nil (:bookmarks store) [{:id (nano-id) :name "Liked Streams"}])
:settings
@@ -115,7 +115,9 @@
(assoc :show-pagination-loading false))
:scroll-to-top nil
:body-overflow false
- :fx [[:dispatch [:queue/show false]]
+ :fx [(when (:main-player/show db)
+ [:dispatch [:player/switch-from-main]])
+ [:dispatch [:queue/show false]]
[:dispatch [:services/fetch-all
[:services/load] [:bad-response]]]
[:dispatch [:kiosks/fetch-all (:service-id db)
@@ -163,3 +165,8 @@
:fetch-homepage
(fn [{:keys [db]} _]
{:fx [[:dispatch [:services/fetch-all [:load-homepage] [:bad-response]]]]}))
+
+(rf/reg-event-fx
+ :change-view
+ (fn [{:keys [db]} [_ view]]
+ {:db (assoc-in db [:current-match :data :view] view)}))
diff --git a/src/frontend/tubo/player/events.cljs b/src/frontend/tubo/player/events.cljs
index 70bb0cc..7ee2088 100644
--- a/src/frontend/tubo/player/events.cljs
+++ b/src/frontend/tubo/player/events.cljs
@@ -1,5 +1,7 @@
(ns tubo.player.events
(:require
+ [tubo.components.player :as player :refer [get-player-sources]]
+ [tubo.stream.views :as stream]
[tubo.utils :as utils]
[goog.object :as gobj]
[re-frame.core :as rf]
@@ -8,21 +10,24 @@
(rf/reg-fx
:volume
(fn [{:keys [player volume]}]
- (when (and @player (> (.-readyState @player) 0))
+ (when @player
(set! (.-volume @player) (/ volume 100)))))
(rf/reg-fx
:mute
(fn [{:keys [player muted?]}]
- (when (and @player (> (.-readyState @player) 0))
+ (when @player
(set! (.-muted @player) muted?))))
(rf/reg-fx
:src
(fn [{:keys [player src current-pos]}]
- (set! (.-src @player) src)
- (set! (.-onended @player)
- #(rf/dispatch [:queue/change-pos (inc current-pos)]))))
+ (set! (.-source @player) (clj->js src))))
+
+(rf/reg-fx
+ :loop
+ (fn [{:keys [player loop]}]
+ (set! (.-loop @player) loop)))
(rf/reg-fx
:current-time
@@ -30,73 +35,83 @@
(set! (.-currentTime @player) time)))
(rf/reg-event-fx
- :player/seek
+ :background-player/seek
[(rf/inject-cofx ::inject/sub [:player])]
(fn [{:keys [db player]} [_ time]]
- {:current-time {:time time :player player}}))
+ (when (:background-player/ready db)
+ {:current-time {:time time :player player}})))
+
+(rf/reg-event-fx
+ :main-player/seek
+ [(rf/inject-cofx ::inject/sub [:main-player])]
+ (fn [{:keys [db main-player]} [_ time]]
+ {:current-time {:time time :player main-player}}))
(rf/reg-fx
- :pause
+ :pause!
(fn [{:keys [paused? player]}]
- (when (and @player (> (.-readyState @player) 0))
- (if paused?
- (.play @player)
- (.pause @player)))))
+ (when @player
+ (set! (.-paused @player) paused?))))
(rf/reg-event-db
- :player/set-paused
+ :background-player/set-paused
(fn [db [_ val]]
(assoc db :paused val)))
(rf/reg-event-fx
- :player/pause
+ :background-player/pause
[(rf/inject-cofx ::inject/sub [:player])]
(fn [{:keys [db player]} [_ paused?]]
- {:pause {:paused? (not paused?)
- :player player}}))
+ {:pause! {:paused? paused?
+ :player player}
+ :db (assoc db :paused paused?)}))
(rf/reg-event-fx
- :player/stop
- (fn [{:keys [db]}]
- {:fx [[:dispatch [:player/pause true]]
- [:dispatch [:player/seek 0]]]}))
+ :main-player/pause
+ [(rf/inject-cofx ::inject/sub [:main-player])]
+ (fn [{:keys [db main-player]} [_ paused?]]
+ (when (:main-player/ready db)
+ {:pause! {:paused? paused?
+ :player main-player}})))
(rf/reg-event-fx
- :player/start-in-background
- [(rf/inject-cofx ::inject/sub [:player])
- (rf/inject-cofx ::inject/sub [:elapsed-time])]
- (fn [{:keys [db player]} _]
- {:fx [[:dispatch [:player/set-paused true]]
- [:dispatch [:player/pause false]]
- [:dispatch [:player/change-volume (:volume-level db) player]]]
- :db (assoc db :player-ready (and @player (> (.-readyState @player) 0)))}))
+ :background-player/play
+ [(rf/inject-cofx ::inject/sub [:elapsed-time])
+ (rf/inject-cofx ::inject/sub [:main-player])]
+ (fn [{:keys [db elapsed-time main-player]}]
+ {:fx [[:dispatch [:background-player/seek @elapsed-time]]
+ (when (and (:main-player/ready db) @main-player)
+ [:dispatch [:main-player/pause true]])]}))
-(rf/reg-fx
- :audio-poster-mode
- (fn [{:keys [player options]}]
- (.audioPosterMode
- @player
- (-> (filter #(= (:src %) (.src @player)) (:sources options))
- first
- :label
- (clojure.string/includes? "audio-only")))))
+(rf/reg-event-fx
+ :main-player/play
+ [(rf/inject-cofx ::inject/sub [:elapsed-time])
+ (rf/inject-cofx ::inject/sub [:player])]
+ (fn [{:keys [db elapsed-time player]}]
+ {:fx [(when (and (:background-player/ready db) @player)
+ [:dispatch [:background-player/pause true]])]}))
-(rf/reg-fx
- :slider-color
- (fn [{:keys [player color]}]
- (doseq [class [".vjs-play-progress" ".vjs-volume-level" ".vjs-slider-bar"]]
- (set! (.. (.$ (.getChild ^videojs/VideoJsPlayer @player "ControlBar") class) -style -background) color))))
+(rf/reg-event-fx
+ :background-player/stop
+ (fn [{:keys [db]}]
+ {:fx [[:dispatch [:background-player/pause true]]
+ [:dispatch [:background-player/seek 0]]]}))
(rf/reg-event-fx
- :player/set-slider-color
- (fn [_ [_ !player service-id]]
- {:slider-color {:player !player :color (utils/get-service-color service-id)}}))
+ :background-player/start
+ [(rf/inject-cofx ::inject/sub [:player])
+ (rf/inject-cofx ::inject/sub [:elapsed-time])]
+ (fn [{:keys [db player elapsed-time]} _]
+ {:fx [[:dispatch [:background-player/pause false]]
+ [:dispatch [:player/change-volume (:volume-level db) player]]]}))
(rf/reg-event-fx
- :player/start-in-main
+ :main-player/start
[(rf/inject-cofx ::inject/sub [:elapsed-time])]
- (fn [{:keys [db]} [_ !player options service-id]]
- {:fx [[:audio-poster-mode {:player !player :options options}]]}))
+ (fn [{:keys [db elapsed-time]} _]
+ {:fx [[:dispatch [:main-player/pause false]]
+ (when (and (:main-player/show db) (not (:background-player/ready db)))
+ [:dispatch [:main-player/seek @elapsed-time]])]}))
(rf/reg-fx
:media-session-metadata
@@ -114,13 +129,13 @@
#(.setPositionState js/navigator.mediaSession
{:duration (.-duration @player)
:playbackRate (.-playbackRate @player)
- :position (.-currentTime @player)})
+ :position current-time})
seek #(do (rf/dispatch [:seek %]) (update-position))
events
{"play" #(.play @player)
"pause" #(.pause @player)
- "previoustrack" #(rf/dispatch [:change-queue-pos (dec current-pos)])
- "nexttrack" #(rf/dispatch [:change-queue-pos (inc current-pos)])
+ "previoustrack" #(rf/dispatch [:queue/change-pos (dec current-pos)])
+ "nexttrack" #(rf/dispatch [:queue/change-pos (inc current-pos)])
"seekbackward" (fn [^js/navigator.MediaSessionActionDetails details]
(seek (- current-time (or (.-seekOffset details) 10))))
"seekforward" (fn [^js/navigator.MediaSessionActionDetails details]
@@ -140,7 +155,7 @@
:volume {:player player :volume value}}))
(rf/reg-event-fx
- :player/mute
+ :background-player/mute
[(rf/inject-cofx :store)]
(fn [{:keys [db store]} [_ value player]]
{:db (assoc db :muted value)
@@ -159,38 +174,71 @@
:store (assoc store :loop-playback loop-state)})))
(rf/reg-event-fx
- :player/dispose
+ :background-player/dispose
[(rf/inject-cofx :store)]
(fn [{:keys [db store]} _]
(let [remove-entries
(fn [elem]
(-> elem
- (update :show-background-player #(not %))
- (assoc :player-ready false)
+ (assoc :background-player/show false)
(assoc :queue [])
(assoc :queue-pos 0)))]
{:db (remove-entries db)
:store (remove-entries store)
- :fx [[:dispatch [:player/pause true]]
- [:dispatch [:player/seek 0]]]})))
+ :fx [[:dispatch [:background-player/pause true]]
+ [:dispatch [:background-player/seek 0]]]})))
+
+(rf/reg-event-db
+ :background-player/ready
+ (fn [db [_ ready]]
+ (assoc db :background-player/ready ready)))
+
+(rf/reg-event-db
+ :main-player/ready
+ (fn [db [_ ready]]
+ (assoc db :main-player/ready ready)))
(rf/reg-event-fx
:player/switch-to-background
[(rf/inject-cofx :store)]
- (fn [{:keys [db store]} [_ stream]]
+ (fn [{:keys [db store]} [_ stream notify?]]
(let [updated-db (update db :queue conj stream)
idx (.indexOf (:queue updated-db) stream)]
- {:db (-> updated-db
- (assoc :show-background-player true))
+ {:db (assoc updated-db :background-player/show (not (:main-player/show db)))
:store (-> store
- (assoc :show-background-player true)
+ (assoc :background-player/show (not (:main-player/show db)))
(assoc :queue (:queue updated-db)))
:fx [[:dispatch [:player/fetch-stream
(:url stream) idx (= (count (:queue db)) 0)]]
- (when-not (= (count (:queue db)) 0)
+ (when (and notify? (not (= (count (:queue db)) 0)))
[:dispatch [:notifications/add
- {:status-text (str "Added stream to queue")
- :failure :success}]])]})))
+ {:status-text "Added stream to queue"
+ :failure :info}]])]})))
+
+(rf/reg-event-fx
+ :player/show-main-player
+ (fn [{:keys [db]} [_ val]]
+ {:db (assoc db :main-player/show val)
+ :body-overflow val}))
+
+(rf/reg-event-fx
+ :player/switch-from-main
+ [(rf/inject-cofx :store)
+ (rf/inject-cofx ::inject/sub [:elapsed-time])]
+ (fn [{:keys [db store elapsed-time]} [_ stream]]
+ {:db (assoc db :background-player/show true)
+ :store (assoc store :background-player/show true)
+ :fx [[:dispatch [:player/show-main-player false]]
+ [:dispatch [:main-player/pause true]]]}))
+
+(rf/reg-event-fx
+ :player/switch-to-main
+ [(rf/inject-cofx :store)]
+ (fn [{:keys [db store]} [_ stream]]
+ {:fx [[:dispatch [:player/show-main-player true]]]
+ :db (assoc db :background-player/show false)
+ :store (assoc store :background-player/show false)
+ :scroll-to-top nil}))
(rf/reg-event-fx
:player/load-related-streams
@@ -203,49 +251,42 @@
[(rf/inject-cofx ::inject/sub [:player])]
(fn [{:keys [db player]} [_ idx play? res]]
(let [stream-res (js->clj res :keywordize-keys true)]
- {:db (assoc db :show-background-player-loading false)
- :fx (apply conj [[:dispatch [:queue/change-stream-source
- (-> stream-res :audio-streams first :content)
- idx]]]
- (when play?
- [[:src
- {:player player
- :src (-> stream-res :audio-streams first :content)
- :current-pos (:queue-pos db)}]
- [:media-session-metadata
+ {:db (assoc db :background-player/loading false)
+ :fx (apply conj [(when play? [:dispatch [:queue/change-stream stream-res idx]])]
+ (when (and (:background-player/ready db) play?)
+ [[:media-session-metadata
{:title (:name stream-res)
:artist (:uploader-name stream-res)
:artwork [{:src (:thumbnail-url stream-res)}]}]
[:media-session-handlers
{:current-pos (:queue-pos db)
- :player player}]]))})))
+ :player player}]]))})))
(rf/reg-event-fx
:player/bad-response
- (fn [{:keys [db]} [_ play? res]]
+ (fn [{:keys [db]} [_ idx play? res]]
{:db (assoc db
- :show-background-player-loading false
- :player-ready true)
+ :background-player/loading false)
:fx [[:dispatch [:bad-response res]]
(when play?
(if (> (-> db :queue count) 1)
- [:dispatch [:queue/change-pos (-> db :queue-pos inc)]]
- [:dispatch [:player/dispose]]))]}))
+ [:dispatch [:queue/change-pos (inc idx)]]
+ [:dispatch [:background-player/dispose]]))]}))
(rf/reg-event-fx
:player/fetch-related-streams
(fn [{:keys [db]} [_ url]]
{:fx [[:dispatch [:stream/fetch url
[:player/load-related-streams]] [:bad-response]]]
- :db (assoc db :show-background-player-loading true)}))
+ :db (assoc db :background-player/loading true)}))
(rf/reg-event-fx
:player/fetch-stream
(fn [{:keys [db]} [_ url idx play?]]
{:fx [[:dispatch [:stream/fetch url
[:player/load-stream idx play?]
- [:player/bad-response play?]]]]
- :db (assoc db :show-background-player-loading true)}))
+ [:player/bad-response idx play?]]]]
+ :db (assoc db :background-player/loading play?)}))
(rf/reg-event-fx
:player/start-radio
@@ -253,4 +294,13 @@
{:fx [[:dispatch [:player/switch-to-background stream]]
(when (not= (count (:queue db)) 0)
[:dispatch [:queue/change-pos (count (:queue db))]])
- [:dispatch [:player/fetch-related-streams (:url stream)]]]}))
+ [:dispatch [:player/fetch-related-streams (:url stream)]]
+ [:dispatch [:notifications/add
+ {:status-text "Started stream radio"
+ :failure :info}]]]}))
+
+(rf/reg-event-db
+ :main-player/toggle-layout
+ (fn [db [_ layout]]
+ (assoc-in db [:queue (:queue-pos db) layout]
+ (not (get-in db [:queue (:queue-pos db) layout])))))
diff --git a/src/frontend/tubo/player/subs.cljs b/src/frontend/tubo/player/subs.cljs
index 55da360..0b0f93c 100644
--- a/src/frontend/tubo/player/subs.cljs
+++ b/src/frontend/tubo/player/subs.cljs
@@ -4,6 +4,7 @@
[reagent.core :as r]))
(defonce !player (atom nil))
+(defonce !main-player (atom nil))
(defonce !elapsed-time (r/atom 0))
(rf/reg-sub
@@ -12,19 +13,29 @@
!player))
(rf/reg-sub
- :player-ready
+ :main-player
(fn [db _]
- (:player-ready db)))
+ !main-player))
(rf/reg-sub
- :show-background-player
+ :background-player/ready
(fn [db _]
- (:show-background-player db)))
+ (:background-player/ready db)))
(rf/reg-sub
- :show-background-player-loading
+ :main-player/ready
(fn [db _]
- (:show-background-player-loading db)))
+ (:main-player/ready db)))
+
+(rf/reg-sub
+ :background-player/show
+ (fn [db _]
+ (:background-player/show db)))
+
+(rf/reg-sub
+ :background-player/loading
+ (fn [db _]
+ (:background-player/loading db)))
(rf/reg-sub
:loop-playback
@@ -50,3 +61,8 @@
:elapsed-time
(fn [db _]
!elapsed-time))
+
+(rf/reg-sub
+ :main-player/show
+ (fn [db _]
+ (:main-player/show db)))
diff --git a/src/frontend/tubo/player/views.cljs b/src/frontend/tubo/player/views.cljs
index d7b851d..8d56764 100644
--- a/src/frontend/tubo/player/views.cljs
+++ b/src/frontend/tubo/player/views.cljs
@@ -2,41 +2,14 @@
(:require
[re-frame.core :as rf]
[reagent.core :as r]
- [reagent.dom :as rdom]
[reitit.frontend.easy :as rfe]
[tubo.bookmarks.modals :as modals]
[tubo.components.layout :as layout]
[tubo.components.player :as player]
+ [tubo.queue.views :as queue]
+ [tubo.stream.views :as stream]
[tubo.utils :as utils]
- ["video.js" :as videojs]
- ["videojs-mobile-ui"]
- ["@silvermine/videojs-quality-selector" :as VideojsQualitySelector]))
-
-(defn audio
- [!player]
- (let [{:keys [stream]} @(rf/subscribe [:queue-stream])
- queue-pos @(rf/subscribe [:queue-pos])]
- (r/create-class
- {: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)) stream)))
- :reagent-render
- (fn [!player]
- (let [!elapsed-time @(rf/subscribe [:elapsed-time])
- muted? @(rf/subscribe [:muted])
- volume-level @(rf/subscribe [:volume-level])
- loop-playback @(rf/subscribe [:loop-playback])]
- [:audio
- {:ref #(reset! !player %)
- :loop (= loop-playback :stream)
- :muted muted?
- :on-loaded-data #(rf/dispatch [:player/start-in-background])
- :on-time-update #(reset! !elapsed-time (.-currentTime @!player))
- :on-pause #(rf/dispatch [:player/set-paused true])
- :on-play #(rf/dispatch [:player/set-paused false])}]))})))
+ ["@vidstack/react" :refer (useStore MediaPlayerInstance)]))
(defn stream-metadata
[{:keys [thumbnail-url url name uploader-url uploader-name]}]
@@ -56,13 +29,15 @@
(defn main-controls
[!player color]
- (let [queue @(rf/subscribe [:queue])
- queue-pos @(rf/subscribe [:queue-pos])
- loading? @(rf/subscribe [:show-background-player-loading])
- !elapsed-time @(rf/subscribe [:elapsed-time])
- loop-playback @(rf/subscribe [:loop-playback])
- paused? @(rf/subscribe [:paused])
- player-ready? @(rf/subscribe [:player-ready])]
+ (let [queue @(rf/subscribe [:queue])
+ queue-pos @(rf/subscribe [:queue-pos])
+ loading? @(rf/subscribe [:background-player/loading])
+ loop-playback @(rf/subscribe [:loop-playback])
+ !main-player @(rf/subscribe [:main-player])
+ bg-player-ready? @(rf/subscribe [:background-player/ready])
+ main-player-ready? @(rf/subscribe [:main-player/ready])
+ paused? @(rf/subscribe [:paused])
+ !elapsed-time @(rf/subscribe [:elapsed-time])]
[:div.flex.flex-col.items-center.ml-auto
[:div.flex.justify-end
[player/loop-button loop-playback color]
@@ -72,35 +47,35 @@
:disabled? (not (and queue (not= queue-pos 0)))]
[player/button
:icon [:i.fa-solid.fa-backward]
- :on-click #(rf/dispatch [:player/seek (- @!elapsed-time 5)])]
+ :on-click #(rf/dispatch [:background-player/seek (- @!elapsed-time 5)])]
[player/button
- :icon (if (or (not loading?) player-ready?)
+ :icon (if (and (not loading?) @!player)
(if paused?
[:i.fa-solid.fa-play]
[:i.fa-solid.fa-pause])
[layout/loading-icon color "lg:text-2xl"])
- :on-click #(rf/dispatch [:player/pause (not paused?)])
+ :on-click #(rf/dispatch [:background-player/pause (not (.-paused @!player))])
:show-on-mobile? true
:extra-classes ["lg:text-2xl"]]
[player/button
:icon [:i.fa-solid.fa-forward]
- :on-click #(rf/dispatch [:player/seek (+ @!elapsed-time 5)])]
+ :on-click #(rf/dispatch [:background-player/seek (+ @!elapsed-time 5)])]
[player/button
:icon [:i.fa-solid.fa-forward-step]
:on-click #(rf/dispatch [:queue/change-pos (inc queue-pos)])
:disabled? (not (and queue (< (inc queue-pos) (count queue))))]]
[:div.hidden.lg:flex.items-center.text-sm
[:span.mx-2
- (if (and @!player @!elapsed-time) (utils/format-duration @!elapsed-time) "--:--")]
+ (if (and bg-player-ready? @!player @!elapsed-time) (utils/format-duration @!elapsed-time) "--:--")]
[:div.w-20.lg:w-64.mx-2.flex.items-center
[player/time-slider !player !elapsed-time color]]
[:span.mx-2
- (if (and @!player player-ready?) (utils/format-duration (.-duration @!player)) "--:--")]]]))
+ (if (and bg-player-ready? @!player) (utils/format-duration (.-duration @!player)) "--:--")]]]))
(defn extra-controls
[!player {:keys [url uploader-url] :as stream} color]
(let [!menu-active? (r/atom nil)]
- (fn []
+ (fn [!player {:keys [url uploader-url] :as stream} color]
(let [muted? @(rf/subscribe [:muted])
volume @(rf/subscribe [:volume-level])
queue @(rf/subscribe [:queue])
@@ -131,6 +106,9 @@
{:label "Remove from queue"
:icon [:i.fa-solid.fa-trash]
:on-click #(rf/dispatch [:queue/remove queue-pos])}
+ {:label "Switch to main"
+ :icon [:i.fa-solid.fa-display]
+ :on-click #(rf/dispatch [:player/switch-to-main])}
{:label "Show channel details"
:icon [:i.fa-solid.fa-user]
:on-click #(rf/dispatch [:navigate
@@ -139,46 +117,72 @@
:query {:url uploader-url}}])}
{:label "Close player"
:icon [:i.fa-solid.fa-close]
- :on-click #(rf/dispatch [:player/dispose])}]
+ :on-click #(rf/dispatch [:background-player/dispose])}]
:menu-styles {:bottom "30px" :top nil :right "10px"}
:extra-classes [:pt-1 :!pl-4 :px-3]]]))))
(defn background-player
[]
- (let [!player @(rf/subscribe [:player])
- stream @(rf/subscribe [:queue-stream])
- show-player? @(rf/subscribe [:show-background-player])
- show-queue? @(rf/subscribe [:show-queue])
- dark-theme? @(rf/subscribe [:dark-theme])
- color (-> stream :service-id utils/get-service-color)
- bg-color (str "rgba(" (if dark-theme? "23,23,23" "255,255,255") ",0.95)")
- bg-image (str "linear-gradient(" bg-color "," bg-color "),url(" (:thumbnail-url stream) ")")]
- (when show-player?
- [:div.sticky.absolute.left-0.bottom-0.z-10.p-3.transition-all.ease-in
- {:style
- {:visibility (when show-queue? "hidden")
- :opacity (if show-queue? 0 1)
- :background-image bg-image
- :background-size "cover"
- :background-position "center"
- :background-repeat "no-repeat"}}
- [:div.flex.items-center.justify-between
- [audio !player]
- [stream-metadata stream]
- [main-controls !player color]
- [extra-controls !player stream color]]])))
+ (let [!show-tooltip? (r/atom nil)]
+ (fn []
+ (let [!player @(rf/subscribe [:player])
+ stream @(rf/subscribe [:queue-stream])
+ show-queue? @(rf/subscribe [:show-queue])
+ show-player? @(rf/subscribe [:background-player/show])
+ dark-theme? @(rf/subscribe [:dark-theme])
+ muted? @(rf/subscribe [:muted])
+ loop-playback @(rf/subscribe [:loop-playback])
+ color (-> stream :service-id utils/get-service-color)
+ bg-color (str "rgba(" (if dark-theme? "23,23,23" "255,255,255") ",0.95)")
+ bg-image (str "linear-gradient(" bg-color "," bg-color "),url(" (:thumbnail-url stream) ")")]
+ (when show-player?
+ [:div.sticky.absolute.left-0.bottom-0.z-10.p-3.transition-all.ease-in.relative
+ {:on-mouse-over #(reset! !show-tooltip? true)
+ :on-mouse-out #(reset! !show-tooltip? false)
+ :style
+ {:visibility (when show-queue? "hidden")
+ :opacity (if show-queue? 0 1)
+ :background-image bg-image
+ :background-size "cover"
+ :background-position "center"
+ :background-repeat "no-repeat"}}
+ [:div.absolute.flex.items-center.justify-center.w-full.transition.ease-in-out.h-fit.bottom-full.left-0.py-1
+ {:class [(when-not @!show-tooltip? :invisible) (if @!show-tooltip? :opacity-1 :opacity-0)]}
+ [:button.px-5.rounded.rounded-lg.border.border.border-neutral-300.dark:border-stone-700
+ {:on-click #(do (rf/dispatch [:player/switch-to-main stream]) (reset! !show-tooltip? false))
+ :style {:background bg-color}}
+ [:i.fa-solid.fa-caret-up]]]
+ [:div.flex.items-center
+ [player/audio-player stream !player]
+ [stream-metadata stream]
+ [main-controls !player color]
+ [extra-controls !player stream color]]])))))
-(defn main-player
- [options service-id]
- (let [!player (atom nil)]
- (r/create-class
- {:component-did-mount
- (fn [^videojs/VideoJsPlayer this]
- (VideojsQualitySelector videojs)
- (reset! !player (videojs (rdom/dom-node this) (clj->js options)))
- (.on @!player "ready" (fn []
- (.mobileUi ^videojs/VideoJsPlayer @!player)
- (rf/dispatch [:player/set-slider-color !player service-id])))
- (.on @!player "play" #(rf/dispatch [:player/start-in-main !player options])))
- :component-will-unmount #(when @!player (.dispose @!player))
- :reagent-render (fn [options] [:video-js.vjs-tubo])})))
+(defn main-player []
+ (let [queue @(rf/subscribe [:queue])
+ queue-pos @(rf/subscribe [:queue-pos])
+ bookmarks @(rf/subscribe [:bookmarks])
+ !player @(rf/subscribe [:main-player])
+ {:keys [service-id] :as stream} @(rf/subscribe [:queue-stream])
+ show-player? @(rf/subscribe [:main-player/show])]
+ [:div.fixed.w-full.bg-neutral-100.dark:bg-neutral-900.overflow-auto.z-10.transition-all.ease-in-out.shadow-lg.shadow-neutral-900.dark:shadow-neutral-300
+ {:class ["rounded-t-[50px]" "h-[calc(100%-56px)]" (if show-player? "translate-y-0" "translate-y-full")]}
+ [:div.sticky.z-10.right-0.top-0
+ [:button.absolute.text-white.m-8.text-2xl.z-10.right-0
+ {:on-click #(rf/dispatch [:player/switch-from-main nil])}
+ [:i.fa-solid.fa-close
+ {:class "drop-shadow-[0_0_1px_#000]"}]]]
+ (when (and show-player? stream)
+ [:div
+ [:div.flex.flex-col.items-center.w-full.xl:py-6
+ [player/video-player stream !player]]
+ [:div.flex.items-center.justify-center
+ [:div.flex.flex-col.gap-y-1.w-full.h-64.overflow-y-auto
+ {:class ["lg:w-4/5" "xl:w-3/5"]}
+ (for [[i item] (map-indexed vector queue)]
+ ^{:key i} [queue/queue-item item queue queue-pos i bookmarks])]]
+ [layout/content-container
+ [stream/metadata stream]
+ [stream/description stream]
+ [stream/comments stream]
+ [stream/suggested stream]]])]))
diff --git a/src/frontend/tubo/queue/events.cljs b/src/frontend/tubo/queue/events.cljs
index e60c661..974700a 100644
--- a/src/frontend/tubo/queue/events.cljs
+++ b/src/frontend/tubo/queue/events.cljs
@@ -18,15 +18,15 @@
:fx (if notify?
[[:dispatch [:notifications/add
{:status-text "Added stream to queue"
- :failure :success}]]]
+ :failure :info}]]]
[])})))
(rf/reg-event-fx
:queue/add-n
[(rf/inject-cofx :store)]
(fn [{:keys [db store]} [_ streams notify?]]
- {:db (assoc db :show-background-player true)
- :store (assoc store :show-background-player true)
+ {:db (assoc db :background-player/show (not (:main-player/show db)))
+ :store (assoc store :background-player/show (not (:main-player/show db)))
:fx (into (map (fn [stream] [:dispatch [:queue/add stream]]) streams)
[[:dispatch [:player/fetch-stream (-> streams first :url)
(count (:queue db)) (= (count (:queue db)) 0)]]
@@ -34,7 +34,7 @@
[:dispatch [:notifications/add
{:status-text (str "Added " (count streams)
" streams to queue")
- :failure :success}]])])}))
+ :failure :info}]])])}))
(rf/reg-event-fx
:queue/remove
diff --git a/src/frontend/tubo/queue/views.cljs b/src/frontend/tubo/queue/views.cljs
index 7d7c83b..c6326fe 100644
--- a/src/frontend/tubo/queue/views.cljs
+++ b/src/frontend/tubo/queue/views.cljs
@@ -55,10 +55,11 @@
(defn queue-item
[item queue queue-pos i bookmarks]
- (let [!menu-active? (r/atom false)]
+ (let [!menu-active? (r/atom false)
+ show-main-player? @(rf/subscribe [:main-player/show])]
(fn [item queue queue-pos i bookmarks]
[:div.relative.w-full
- {:ref #(when (and queue (= queue-pos i)) (rf/dispatch [:scroll-into-view %]))}
+ {:ref #(when (and queue (= queue-pos i) (not show-main-player?)) (rf/dispatch [:scroll-into-view %]))}
[item-metadata item queue-pos i]
[popover item i !menu-active? bookmarks]])))
@@ -75,25 +76,26 @@
uploader-name]])
(defn main-controls
- [{:keys [service-id]} queue queue-pos]
- (let [loop-playback @(rf/subscribe [:loop-playback])
- service-color (and service-id (utils/get-service-color service-id))
- !player @(rf/subscribe [:player])
- loading? @(rf/subscribe [:show-background-player-loading])
- player-ready? @(rf/subscribe [:player-ready])
- paused? @(rf/subscribe [:paused])
- !elapsed-time @(rf/subscribe [:elapsed-time])
- queue @(rf/subscribe [:queue])
- queue-pos @(rf/subscribe [:queue-pos])]
+ [{:keys [service-id]} queue queue-pos color]
+ (let [loop-playback @(rf/subscribe [:loop-playback])
+ !player @(rf/subscribe [:player])
+ !main-player @(rf/subscribe [:main-player])
+ loading? @(rf/subscribe [:background-player/loading])
+ bg-player-ready? @(rf/subscribe [:background-player/ready])
+ main-player-ready? @(rf/subscribe [:main-player/ready])
+ paused? @(rf/subscribe [:paused])
+ !elapsed-time @(rf/subscribe [:elapsed-time])
+ queue @(rf/subscribe [:queue])
+ queue-pos @(rf/subscribe [:queue-pos])]
[:<>
[:div.flex.flex-auto.py-2.w-full.items-center.text-sm
[:span.mr-4.whitespace-nowrap
- (if (and @!player @!elapsed-time) (utils/format-duration @!elapsed-time) "--:--")]
- [player/time-slider !player !elapsed-time service-color]
+ (if (and bg-player-ready? @!player @!elapsed-time) (utils/format-duration @!elapsed-time) "--:--")]
+ [player/time-slider !player !elapsed-time color]
[:span.ml-4.whitespace-nowrap
- (if (and @!player player-ready?) (utils/format-duration (.-duration @!player)) "--:--")]]
+ (if (and bg-player-ready? @!player) (utils/format-duration (.-duration @!player)) "--:--")]]
[:div.flex.justify-center.items-center
- [player/loop-button loop-playback service-color true]
+ [player/loop-button loop-playback color true]
[player/button
:icon [:i.fa-solid.fa-backward-step]
:on-click #(rf/dispatch [:queue/change-pos (dec queue-pos)])
@@ -102,21 +104,21 @@
:show-on-mobile? true]
[player/button
:icon [:i.fa-solid.fa-backward]
- :on-click #(rf/dispatch [:player/seek (- @!elapsed-time 5)])
+ :on-click #(rf/dispatch [:background-player/seek (- @!elapsed-time 5)])
:extra-classes [:text-xl]
:show-on-mobile? true]
[player/button
- :icon (if (or (not loading?) player-ready?)
+ :icon (if (and (not loading?) @!player)
(if paused?
[:i.fa-solid.fa-play]
[:i.fa-solid.fa-pause])
- [layout/loading-icon service-color :text-3xl])
- :on-click #(rf/dispatch [:player/pause (not paused?)])
- :extra-classes [:text-3xl]
- :show-on-mobile? true]
+ [layout/loading-icon color :text-3xl])
+ :on-click #(rf/dispatch [:background-player/pause (not (.-paused @!player))])
+ :show-on-mobile? true
+ :extra-classes [:text-3xl]]
[player/button
:icon [:i.fa-solid.fa-forward]
- :on-click #(rf/dispatch [:player/seek (+ @!elapsed-time 5)])
+ :on-click #(rf/dispatch [:background-player/seek (+ @!elapsed-time 5)])
:extra-classes [:text-xl]
:show-on-mobile? true]
[player/button
@@ -133,11 +135,12 @@
(defn queue
[]
- (let [show-queue @(rf/subscribe [:show-queue])
- stream @(rf/subscribe [:queue-stream])
- bookmarks @(rf/subscribe [:bookmarks])
- queue-pos @(rf/subscribe [:queue-pos])
- queue @(rf/subscribe [:queue])]
+ (let [show-queue @(rf/subscribe [:show-queue])
+ stream @(rf/subscribe [:queue-stream])
+ bookmarks @(rf/subscribe [:bookmarks])
+ queue-pos @(rf/subscribe [:queue-pos])
+ queue @(rf/subscribe [:queue])
+ color (-> stream :service-id utils/get-service-color)]
[:div.fixed.flex.flex-col.items-center.min-w-full.w-full.z-10.backdrop-blur
{:class ["dark:bg-neutral-900/90" "bg-neutral-100/90"
"min-h-[calc(100dvh-56px)]" "h-[calc(100dvh-56px)]"
@@ -151,4 +154,4 @@
^{:key i} [queue-item item queue queue-pos i bookmarks])]
[:div.flex.flex-col.py-4.shrink-0.px-5
[queue-metadata stream]
- [main-controls stream queue queue-pos]]]]))
+ [main-controls stream queue queue-pos color]]]]))
diff --git a/src/frontend/tubo/stream/views.cljs b/src/frontend/tubo/stream/views.cljs
index 31e001f..4d99d04 100644
--- a/src/frontend/tubo/stream/views.cljs
+++ b/src/frontend/tubo/stream/views.cljs
@@ -3,48 +3,14 @@
[reagent.core :as r]
[re-frame.core :as rf]
[reitit.frontend.easy :as rfe]
+ [tubo.components.player :refer [get-player-sources]]
[tubo.bookmarks.modals :as modals]
[tubo.comments.views :as comments]
[tubo.components.items :as items]
[tubo.components.layout :as layout]
- [tubo.player.views :as player]
+ [tubo.components.player :as player]
[tubo.utils :as utils]))
-(def player-elements
- ["PlayToggle" "ProgressControl" "VolumePanel" "CurrentTimeDisplay"
- "TimeDivider" "DurationDisplay" "Spacer" "QualitySelector"
- "PlaybackRateMenuButton" "FullscreenToggle"])
-
-(defn get-player-sources
- [available-streams]
- (reverse (map (fn [{:keys [content format resolution averageBitrate]}]
- {:src content
- :type "video/mp4"
- :label (str (or resolution "audio-only") " "
- format
- (when-not resolution
- (str " " averageBitrate "kbit/s")))})
- available-streams)))
-
-(defn player
- [{:keys [thumbnail-url audio-streams video-streams service-id]}]
- (let [page-loading? @(rf/subscribe [:show-page-loading])]
- (when-not page-loading?
- [:div.flex.flex-col.flex-auto.items-center.xl:py-6.!pb-0
- [:div.flex.flex-col.flex-auto.w-full {:class ["xl:w-3/5"]}
- [:div.flex.justify-center.relative
- {:class "h-[300px] md:h-[450px] lg:h-[600px]"}
- [player/main-player
- {:sources (get-player-sources (into audio-streams video-streams))
- :poster thumbnail-url
- :controls true
- :controlBar {:children player-elements}
- :preload "metadata"
- :responsive true
- :fill true
- :playbackRates [0.5 1 1.5 2]}
- service-id]]]])))
-
(defn metadata-popover
[_]
(let [!menu-active? (r/atom nil)]
@@ -54,7 +20,7 @@
[layout/popover-menu !menu-active?
[{:label "Add to queue"
:icon [:i.fa-solid.fa-headphones]
- :on-click #(rf/dispatch [:player/switch-to-background stream])}
+ :on-click #(rf/dispatch [:player/switch-to-background stream true])}
{:label "Play radio"
:icon [:i.fa-solid.fa-tower-cell]
:on-click #(rf/dispatch [:player/start-radio stream])}
@@ -121,7 +87,10 @@
(let [show? (:show-description @(rf/subscribe [:settings]))]
(when (and show? (not (empty? description)))
[layout/show-more-container show-description description
- #(rf/dispatch [:stream/toggle-layout :show-description])])))
+ #(rf/dispatch [(if @(rf/subscribe [:main-player/show])
+ :main-player/toggle-layout
+ :stream/toggle-layout)
+ :show-description])])))
(defn comments
[{:keys [comments-page show-comments show-comments-loading url] :as stream}]
@@ -150,7 +119,9 @@
(when (and show? (not (empty? related-streams)))
[layout/accordeon
{:label "Suggested"
- :on-open #(rf/dispatch [:stream/toggle-layout :show-related])
+ :on-open #(rf/dispatch [(if @(rf/subscribe [:main-player/show])
+ :main-player/toggle-layout
+ :stream/toggle-layout) :show-related])
:open? (not show-related)
:left-icon "fa-solid fa-list"
:right-button [layout/popover-menu !menu-active?
@@ -164,9 +135,13 @@
(defn stream
[]
- (let [stream @(rf/subscribe [:stream])]
+ (let [{:keys [audio-streams video-streams name thumbnail-url] :as stream} @(rf/subscribe [:stream])
+ !player @(rf/subscribe [:main-player])
+ page-loading? @(rf/subscribe [:show-page-loading])]
[:<>
- [player stream]
+ (when-not page-loading?
+ [:div.flex.flex-col.justify-center.items-center.lg:pt-4
+ [player/video-player stream !player]])
[layout/content-container
[metadata stream]
[description stream]
diff --git a/src/frontend/tubo/utils.cljs b/src/frontend/tubo/utils.cljs
index 58bb3e5..4b4a45a 100644
--- a/src/frontend/tubo/utils.cljs
+++ b/src/frontend/tubo/utils.cljs
@@ -46,5 +46,4 @@
[num]
(let [duration (and (not (js/isNaN num)) (js/Date. (* num 1000)))
slice (and duration #(.slice % (if (>= (.getUTCHours duration) 1) 11 14) 19))]
- (when slice
- (-> duration (.toISOString) slice))))
+ (if slice (-> duration (.toISOString) slice) "--:--")))
diff --git a/src/frontend/tubo/views.cljs b/src/frontend/tubo/views.cljs
index 12fbe54..124f3ce 100644
--- a/src/frontend/tubo/views.cljs
+++ b/src/frontend/tubo/views.cljs
@@ -18,4 +18,5 @@
(when-let [view (-> current-match :data :view)]
[view current-match])
[queue/queue]
+ [player/main-player]
[player/background-player]]]]))