diff options
Diffstat (limited to 'src/frontend')
-rw-r--r-- | src/frontend/tubo/components/player.cljs | 94 | ||||
-rw-r--r-- | src/frontend/tubo/events.cljs | 11 | ||||
-rw-r--r-- | src/frontend/tubo/player/events.cljs | 218 | ||||
-rw-r--r-- | src/frontend/tubo/player/subs.cljs | 28 | ||||
-rw-r--r-- | src/frontend/tubo/player/views.cljs | 166 | ||||
-rw-r--r-- | src/frontend/tubo/queue/events.cljs | 8 | ||||
-rw-r--r-- | src/frontend/tubo/queue/views.cljs | 61 | ||||
-rw-r--r-- | src/frontend/tubo/stream/views.cljs | 57 | ||||
-rw-r--r-- | src/frontend/tubo/utils.cljs | 3 | ||||
-rw-r--r-- | src/frontend/tubo/views.cljs | 1 |
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]]]])) |