aboutsummaryrefslogtreecommitdiff
path: root/src/frontend/tubo
diff options
context:
space:
mode:
Diffstat (limited to 'src/frontend/tubo')
-rw-r--r--src/frontend/tubo/components/audio_player.cljs65
-rw-r--r--src/frontend/tubo/components/play_queue.cljs117
-rw-r--r--src/frontend/tubo/events.cljs23
-rw-r--r--src/frontend/tubo/subs.cljs38
-rw-r--r--src/frontend/tubo/views.cljs2
5 files changed, 210 insertions, 35 deletions
diff --git a/src/frontend/tubo/components/audio_player.cljs b/src/frontend/tubo/components/audio_player.cljs
index fac2cf1..f4945c8 100644
--- a/src/frontend/tubo/components/audio_player.cljs
+++ b/src/frontend/tubo/components/audio_player.cljs
@@ -9,32 +9,36 @@
(defn player
[]
- (let [!player (r/atom nil)
- !elapsed-time (r/atom 0)
- !autoplay? (r/atom true)
+ (let [!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
+ {:keys [uploader-name uploader-url thumbnail-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])
+ show-media-queue? @(rf/subscribe [:show-media-queue])
is-window-visible @(rf/subscribe [:is-window-visible])
loop-file? @(rf/subscribe [:loop-file])
- loop-playlist? @(rf/subscribe [:loop-playlist])]
+ loop-playlist? @(rf/subscribe [:loop-playlist])
+ !elapsed-time @(rf/subscribe [:elapsed-time])
+ !player @(rf/subscribe [:player])]
(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.sticky.bottom-0.z-40.bg-white.dark:bg-neutral-900.px-3.py-5.sm:p-5.absolute.box-border.m-0
+ {:style {:borderTop (str "2px solid " service-color) :display (when show-media-queue? "none")}}
[:div.flex.items-center.justify-between
[:div.flex.items-center
- [:div.flex.flex-col
+ [:div {:style {:height "40px" :width "70px" :maxWidth "70px" :minWidth "70px"}}
+ [:img.min-h-full.max-h-full.object-cover.min-w-full.max-w-full.w-full {:src thumbnail-url}]]
+ [:div.flex.flex-col.px-4
[: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 %)
+ {:src stream
+ :ref #(reset! !player %)
:loop loop-file?
:on-time-update #(when (and @!player (> (.-readyState @!player) 0))
(reset! !elapsed-time (.-currentTime @!player)))
@@ -51,7 +55,10 @@
(set! (.-src @!player) (:stream (nth media-queue idx)))
(.play @!player))))}]]
[:div.flex
- [:button.focus:outline-none.mx-1.sm:mx-2
+ [:button:focus:ring-transparent.mx-2.cursor-pointer
+ {:on-click #(rf/dispatch [::events/toggle-media-queue])}
+ [:i.fa-solid.fa-list]]
+ [:button.hidden.ml:block.focus:outline-none.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))
@@ -60,7 +67,10 @@
(- media-queue-pos 1)])
(reset! !elapsed-time 0)))}
[:i.fa-solid.fa-backward-step]]
- [:button.focus:outline-none.mx-1.sm:mx-2
+ [:button.hidden.ml:block.focus:outline-none.mx-2
+ {:on-click #(set! (.-currentTime @!player) (- @!elapsed-time 5))}
+ [:i.fa-solid.fa-backward]]
+ [:button.focus:outline-none.mx-2
{:on-click #(when-let [player @!player]
(if (.-paused player)
(.play player)
@@ -72,7 +82,10 @@
[:i.fa-solid.fa-play]
[:i.fa-solid.fa-pause]))
[:i.fa-solid.fa-play])]
- [:button.focus:ring-transparent.mx-1.sm:mx-2
+ [:button.hidden.ml:block.focus:outline-none.mx-2
+ {:on-click #(set! (.-currentTime @!player) (+ @!elapsed-time 5))}
+ [:i.fa-solid.fa-forward]]
+ [:button.hidden.ml:block.focus:ring-transparent.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)))
@@ -81,14 +94,9 @@
(+ 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
+ [:div.flex.items-center
+ [:span.hidden.ml:block.mx-2 (if @!elapsed-time (util/format-duration @!elapsed-time) "00:00")]
+ [:input.hidden.ml:block.mx-2.w-20.ml:w-56.bg-gray-200.rounded-lg.cursor-pointer.focus:outline-none.h-1
{:type "range"
:on-input #(reset! !elapsed-time (.. % -target -value))
:on-change #(and @!player (> (.-readyState @!player) 0)
@@ -98,30 +106,33 @@
(.floor js/Math (.-duration @!player))
100)
:value @!elapsed-time}]
- [:button.focus:ring-transparent.mx-1.sm:mx-2
+ [:span.hidden.ml:block.mx-2 (if (and @!player (> (.-readyState @!player) 0))
+ (util/format-duration (.-duration @!player))
+ "00:00")]
+ [:button.hidden.ml:flex.focus:ring-transparent.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
+ [:button.hidden.ml:flex.focus:ring-transparent.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
+ [:div.hidden.ml:flex.items-center
+ [: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
+ [:input.w-20.bg-gray-200.rounded-lg.cursor-pointer.focus:outline-none.h-1.range-sm.mx-2
{: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
+ :value (if (and @!player (.-muted @!player)) 0 @!volume-level)}]]]
+ [:div.mx-2
[:i.fa-solid.fa-close.cursor-pointer
{:on-click (fn []
(rf/dispatch [::events/toggle-audio-player])
diff --git a/src/frontend/tubo/components/play_queue.cljs b/src/frontend/tubo/components/play_queue.cljs
new file mode 100644
index 0000000..98ce513
--- /dev/null
+++ b/src/frontend/tubo/components/play_queue.cljs
@@ -0,0 +1,117 @@
+(ns tubo.components.play-queue
+ (:require
+ [re-frame.core :as rf]
+ [reitit.frontend.easy :as rfe]
+ [tubo.components.items :as items]
+ [tubo.events :as events]
+ [tubo.util :as util]))
+
+(defn queue
+ []
+ (let [show-media-queue @(rf/subscribe [:show-media-queue])
+ 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])
+ !elapsed-time @(rf/subscribe [:elapsed-time])
+ !player @(rf/subscribe [:player])
+ loop-file? @(rf/subscribe [:loop-file])
+ loop-playlist? @(rf/subscribe [:loop-playlist])]
+ (when (and show-media-queue media-queue)
+ [:div.fixed.flex.flex-col.items-center.px-5.py-2.min-w-full.w-full.z-30
+ {:style {:minHeight "calc(100vh - 56px)" :height "calc(100vh - 56px)"}
+ :class "dark:bg-neutral-900/90 bg-white/90 backdrop-blur"}
+ [:div.flex.justify-between.pl-4.items-center.w-full.shrink-0
+ {:class "ml:w-4/5 xl:w-3/5"}
+ [:h1.text-2xl.font-bold.py-6 "Play Queue"]
+ [:div.mx-2
+ [:i.fa-solid.fa-close.cursor-pointer
+ {:on-click #(rf/dispatch [::events/toggle-media-queue])}]]]
+ [:div.flex.flex-col.p-4.w-full.overflow-y-auto.flex-auto
+ {:class "ml:w-4/5 xl:w-3/5"}
+ [:div
+ (for [[i {:keys [uploader-name uploader-url name duration
+ stream url service-color thumbnail-url]}] (map-indexed vector media-queue)]
+ (let [service-name (case service-color
+ "#cc0000" "YouTube"
+ "#ff7700" "SoundCloud"
+ "#333333" "media.ccc.de"
+ "#F2690D" "PeerTube"
+ "#629aa9" "Bandcamp")]
+ [:div.flex.w-full.rounded.px-2.cursor-pointer
+ {:key i
+ :class (when (= i media-queue-pos) "bg-[#f0f0f0] dark:bg-stone-800")
+ :on-click #(do
+ (rf/dispatch [::events/change-media-queue-pos i])
+ (reset! !elapsed-time 0))}
+ [:div.xs:w-56.items-center
+ [items/thumbnail thumbnail-url nil url name duration]]
+ [:div.flex.flex-col.px-4.py-2.w-full
+ [:h1.pb-4.line-clamp-2 name]
+ [:h3.text-neutral-600.dark:text-neutral-300.text-sm
+ [:span.pr-2 uploader-name]
+ [:span {:dangerouslySetInnerHTML {:__html "&bull;"}}]
+ [:span.pl-2 service-name]]]]))]]
+ [:div.flex.flex-col.p-4.w-full.shrink-0
+ {:class "ml:w-4/5 xl:w-3/5"}
+ [:div.flex.flex-col.w-full.py-2
+ [:a.text-md.line-clamp-1
+ {:href (rfe/href :tubo.routes/stream nil {:url url})} name]
+ [:a.text-sm.pt-2.text-neutral-600.dark:text-neutral-300.line-clamp-1
+ {:href (rfe/href :tubo.routes/channel nil {:url uploader-url})} uploader-name]]
+ [:div.flex.flex-auto.py-2.w-full.items-center
+ [:span (if @!elapsed-time (util/format-duration @!elapsed-time) "00:00")]
+ [:input.mx-2.bg-gray-200.rounded-lg.cursor-pointer.focus:outline-none.w-full.h-1
+ {: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}]
+ [:span (if (and @!player (> (.-readyState @!player) 0))
+ (util/format-duration (.-duration @!player))
+ "00:00")]]
+ [:div.flex.justify-center.items-center
+ [:button.focus:ring-transparent.mx-2
+ {:on-click #(rf/dispatch [::events/toggle-loop-file])}
+ [:i.fa-solid.fa-repeat
+ {:style {:color (when loop-file? service-color)}}]]
+ [:button.focus:outline-none.mx-2.text-xl
+ {: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-2.text-xl
+ {:on-click #(set! (.-currentTime @!player) (- @!elapsed-time 5))}
+ [:i.fa-solid.fa-backward]]
+ [:button.focus:outline-none.mx-2.text-3xl
+ {:on-click #(when-let [player @!player]
+ (if (.-paused player)
+ (.play player)
+ (.pause player)))}
+ (if (and @!player (.-paused @!player))
+ [:i.fa-solid.fa-play]
+ [:i.fa-solid.fa-pause])]
+ [:button.focus:outline-none.mx-2.text-xl
+ {:on-click #(set! (.-currentTime @!player) (+ @!elapsed-time 5))}
+ [:i.fa-solid.fa-forward]]
+ [:button.focus:ring-transparent.mx-2.text-xl
+ {: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]]
+ [:button.focus:ring-transparent.mx-2
+ {:on-click #(rf/dispatch [::events/toggle-loop-playlist])}
+ [:i.fa-solid.fa-retweet
+ {:style {:color (when loop-playlist? service-color)}}]]]]])))
diff --git a/src/frontend/tubo/events.cljs b/src/frontend/tubo/events.cljs
index 54b1a37..2d5b549 100644
--- a/src/frontend/tubo/events.cljs
+++ b/src/frontend/tubo/events.cljs
@@ -45,15 +45,27 @@
(fn [_]
(.back js/window.history)))
+(rf/reg-fx
+ ::body-overflow!
+ (fn [active]
+ (set! (.. js/document.body -style -overflow) (if active "hidden" "auto"))))
+
(rf/reg-event-fx
::history-back
(fn [_ _]
{::history-back! nil}))
-(rf/reg-event-db
+(rf/reg-event-fx
::toggle-mobile-nav
- (fn [db _]
- (assoc db :show-mobile-nav (not (:show-mobile-nav db)))))
+ (fn [{:keys [db]} _]
+ {:db (assoc db :show-mobile-nav (not (:show-mobile-nav db)))
+ ::body-overflow! (not (:show-mobile-nav db))}))
+
+(rf/reg-event-fx
+ ::toggle-media-queue
+ (fn [{:keys [db]} _]
+ {:db (assoc db :show-media-queue (not (:show-media-queue db)))
+ ::body-overflow! (not (:show-media-queue db))}))
(rf/reg-event-fx
::toggle-loop-file
@@ -77,8 +89,11 @@
match (assoc new-match :controllers controllers)]
{:db (-> db
(assoc :current-match match)
+ (assoc :show-media-queue false)
+ (assoc :show-mobile-nav false)
(assoc :show-pagination-loading false))
- ::scroll-to-top nil})))
+ ::scroll-to-top nil
+ ::body-overflow! false})))
(rf/reg-event-fx
::navigate
diff --git a/src/frontend/tubo/subs.cljs b/src/frontend/tubo/subs.cljs
index 1ca0067..ed51d60 100644
--- a/src/frontend/tubo/subs.cljs
+++ b/src/frontend/tubo/subs.cljs
@@ -3,13 +3,13 @@
[reagent.core :as r]
[re-frame.core :as rf]))
-(defonce is-window-visible
+(defonce !is-window-visible
(let [a (r/atom true)]
(.addEventListener js/window "focus" #(reset! a true))
(.addEventListener js/window "blur" #(reset! a false))
a))
-(defonce scroll-distance
+(defonce !scroll-distance
(let [a (r/atom 0)
compute-scroll-distance #(when (> (.-scrollY js/window) 0)
(reset! a (+ (.-scrollY js/window) (.-innerHeight js/window))))]
@@ -17,15 +17,28 @@
(.addEventListener js/window "touchmove" compute-scroll-distance)
a))
+(def !elapsed-time (r/atom 0))
+(def !player (r/atom nil))
+
(rf/reg-sub
:is-window-visible
(fn [_ _]
- @is-window-visible))
+ @!is-window-visible))
(rf/reg-sub
:scrolled-to-bottom
(fn [_ _]
- (> (+ @scroll-distance 35) (.-scrollHeight js/document.body))))
+ (> (+ @!scroll-distance 35) (.-scrollHeight js/document.body))))
+
+(rf/reg-sub
+ :elapsed-time
+ (fn [db _]
+ !elapsed-time))
+
+(rf/reg-sub
+ :player
+ (fn [db _]
+ !player))
(rf/reg-sub
:http-response
@@ -80,6 +93,18 @@
4 "#629aa9")))
(rf/reg-sub
+ :service-name
+ (fn [_]
+ (rf/subscribe [:service-id]))
+ (fn [id _]
+ (case id
+ 0 "YouTube"
+ 1 "SoundCloud"
+ 2 "media.ccc.de"
+ 3 "PeerTube"
+ 4 "Bandcamp")))
+
+(rf/reg-sub
:services
(fn [db _]
(:services db)))
@@ -147,6 +172,11 @@
(:show-mobile-nav db)))
(rf/reg-sub
+ :show-media-queue
+ (fn [db _]
+ (:show-media-queue db)))
+
+(rf/reg-sub
:theme
(fn [db _]
(:theme db)))
diff --git a/src/frontend/tubo/views.cljs b/src/frontend/tubo/views.cljs
index 5978808..736a60d 100644
--- a/src/frontend/tubo/views.cljs
+++ b/src/frontend/tubo/views.cljs
@@ -5,6 +5,7 @@
[reagent.core :as r]
[tubo.components.navigation :as navigation]
[tubo.components.audio-player :as player]
+ [tubo.components.play-queue :as queue]
[tubo.events :as events]
[tubo.routes :as routes]))
@@ -152,4 +153,5 @@
(when-let [view (-> current-match :data :view)]
[view current-match])
[footer]
+ [queue/queue]
[player/player]]]]))