aboutsummaryrefslogtreecommitdiff
path: root/src/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'src/frontend')
-rw-r--r--src/frontend/tubo/components/audio_player.cljs243
-rw-r--r--src/frontend/tubo/components/play_queue.cljs48
-rw-r--r--src/frontend/tubo/events.cljs52
3 files changed, 170 insertions, 173 deletions
diff --git a/src/frontend/tubo/components/audio_player.cljs b/src/frontend/tubo/components/audio_player.cljs
index 7cc5fb1..ae4c4e4 100644
--- a/src/frontend/tubo/components/audio_player.cljs
+++ b/src/frontend/tubo/components/audio_player.cljs
@@ -9,126 +9,123 @@
(defn player
[]
- (let [!autoplay? (r/atom true)]
- (fn []
- (let [media-queue @(rf/subscribe [:media-queue])
- media-queue-pos @(rf/subscribe [:media-queue-pos])
- {: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])
- volume-level @(rf/subscribe [:volume-level])
- muted? @(rf/subscribe [:muted])
- !elapsed-time @(rf/subscribe [:elapsed-time])
- !player @(rf/subscribe [:player])]
- (when show-audio-player?
- [:div.sticky.bottom-0.z-40.bg-white.dark:bg-neutral-900.p-3.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 {: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-2
- [: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))
- (rf/dispatch [::events/start-playback @!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)))
- (rf/dispatch [::events/start-playback @!player]))))}]]
- [:div.flex
- [: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))
- #(do
- (rf/dispatch [::events/change-media-queue-pos
- (- media-queue-pos 1)])
- (reset! !elapsed-time 0)))}
- [:i.fa-solid.fa-backward-step]]
- [: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 #(rf/dispatch [::events/start-playback @!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.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)))
- #(do
- (rf/dispatch [::events/change-media-queue-pos
- (+ media-queue-pos 1)])
- (reset! !elapsed-time 0)))}
- [:i.fa-solid.fa-forward-step]]
- [: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)
- (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.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.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.ml:flex.items-center
- [:button.focus:outline-none.mx-2
- {:on-click #(rf/dispatch [::events/toggle-mute @!player])}
- (if (or (and @!player muted?))
- [: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.h-1.range-sm.mx-2
- {:type "range"
- :on-input #(rf/dispatch [::events/change-volume-level (.. % -target -value) @!player])
- :style {:accentColor service-color}
- :max 100
- :value volume-level}]]]
- [:div.mx-2
- [:i.fa-solid.fa-close.cursor-pointer
- {:on-click (fn []
- (rf/dispatch [::events/toggle-audio-player])
- (.pause @!player))}]]]]])))))
+ (let [media-queue @(rf/subscribe [:media-queue])
+ media-queue-pos @(rf/subscribe [:media-queue-pos])
+ {: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])
+ volume-level @(rf/subscribe [:volume-level])
+ muted? @(rf/subscribe [:muted])
+ !elapsed-time @(rf/subscribe [:elapsed-time])
+ !player @(rf/subscribe [:player])
+ player-ready? (and @!player (> (.-readyState @!player) 0))]
+ (when show-audio-player?
+ [:div.sticky.bottom-0.z-40.bg-white.dark:bg-neutral-900.p-3.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 {: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-2
+ [: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 player-ready?
+ (reset! !elapsed-time (.-currentTime @!player)))
+ :on-loaded-data #(do (.play @!player)
+ (set! (.-currentTime @!player) @!elapsed-time))
+ :on-ended #(when player-ready?
+ (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])
+ (when (and (not is-window-visible) loop-playlist?)
+ (set! (.-src @!player) (:stream (nth media-queue idx)))
+ (.play @!player))))}]]
+ [:div.flex
+ [: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))
+ (rf/dispatch [::events/change-media-queue-pos
+ (- media-queue-pos 1)]))}
+ [:i.fa-solid.fa-backward-step]]
+ [: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 #(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.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)))
+ (rf/dispatch [::events/change-media-queue-pos
+ (+ media-queue-pos 1)]))}
+ [:i.fa-solid.fa-forward-step]]
+ [: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)
+ (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.hidden.ml:block.mx-2
+ (if player-ready? (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.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.ml:flex.items-center
+ [:button.focus:outline-none.mx-2
+ {:on-click #(rf/dispatch [::events/toggle-mute @!player])}
+ (if (or (and @!player muted?))
+ [: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.h-1.range-sm.mx-2
+ {:type "range"
+ :on-input #(rf/dispatch [::events/change-volume-level (.. % -target -value) @!player])
+ :style {:accentColor service-color}
+ :max 100
+ :value volume-level}]]]
+ [:div.mx-2
+ [:i.fa-solid.fa-close.cursor-pointer
+ {:on-click (fn []
+ (rf/dispatch [::events/toggle-audio-player])
+ (.pause @!player)
+ (set! (.-currentTime @!player) 0))}]]]]])))
diff --git a/src/frontend/tubo/components/play_queue.cljs b/src/frontend/tubo/components/play_queue.cljs
index 258d06e..bf9f433 100644
--- a/src/frontend/tubo/components/play_queue.cljs
+++ b/src/frontend/tubo/components/play_queue.cljs
@@ -3,12 +3,14 @@
[re-frame.core :as rf]
[reitit.frontend.easy :as rfe]
[tubo.components.items :as items]
+ [tubo.components.loading :as loading]
[tubo.events :as events]
[tubo.util :as util]))
(defn queue
[]
(let [show-media-queue @(rf/subscribe [:show-media-queue])
+ show-audio-player-loading? @(rf/subscribe [:show-audio-player-loading])
media-queue @(rf/subscribe [:media-queue])
media-queue-pos @(rf/subscribe [:media-queue-pos])
{:keys [uploader-name uploader-url
@@ -16,7 +18,8 @@
!elapsed-time @(rf/subscribe [:elapsed-time])
!player @(rf/subscribe [:player])
loop-file? @(rf/subscribe [:loop-file])
- loop-playlist? @(rf/subscribe [:loop-playlist])]
+ loop-playlist? @(rf/subscribe [:loop-playlist])
+ player-ready? (and @!player (> (.-readyState @!player) 0))]
(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)"}
@@ -40,9 +43,7 @@
[:div.flex.w-full.h-24.rounded.px-2.cursor-pointer.my-2
{: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))}
+ :on-click #(rf/dispatch [::events/change-media-queue-pos i])}
[:div.w-56
[items/thumbnail thumbnail-url nil url name duration {:classes "h-24"}]]
[:div.flex.flex-col.px-4.py-2.w-full
@@ -63,14 +64,14 @@
[: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))
+ :on-change #(when player-ready?
+ (set! (.-currentTime @!player) @!elapsed-time))
:style {:accentColor service-color}
- :max (if (and @!player (> (.-readyState @!player) 0))
+ :max (if player-ready?
(.floor js/Math (.-duration @!player))
100)
:value @!elapsed-time}]
- [:span (if (and @!player (> (.-readyState @!player) 0))
+ [:span (if player-ready?
(util/format-duration (.-duration @!player))
"00:00")]]
[:div.flex.justify-center.items-center
@@ -81,34 +82,31 @@
[: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)))}
+ :on-click #(when (and media-queue (not= media-queue-pos 0))
+ (rf/dispatch [::events/change-media-queue-pos
+ (- media-queue-pos 1)]))}
[: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])]
+ {:on-click #(if (.-paused @!player)
+ (.play @!player)
+ (.pause @!player))}
+ (if show-audio-player-loading?
+ [loading/loading-icon service-color "text-3xl"]
+ (if (.-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)))}
+ :on-click #(when (and media-queue (< (+ media-queue-pos 1) (count media-queue)))
+ (rf/dispatch [::events/change-media-queue-pos
+ (+ media-queue-pos 1)]))}
[:i.fa-solid.fa-forward-step]]
[:button.focus:ring-transparent.mx-2
{:on-click #(rf/dispatch [::events/toggle-loop-playlist])}
diff --git a/src/frontend/tubo/events.cljs b/src/frontend/tubo/events.cljs
index 8254cd4..79c9e93 100644
--- a/src/frontend/tubo/events.cljs
+++ b/src/frontend/tubo/events.cljs
@@ -72,9 +72,9 @@
(rf/reg-fx
::player-playback
- (fn [{:keys [player]}]
+ (fn [{:keys [player paused?]}]
(when (and player (> (.-readyState player) 0))
- (if (.-paused player)
+ (if paused?
(.play player)
(.pause player)))))
@@ -106,13 +106,6 @@
::player-volume {:player player :volume value}}))
(rf/reg-event-fx
- ::start-playback
- (fn [{:keys [db]} [_ player]]
- {::player-playback {:player player}
- ::player-volume {:player player :volume (:volume-level db)}
- ::player-mute {:player player :muted? (:muted db)}}))
-
-(rf/reg-event-fx
::toggle-mute
[(rf/inject-cofx :store)]
(fn [{:keys [db store]} [_ player]]
@@ -238,27 +231,28 @@
::add-to-media-queue
[(rf/inject-cofx :store)]
(fn [{:keys [db store]} [_ stream]]
- (let [updated-db (update db :media-queue conj stream)
- idx (.indexOf (:media-queue updated-db) stream)]
+ (let [updated-db (update db :media-queue conj stream)]
{:db updated-db
- :store (assoc store :media-queue (:media-queue updated-db))
- :fx [[:dispatch [::fetch-audio-player-stream (:url stream) idx]]]})))
+ :store (assoc store :media-queue (:media-queue updated-db))})))
(rf/reg-event-fx
::change-media-queue-pos
[(rf/inject-cofx :store)]
(fn [{:keys [db store]} [_ idx]]
- {:db (assoc db :media-queue-pos idx)
- :store (assoc store :media-queue-pos idx)}))
+ (let [stream (get (:media-queue db) idx)]
+ {:db (-> db
+ (assoc :media-queue-pos idx)
+ (assoc-in [:media-queue idx :stream] ""))
+ :store (assoc store :media-queue-pos idx)
+ :fx [[:dispatch [::fetch-audio-player-stream (:url stream) idx]]]})))
(rf/reg-event-fx
::change-media-queue-stream
[(rf/inject-cofx :store)]
(fn [{:keys [db store]} [_ src idx]]
(let [update-entry #(assoc-in % [:media-queue idx :stream] src)]
- (when-not (-> db :media-queue (nth idx) :stream)
- {:db (update-entry db)
- :store (update-entry store)}))))
+ {:db (update-entry db)
+ :store (update-entry store)})))
(rf/reg-event-fx
::toggle-audio-player
@@ -277,9 +271,15 @@
::switch-to-audio-player
[(rf/inject-cofx :store)]
(fn [{:keys [db store]} [_ stream service-color]]
- {:db (assoc db :show-audio-player true)
- :store (assoc store :show-audio-player true)
- :fx [[:dispatch [::add-to-media-queue (conj stream {:service-color service-color})]]]}))
+ (let [full-stream (conj {:service-color service-color} stream)
+ updated-db (update db :media-queue conj full-stream)
+ idx (.indexOf (:media-queue updated-db) full-stream)]
+ {:db (-> updated-db
+ (assoc :show-audio-player true))
+ :store (-> store
+ (assoc :show-audio-player true)
+ (assoc :media-queue (:media-queue updated-db)))
+ :fx [[:dispatch [::fetch-audio-player-stream (:url stream) idx]]]})))
(rf/reg-event-fx
::enqueue-related-streams
@@ -287,10 +287,12 @@
(fn [{:keys [db store]} [_ streams service-color]]
{:db (assoc db :show-audio-player true)
:store (assoc store :show-audio-player true)
- :fx (into [] (map #(identity [:dispatch
- [::add-to-media-queue
- (conj {:service-color service-color} %)]])
- streams))}))
+ :fx (into [] (conj
+ (map #(identity [:dispatch
+ [::add-to-media-queue
+ (conj {:service-color service-color} %)]])
+ streams)
+ [:dispatch [::fetch-audio-player-stream (:url (first streams)) 0]]))}))
(rf/reg-event-fx
::add-to-bookmarks