diff options
-rw-r--r-- | src/frontend/tubo/components/audio_player.cljs | 128 | ||||
-rw-r--r-- | src/frontend/tubo/components/items.cljs | 2 | ||||
-rw-r--r-- | src/frontend/tubo/components/player.cljs | 128 | ||||
-rw-r--r-- | src/frontend/tubo/components/video_player.cljs | 27 | ||||
-rw-r--r-- | src/frontend/tubo/events.cljs | 27 | ||||
-rw-r--r-- | src/frontend/tubo/subs.cljs | 24 | ||||
-rw-r--r-- | src/frontend/tubo/views/stream.cljs | 56 |
7 files changed, 231 insertions, 161 deletions
diff --git a/src/frontend/tubo/components/audio_player.cljs b/src/frontend/tubo/components/audio_player.cljs new file mode 100644 index 0000000..fac2cf1 --- /dev/null +++ b/src/frontend/tubo/components/audio_player.cljs @@ -0,0 +1,128 @@ +(ns tubo.components.audio-player + (:require + [reagent.core :as r] + [re-frame.core :as rf] + [reitit.frontend.easy :as rfe] + [tubo.components.loading :as loading] + [tubo.events :as events] + [tubo.util :as util])) + +(defn player + [] + (let [!player (r/atom nil) + !elapsed-time (r/atom 0) + !autoplay? (r/atom true) + !volume-level (r/atom 100)] + (fn [] + (let [media-queue @(rf/subscribe [:media-queue]) + media-queue-pos @(rf/subscribe [:media-queue-pos]) + {:keys [uploader-name uploader-url + name stream url service-color] :as current-stream} @(rf/subscribe [:media-queue-stream]) + show-audio-player? @(rf/subscribe [:show-audio-player]) + show-audio-player-loading? @(rf/subscribe [:show-audio-player-loading]) + is-window-visible @(rf/subscribe [:is-window-visible]) + loop-file? @(rf/subscribe [:loop-file]) + loop-playlist? @(rf/subscribe [:loop-playlist])] + (when show-audio-player? + [:div.sticky.bottom-0.z-50.bg-white.dark:bg-neutral-900.px-3.py-5.sm:p-5.absolute.box-border.m-0 + {:style {:borderColor service-color :borderTopWidth "2px" :borderStyle "solid"}} + [:div.flex.items-center.justify-between + [:div.flex.items-center + [:div.flex.flex-col + [:a.text-xs.line-clamp-1 + {:href (rfe/href :tubo.routes/stream nil {:url url})} name] + [:a.text-xs.pt-2.text-neutral-600.dark:text-neutral-300.line-clamp-1 + {:href (rfe/href :tubo.routes/channel nil {:url uploader-url})} uploader-name]] + [:audio + {:src stream :ref #(reset! !player %) + :loop loop-file? + :on-time-update #(when (and @!player (> (.-readyState @!player) 0)) + (reset! !elapsed-time (.-currentTime @!player))) + :on-loaded-data #(when (and @!player (> (.-readyState @!player) 0)) + (.play @!player) + (set! (.-currentTime @!player) @!elapsed-time)) + :on-ended #(when (and @!player (> (.-readyState @!player) 0)) + (let [idx (if (< (+ media-queue-pos 1) (count media-queue)) + (+ media-queue-pos 1) + (if loop-playlist? 0 media-queue-pos))] + (rf/dispatch [::events/change-media-queue-pos idx]) + (reset! !elapsed-time 0) + (when (and (not is-window-visible) loop-playlist?) + (set! (.-src @!player) (:stream (nth media-queue idx))) + (.play @!player))))}]] + [:div.flex + [:button.focus:outline-none.mx-1.sm:mx-2 + {:class (when-not (and media-queue (not= media-queue-pos 0)) + "opacity-50 cursor-auto") + :on-click (when (and media-queue (not= media-queue-pos 0)) + #(do + (rf/dispatch [::events/change-media-queue-pos + (- media-queue-pos 1)]) + (reset! !elapsed-time 0)))} + [:i.fa-solid.fa-backward-step]] + [:button.focus:outline-none.mx-1.sm:mx-2 + {:on-click #(when-let [player @!player] + (if (.-paused player) + (.play player) + (.pause player)))} + (if @!player + (if show-audio-player-loading? + [loading/loading-icon service-color "text-1xl"] + (if (.-paused @!player) + [:i.fa-solid.fa-play] + [:i.fa-solid.fa-pause])) + [:i.fa-solid.fa-play])] + [:button.focus:ring-transparent.mx-1.sm:mx-2 + {:class (when-not (and media-queue (< (+ media-queue-pos 1) (count media-queue))) + "opacity-50 cursor-auto") + :on-click (when (and media-queue (< (+ media-queue-pos 1) (count media-queue))) + #(do + (rf/dispatch [::events/change-media-queue-pos + (+ media-queue-pos 1)]) + (reset! !elapsed-time 0)))} + [:i.fa-solid.fa-forward-step]] + [:div.flex + [:div.mx-2.hidden.sm:flex + [:span (if @!elapsed-time (util/format-duration @!elapsed-time) "00:00")] + [:span.mx-2 "/"] + [:span (if (and @!player (> (.-readyState @!player) 0)) + (util/format-duration (.-duration @!player)) + "00:00")]] + [:input.mx-2.w-20.ml:w-80.bg-gray-200.rounded-lg.cursor-pointer.focus:outline-none + {:type "range" + :on-input #(reset! !elapsed-time (.. % -target -value)) + :on-change #(and @!player (> (.-readyState @!player) 0) + (set! (.-currentTime @!player) @!elapsed-time)) + :style {:accentColor service-color} + :max (if (and @!player (> (.-readyState @!player) 0)) + (.floor js/Math (.-duration @!player)) + 100) + :value @!elapsed-time}] + [:button.focus:ring-transparent.mx-1.sm:mx-2 + {:on-click #(rf/dispatch [::events/toggle-loop-file])} + [:i.fa-solid.fa-repeat + {:style {:color (when loop-file? service-color)}}]] + [:button.focus:ring-transparent.mx-1.sm:mx-2 + {:on-click #(rf/dispatch [::events/toggle-loop-playlist])} + [:i.fa-solid.fa-retweet + {:style {:color (when loop-playlist? service-color)}}]] + [:div.hidden.sm:flex + [:button.focus:outline-none.mx-1.sm:mx-2 + {:on-click #(when-let [player @!player] + (set! (.-muted player) (not (.-muted player))))} + (if (and @!player (.-muted @!player)) + [:i.fa-solid.fa-volume-xmark] + [:i.fa-solid.fa-volume-low])] + [:input.w-20.bg-gray-200.rounded-lg.cursor-pointer.focus:outline-none + {:type "range" + :on-input #(do (reset! !volume-level (.. % -target -value)) + (and @!player (> (.-readyState @!player) 0) + (set! (.-volume @!player) (/ @!volume-level 100)))) + :style {:accentColor service-color} + :max 100 + :value @!volume-level}]]] + [:div.mx-1.sm:mx-2 + [:i.fa-solid.fa-close.cursor-pointer + {:on-click (fn [] + (rf/dispatch [::events/toggle-audio-player]) + (.pause @!player))}]]]]]))))) diff --git a/src/frontend/tubo/components/items.cljs b/src/frontend/tubo/components/items.cljs index 75c811b..6ca5515 100644 --- a/src/frontend/tubo/components/items.cljs +++ b/src/frontend/tubo/components/items.cljs @@ -41,7 +41,7 @@ [:i.fa-solid.fa-circle-check])] (when (= type "stream") [:button.pl-4.focus:outline-none - {:on-click #(rf/dispatch [::events/switch-to-global-player + {:on-click #(rf/dispatch [::events/switch-to-audio-player {:uploader-name uploader-name :uploader-url uploader-url :name name diff --git a/src/frontend/tubo/components/player.cljs b/src/frontend/tubo/components/player.cljs deleted file mode 100644 index 1fc01ff..0000000 --- a/src/frontend/tubo/components/player.cljs +++ /dev/null @@ -1,128 +0,0 @@ -(ns tubo.components.player - (:require - [reagent.core :as r] - [reagent.dom :as rdom] - [re-frame.core :as rf] - [reitit.frontend.easy :as rfe] - [tubo.components.loading :as loading] - [tubo.events :as events] - [tubo.util :as util] - ["video.js" :as videojs])) - -(defn global-player - [] - (let [!player (r/atom nil) - !loop? (r/atom nil) - !elapsed-time (r/atom 0) - !volume-level (r/atom 100)] - (fn [] - (let [media-queue @(rf/subscribe [:media-queue]) - media-queue-pos @(rf/subscribe [:media-queue-pos]) - {:keys [uploader-name uploader-url name stream url service-color]} - (and (not-empty media-queue) (nth media-queue media-queue-pos)) - show-global-player? @(rf/subscribe [:show-global-player]) - show-global-player-loading? @(rf/subscribe [:show-global-player-loading])] - (when show-global-player? - [:div.sticky.bottom-0.z-50.bg-white.dark:bg-neutral-900.px-3.py-5.sm:p-5.absolute.box-border.m-0 - {:style {:borderColor service-color :borderTopWidth "2px" :borderStyle "solid"}} - [:div.flex.items-center.justify-between - [:div.flex.items-center - [:div.flex.flex-col - [:a.text-xs.line-clamp-1 - {:href (rfe/href :tubo.routes/stream nil {:url url})} name] - [:a.text-xs.pt-2.text-neutral-600.dark:text-neutral-300.line-clamp-1 - {:href (rfe/href :tubo.routes/channel nil {:url uploader-url})} uploader-name]] - [:audio - {:src stream :ref #(reset! !player %) - :loop @!loop? - :autoPlay true - :on-time-update #(and @!player (> (.-readyState @!player) 0) - (reset! !elapsed-time (.-currentTime @!player))) - :on-ended #(and (< (+ media-queue-pos 1) (count media-queue)) - (rf/dispatch [::events/change-media-queue-pos - (+ media-queue-pos 1)]))}]] - [:div.mx-2.flex - (when (and media-queue (not= media-queue-pos 0)) - [:button.focus:outline-none.mx-2 - {:on-click #(rf/dispatch [::events/change-media-queue-pos - (- media-queue-pos 1)])} - [:i.fa-solid.fa-backward-step]]) - [:button.focus:outline-none.mx-2 - {:on-click #(when-let [player @!player] - (if (.-paused player) - (.play player) - (.pause player)))} - (if @!player - (if show-global-player-loading? - [loading/loading-icon service-color "text-1xl"] - (if (.-paused @!player) - [:i.fa-solid.fa-play] - [:i.fa-solid.fa-pause])) - [:i.fa-solid.fa-play])] - (when (and media-queue (< (+ media-queue-pos 1) (count media-queue))) - [:button.focus:ring-transparent.mx-2 - {:on-click #(rf/dispatch [::events/change-media-queue-pos - (+ media-queue-pos 1)])} - [:i.fa-solid.fa-forward-step]]) - [:div.flex - [:div.mx-2.hidden.sm:flex - [:span (if @!elapsed-time (util/format-duration @!elapsed-time) "00:00")] - [:span.mx-2 "/"] - [:span (when (and @!player (> (.-readyState @!player) 0)) - (util/format-duration (.-duration @!player)))]] - [:input.mx-2.w-20.ml:w-80.bg-gray-200.rounded-lg.cursor-pointer.focus:outline-none - {:type "range" - :on-input #(reset! !elapsed-time (.. % -target -value)) - :on-change #(and @!player (> (.-readyState @!player) 0) - (set! (.-currentTime @!player) @!elapsed-time)) - :style {:accentColor service-color} - :max (if (and @!player (> (.-readyState @!player) 0)) - (.floor js/Math (.-duration @!player)) - 100) - :value @!elapsed-time}] - [:button.focus:ring-transparent.mx-2 - {:on-click (fn [] (swap! !loop? #(not %)))} - [:i.fa-solid.fa-repeat - {:style {:color (when @!loop? service-color)}}]] - [:div.mx-2.flex - [:button.focus:outline-none.mx-2 - {:on-click #(when-let [player @!player] - (set! (.-muted player) (not (.-muted player))))} - (if (and @!player (.-muted @!player)) - [:i.fa-solid.fa-volume-xmark] - [:i.fa-solid.fa-volume-low])] - [:input.w-20.bg-gray-200.rounded-lg.cursor-pointer.focus:outline-none.hidden.sm:block - {:type "range" - :on-input #(do (reset! !volume-level (.. % -target -value)) - (and @!player (> (.-readyState @!player) 0) - (set! (.-volume @!player) (/ @!volume-level 100)))) - :style {:accentColor service-color} - :max 100 - :value @!volume-level}]]] - [:div.ml-2 - [:i.fa-solid.fa-close.cursor-pointer - {:on-click (fn [] - (rf/dispatch [::events/toggle-global-player]) - (.pause @!player))}]]]]]))))) - -(defn stream-player - [options url] - (let [!player (atom nil)] - (r/create-class - {:display-name "StreamPlayer" - :component-did-mount - (fn [this] - (reset! !player (videojs (rdom/dom-node this) (clj->js options)))) - :component-did-update - (fn [this [_ prev-argv prev-more]] - (when (and @!player (not= prev-more (first (r/children this)))) - (.src @!player (apply array (map #(js-obj "type" % "src" (first (r/children this))) - (map #(get % "type") (get options "sources"))))) - (.ready @!player #(.play @!player)))) - :component-will-unmount - (fn [_] - (when @!player - (.dispose @!player))) - :reagent-render - (fn [options url] - [:video-js.vjs-default-skin.vjs-big-play-centered.bottom-0.object-cover.min-h-full.max-h-full.min-w-full])}))) diff --git a/src/frontend/tubo/components/video_player.cljs b/src/frontend/tubo/components/video_player.cljs new file mode 100644 index 0000000..0dd21d5 --- /dev/null +++ b/src/frontend/tubo/components/video_player.cljs @@ -0,0 +1,27 @@ +(ns tubo.components.video-player + (:require + [reagent.core :as r] + [reagent.dom :as rdom] + ["video.js" :as videojs])) + +(defn player + [options url] + (let [!player (atom nil)] + (r/create-class + {:display-name "StreamPlayer" + :component-did-mount + (fn [this] + (reset! !player (videojs (rdom/dom-node this) (clj->js options)))) + :component-did-update + (fn [this [_ prev-argv prev-more]] + (when (and @!player (not= prev-more (first (r/children this)))) + (.src @!player (apply array (map #(js-obj "type" % "src" (first (r/children this))) + (map #(get % "type") (get options "sources"))))) + (.ready @!player #(.play @!player)))) + :component-will-unmount + (fn [_] + (when @!player + (.dispose @!player))) + :reagent-render + (fn [options url] + [:video-js.vjs-default-skin.vjs-big-play-centered.bottom-0.object-cover.min-h-full.max-h-full.min-w-full.focus:ring-transparent])}))) diff --git a/src/frontend/tubo/events.cljs b/src/frontend/tubo/events.cljs index 46be7a4..54b1a37 100644 --- a/src/frontend/tubo/events.cljs +++ b/src/frontend/tubo/events.cljs @@ -56,6 +56,20 @@ (assoc db :show-mobile-nav (not (:show-mobile-nav db))))) (rf/reg-event-fx + ::toggle-loop-file + [(rf/inject-cofx :store)] + (fn [{:keys [db store]} _] + {:db (assoc db :loop-file (not (:loop-file db))) + :store (assoc store :loop-file (not (:loop-file store)))})) + +(rf/reg-event-fx + ::toggle-loop-playlist + [(rf/inject-cofx :store)] + (fn [{:keys [db store]} _] + {:db (assoc db :loop-playlist (not (:loop-playlist db))) + :store (assoc store :loop-playlist (not (:loop-playlist store)))})) + +(rf/reg-event-fx ::navigated (fn [{:keys [db]} [_ new-match]] (let [old-match (:current-match db) @@ -348,10 +362,10 @@ :db (assoc db :show-pagination-loading true))))) (rf/reg-event-fx - ::load-global-player-stream + ::load-audio-player-stream (fn [{:keys [db]} [_ res]] (let [stream-res (js->clj res :keywordize-keys true)] - {:db (assoc db :show-global-player-loading false) + {:db (assoc db :show-audio-player-loading false) :fx [[:dispatch [::change-media-queue-stream (-> stream-res :audio-streams first :content)]]]}))) @@ -362,7 +376,8 @@ {:db (assoc db :stream stream-res :show-page-loading false) :fx [[:dispatch [::change-stream-format nil]] - [:dispatch [::get-comments (:url stream-res)]] + (when (and (-> db :settings :show-comments)) + [:dispatch [::get-comments (:url stream-res)]]) [:dispatch [::set-service-styles stream-res]]]}))) (rf/reg-event-fx @@ -372,12 +387,12 @@ [::load-stream-page] [::bad-response]))) (rf/reg-event-fx - ::fetch-global-player-stream + ::fetch-audio-player-stream (fn [{:keys [db]} [_ uri]] (assoc (api/get-request (str "/api/streams/" (js/encodeURIComponent uri)) - [::load-global-player-stream] [::bad-response]) - :db (assoc db :show-global-player-loading true)))) + [::load-audio-player-stream] [::bad-response]) + :db (assoc db :show-audio-player-loading true)))) (rf/reg-event-fx ::get-stream-page diff --git a/src/frontend/tubo/subs.cljs b/src/frontend/tubo/subs.cljs index 0eb5cfb..1ca0067 100644 --- a/src/frontend/tubo/subs.cljs +++ b/src/frontend/tubo/subs.cljs @@ -115,14 +115,21 @@ (:media-queue-pos db))) (rf/reg-sub - :show-global-player + :media-queue-stream + (fn [_] + [(rf/subscribe [:media-queue]) (rf/subscribe [:media-queue-pos])]) + (fn [[queue pos] _] + (and (not-empty queue) (nth queue pos)))) + +(rf/reg-sub + :show-audio-player (fn [db _] - (:show-global-player db))) + (:show-audio-player db))) (rf/reg-sub - :show-global-player-loading + :show-audio-player-loading (fn [db _] - (:show-global-player-loading db))) + (:show-audio-player-loading db))) (rf/reg-sub :show-page-loading @@ -149,3 +156,12 @@ (fn [db _] (:settings db))) +(rf/reg-sub + :loop-file + (fn [db _] + (:loop-file db))) + +(rf/reg-sub + :loop-playlist + (fn [db _] + (:loop-playlist db))) diff --git a/src/frontend/tubo/views/stream.cljs b/src/frontend/tubo/views/stream.cljs index 8e81537..5c8acb8 100644 --- a/src/frontend/tubo/views/stream.cljs +++ b/src/frontend/tubo/views/stream.cljs @@ -7,7 +7,7 @@ [tubo.components.loading :as loading] [tubo.components.navigation :as navigation] [tubo.components.comments :as comments] - [tubo.components.player :as player] + [tubo.components.video-player :as player] [tubo.util :as util])) (defn stream @@ -17,7 +17,10 @@ description uploader-avatar uploader-name uploader-url upload-date related-streams thumbnail-url show-comments-loading comments-page - show-comments service-id] :as stream} @(rf/subscribe [:stream]) + show-comments show-related show-description service-id] + :as stream} @(rf/subscribe [:stream]) + {show-comments? :show-comments show-related? :show-related + show-description? :show-description} @(rf/subscribe [:settings]) available-streams (apply conj audio-streams video-streams) {:keys [content id] :as stream-format} @(rf/subscribe [:stream-format]) page-loading? @(rf/subscribe [:show-page-loading]) @@ -30,12 +33,12 @@ [:div.flex.justify-center.relative {:class "h-[300px] ml:h-[450px] lg:h-[600px]"} (when stream-format - [player/stream-player {"sources" [{"src" content "type" "video/mp4"} - {"src" content "type" "video/webm"}] - "poster" thumbnail-url - "controls" true - "responsive" true - "fill" true} + [player/player {"sources" [{"src" content "type" "video/mp4"} + {"src" content "type" "video/webm"}] + "poster" thumbnail-url + "controls" true + "responsive" true + "fill" true} content])] [:div.px-4.ml:p-0.overflow-x-hidden [:div.flex.flex.w-full.mt-3 @@ -53,7 +56,7 @@ {:style {:zIndex "-1"}} [:i.fa-solid.fa-caret-down]]]) [:button.border.rounded.border-black.px-3.py-1.dark:bg-stone-800 - {:on-click #(rf/dispatch [::events/switch-to-global-player + {:on-click #(rf/dispatch [::events/switch-to-audio-player {:uploader-name uploader-name :uploader-url uploader-url :name name :url url :stream content :service-color service-color}])} [:i.fa-solid.fa-headphones]] @@ -97,29 +100,38 @@ js/Date.parse js/Date. .toDateString)]])]] - [:div.min-w-full.py-3 - [:h1 name] - [:div {:dangerouslySetInnerHTML {:__html description}}]] - (when-not (empty? (:comments comments-page)) + (when show-description? + [:div.py-3.flex.flex-wrap.min-w-full + [:div {:dangerouslySetInnerHTML {:__html description} + :class (when (not show-description) "line-clamp-1")}] + [:div.flex.justify-center.font-bold.min-w-full.pt-4.cursor-pointer + [:button + {:on-click #(rf/dispatch [::events/toggle-stream-layout :show-description])} + (if (not show-description) "Show More" "Show Less")]]]) + (when (and comments-page (not (empty? (:comments comments-page))) show-comments?) [:div.py-6 [:div.flex.items-center [:i.fa-solid.fa-comments] [:p.px-2.py-4 "Comments"] (if show-comments - [:i.fa-solid.fa-chevron-up {:on-click #(rf/dispatch [::events/toggle-comments]) - :style {:cursor "pointer"}}] - [:i.fa-solid.fa-chevron-down {:on-click #(if (or show-comments comments-page) - (rf/dispatch [::events/toggle-comments]) - (rf/dispatch [::events/get-comments url])) - :style {:cursor "pointer"}}])] + [:i.fa-solid.fa-chevron-up.cursor-pointer + {:on-click #(rf/dispatch [::events/toggle-stream-layout :show-comments])}] + [:i.fa-solid.fa-chevron-down.cursor-pointer + {:on-click #(if (or show-comments comments-page) + (rf/dispatch [::events/toggle-stream-layout :show-comments]) + (rf/dispatch [::events/get-comments url]))}])] [:div (if show-comments-loading [loading/loading-icon service-color "text-2xl"] (when (and show-comments comments-page) [comments/comments comments-page uploader-name uploader-avatar url]))]]) - (when-not (empty? related-streams) + (when (and show-related? (not (empty? related-streams))) [:div.py-6 [:div.flex.items-center [:i.fa-solid.fa-list] - [:h1.px-2.text-lg.bold "Related Results"]] - [items/related-streams related-streams nil]])]]])])) + [:h1.px-2.text-lg.bold "Related Results"] + [:i.fa-solid.fa-chevron-up.cursor-pointer + {:class (if (not show-related) "fa-chevron-up" "fa-chevron-down") + :on-click #(rf/dispatch [::events/toggle-stream-layout :show-related])}]] + (when (not show-related) + [items/related-streams related-streams nil])])]]])])) |