aboutsummaryrefslogtreecommitdiff
path: root/src/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'src/frontend')
-rw-r--r--src/frontend/tubo/components/audio_player.cljs128
-rw-r--r--src/frontend/tubo/components/items.cljs2
-rw-r--r--src/frontend/tubo/components/player.cljs128
-rw-r--r--src/frontend/tubo/components/video_player.cljs27
-rw-r--r--src/frontend/tubo/events.cljs27
-rw-r--r--src/frontend/tubo/subs.cljs24
-rw-r--r--src/frontend/tubo/views/stream.cljs56
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])])]]])]))