From 472e4adf02453f69a428fb80bb5fcdae1390925d Mon Sep 17 00:00:00 2001 From: Miguel Ángel Moreno Date: Fri, 19 Apr 2024 15:57:29 +0200 Subject: feat: refine media queue component and handlers --- src/frontend/tubo/components/audio_player.cljs | 11 +- src/frontend/tubo/components/layout.cljs | 4 +- src/frontend/tubo/components/play_queue.cljs | 188 +++++++++++++++---------- src/frontend/tubo/events.cljs | 12 +- 4 files changed, 128 insertions(+), 87 deletions(-) (limited to 'src/frontend') diff --git a/src/frontend/tubo/components/audio_player.cljs b/src/frontend/tubo/components/audio_player.cljs index 149c963..f2e4943 100644 --- a/src/frontend/tubo/components/audio_player.cljs +++ b/src/frontend/tubo/components/audio_player.cljs @@ -95,14 +95,16 @@ muted? @(rf/subscribe [:muted]) bookmarks @(rf/subscribe [:bookmarks]) !player @(rf/subscribe [:player]) + media-queue-pos @(rf/subscribe [:media-queue-pos]) {:keys [theme]} @(rf/subscribe [:settings]) service-color (and service-id (utils/get-service-color service-id)) bg-color (str "rgba(" (if (= theme "dark") "23, 23, 23" "255, 255, 255") ", 0.95)") liked? (some #(= (:url %) url) (-> bookmarks first :items))] (when show-audio-player? - [:div.sticky.bottom-0.z-10.p-3.absolute.box-border.m-0 + [:div.sticky.bottom-0.z-10.p-3.absolute.box-border.m-0.transition-all.ease-in.delay-0 {:style - {:display (when show-media-queue? "none") + {:visibility (when show-media-queue? "hidden") + :opacity (if show-media-queue? 0 1) :background-image (str "linear-gradient(0deg, " bg-color "," bg-color "), url(\"" thumbnail-url "\")") :backgroundSize "cover" :backgroundPosition "center" @@ -120,7 +122,7 @@ [main-controls service-color] [:div.flex.lg:justify-end.lg:flex-1 [player/volume-slider !player volume-level muted? service-color] - [player/button [:i.fa-solid.fa-list] #(rf/dispatch [::events/toggle-media-queue]) + [player/button [:i.fa-solid.fa-list] #(rf/dispatch [::events/show-media-queue true]) :show-on-mobile? true :extra-classes "pl-4 pr-3"] [layout/popover-menu !menu-active? @@ -134,6 +136,9 @@ :icon [:i.fa-solid.fa-plus] :on-click #(rf/dispatch [::events/add-bookmark-list-modal [bookmarks/add-to-bookmark-list-modal current-stream]])} + {:label "Remove from queue" + :icon [:i.fa-solid.fa-trash] + :on-click #(rf/dispatch [::events/remove-from-media-queue media-queue-pos])} {:label "Close player" :icon [:i.fa-solid.fa-close] :on-click #(rf/dispatch [::events/dispose-audio-player])}] diff --git a/src/frontend/tubo/components/layout.cljs b/src/frontend/tubo/components/layout.cljs index 910d9fd..7fc8853 100644 --- a/src/frontend/tubo/components/layout.cljs +++ b/src/frontend/tubo/components/layout.cljs @@ -5,12 +5,12 @@ [tubo.utils :as utils])) (defn thumbnail - [thumbnail-url route name duration & {:keys [classes] :or {classes "h-44 xs:h-28"}}] + [thumbnail-url route name duration & {:keys [classes rounded?] :or {classes "h-44 xs:h-28" rounded? true}}] [:div.flex.py-2.box-border {:class classes} [:div.relative.min-w-full [:a.absolute.min-w-full.min-h-full.z-10 {:href route :title name}] (if thumbnail-url - [:img.rounded.object-cover.min-h-full.max-h-full.min-w-full {:src thumbnail-url}] + [:img.object-cover.min-h-full.max-h-full.min-w-full {:src thumbnail-url :class (when rounded? "rounded")}] [:div.bg-gray-300.flex.min-h-full.min-w-full.justify-center.items-center.rounded [:i.fa-solid.fa-image.text-3xl.text-white]]) (when duration diff --git a/src/frontend/tubo/components/play_queue.cljs b/src/frontend/tubo/components/play_queue.cljs index 4b93ce4..ddb81d5 100644 --- a/src/frontend/tubo/components/play_queue.cljs +++ b/src/frontend/tubo/components/play_queue.cljs @@ -1,7 +1,9 @@ (ns tubo.components.play-queue (:require + [reagent.core :as r] [re-frame.core :as rf] [reitit.frontend.easy :as rfe] + [tubo.components.modals.bookmarks :as bookmarks] [tubo.components.items :as items] [tubo.components.layout :as layout] [tubo.components.player :as player] @@ -9,19 +11,48 @@ [tubo.utils :as utils])) (defn play-queue-item - [{:keys [service-id uploader-name uploader-url name duration - stream url service-color thumbnail-url]} media-queue-pos i] - [:div.flex.w-full.h-24.rounded.cursor-pointer.px-2.my-1 - {:class (when (= i media-queue-pos) "bg-[#f0f0f0] dark:bg-stone-800") - :on-click #(rf/dispatch [::events/change-media-queue-pos i])} - [:div.w-56 - [layout/thumbnail thumbnail-url nil name duration {:classes "h-24"}]] - [:div.flex.flex-col.px-4.py-2.w-full - [:h1.line-clamp-1 name] - [:div.text-neutral-600.dark:text-neutral-300.text-sm.flex.flex-col.xs:flex-row - [:span.line-clamp-1 uploader-name] - [:span.px-2.hidden.xs:inline-block {:dangerouslySetInnerHTML {:__html "•"}}] - [:span (utils/get-service-name service-id)]]]]) + [item media-queue-pos i bookmarks] + (let [!menu-active? (r/atom false)] + (fn [{:keys [service-id uploader-name uploader-url name duration + stream url thumbnail-url] :as item} + media-queue-pos i bookmarks] + (let [liked? (some #(= (:url %) url) (-> bookmarks first :items)) + media-queue-pos @(rf/subscribe [:media-queue-pos]) + media-queue @(rf/subscribe [:media-queue])] + [:div.relative.w-full + {:ref #(when (and (> (count media-queue) 0) (= media-queue-pos i)) + (rf/dispatch [::events/scroll-into-view %]))} + [:div.flex.cursor-pointer.py-2 + {:class (when (= i media-queue-pos) "bg-[#f0f0f0] dark:bg-stone-800") + :on-click #(rf/dispatch [::events/change-media-queue-pos i])} + [:div.flex.items-center.justify-center.min-w-20.w-20.xs:min-w-28.xs:w-28 + [:span.font-bold.text-neutral-400.text-sm + (if (= i media-queue-pos) [:i.fa-solid.fa-play] (inc i))]] + [:div.w-36 + [layout/thumbnail thumbnail-url nil name duration :classes "h-16 !p-0" :rounded? false]] + [:div.flex.flex-col.pl-4.pr-12.w-full + [:h1.line-clamp-1 {:title name} name] + [:div.text-neutral-600.dark:text-neutral-300.text-sm.flex.flex-col.xs:flex-row + [:span.line-clamp-1 {:title uploader-name} uploader-name] + [:span.px-2.hidden.xs:inline-block {:dangerouslySetInnerHTML {:__html "•"}}] + [:span (utils/get-service-name service-id)]]]] + [:div.absolute.right-0.top-0.min-h-full.flex.items-center + [layout/popover-menu !menu-active? + [{:label (if liked? "Remove favorite" "Favorite") + :icon [:i.fa-solid.fa-heart (when liked? {:style {:color (utils/get-service-color service-id)}})] + :on-click #(rf/dispatch [(if liked? ::events/remove-from-likes ::events/add-to-likes) item])} + {:label "Play radio" + :icon [:i.fa-solid.fa-tower-cell] + :on-click #(rf/dispatch [::events/start-stream-radio item])} + {:label "Add to playlist" + :icon [:i.fa-solid.fa-plus] + :on-click #(rf/dispatch [::events/add-bookmark-list-modal + [bookmarks/add-to-bookmark-list-modal item]])} + {:label "Remove from queue" + :icon [:i.fa-solid.fa-trash] + :on-click #(rf/dispatch [::events/remove-from-media-queue i])}] + :menu-styles {:right "40px"} + :extra-classes "px-7 py-2"]]])))) (defn queue [] @@ -36,66 +67,71 @@ !elapsed-time @(rf/subscribe [:elapsed-time]) !player @(rf/subscribe [:player]) loop-playback @(rf/subscribe [:loop-playback]) - 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-10 - {:style {:minHeight "calc(100dvh - 56px)" :height "calc(100dvh - 56px)"} - :class "dark:bg-neutral-900/90 bg-white/90 backdrop-blur"} - [layout/focus-overlay #(rf/dispatch [::events/toggle-media-queue]) show-media-queue true] - [:div.z-20.w-full.flex.flex-col.flex-auto.h-full - {:class "lg:w-4/5 xl:w-3/5"} - [:div.flex.justify-between.items-center.shrink-0 - [:h1.text-2xl.font-bold.py-6 "Play Queue"] - [:button.p-2.text-xl - {:on-click #(rf/dispatch [::events/toggle-media-queue])} - [:i.fa-solid.fa-close]]] - [:div.flex.flex-col.pr-2.overflow-y-auto.flex-auto - (for [[i item] (map-indexed vector media-queue)] - ^{:key i} [play-queue-item item media-queue-pos i])] - [:div.flex.flex-col.py-4.shrink-0 - [: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.mr-2 (if @!elapsed-time (utils/format-duration @!elapsed-time) "00:00")] - [player/time-slider !player !elapsed-time service-color] - [:span.ml-2 (if player-ready? (utils/format-duration (.-duration @!player)) "00:00")]] - [:div.flex.justify-center.items-center - [player/loop-button loop-playback service-color true] - [player/button - [:i.fa-solid.fa-backward-step] - #(when (and media-queue (not= media-queue-pos 0)) - (rf/dispatch [::events/change-media-queue-pos - (- media-queue-pos 1)])) - :disabled? (not (and media-queue (not= media-queue-pos 0))) - :extra-classes "text-xl" - :show-on-mobile? true] - [player/button - [:i.fa-solid.fa-backward] - #(set! (.-currentTime @!player) (- @!elapsed-time 5)) - :extra-classes "text-xl" - :show-on-mobile? true] - [player/button - (if (or loading? (not @!player)) - [layout/loading-icon service-color "text-3xl"] - (if paused? - [:i.fa-solid.fa-play] - [:i.fa-solid.fa-pause])) - #(rf/dispatch [::events/set-player-paused (not paused?)]) - :extra-classes "text-3xl" - :show-on-mobile? true] - [player/button - [:i.fa-solid.fa-forward] - #(set! (.-currentTime @!player) (+ @!elapsed-time 5)) - :extra-classes "text-xl" - :show-on-mobile? true] - [player/button - [:i.fa-solid.fa-forward-step] - #(when (and media-queue (< (+ media-queue-pos 1) (count media-queue))) - (rf/dispatch [::events/change-media-queue-pos - (+ media-queue-pos 1)])) - :disabled? (not (and media-queue (< (+ media-queue-pos 1) (count media-queue)))) - :extra-classes "text-xl" - :show-on-mobile? true]]]]]))) + player-ready? @(rf/subscribe [:player-ready]) + bookmarks @(rf/subscribe [:bookmarks])] + [:div.fixed.flex.flex-col.items-center.min-w-full.w-full.z-10.backdrop-blur + {:style {:minHeight "calc(100dvh - 56px)" + :height "calc(100dvh - 56px)" + :visibility (when-not show-media-queue "hidden") + :opacity (if show-media-queue 1 0)} + :class "dark:bg-neutral-900/90 bg-white/90 backdrop-blur"} + [layout/focus-overlay #(rf/dispatch [::events/show-media-queue false]) show-media-queue true] + [:div.z-20.w-full.flex.flex-col.flex-auto.h-full.lg:pt-5 + {:class "lg:w-4/5 xl:w-3/5"} + [:div.flex.flex-col.overflow-y-auto.flex-auto.gap-y-1 + (for [[i item] (map-indexed vector media-queue)] + ^{:key i} [play-queue-item item media-queue-pos i bookmarks])] + [:div.flex.flex-col.py-4.shrink-0.px-5 + [:div.flex.flex-col.w-full.py-2 + [:a.text-md.line-clamp-1 + {:href (rfe/href :tubo.routes/stream nil {:url url}) + :title name} + 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}) + :title uploader-name} + uploader-name]] + [:div.flex.flex-auto.py-2.w-full.items-center.text-sm + [:span.mr-4 (if (and @!elapsed-time @!player) (utils/format-duration @!elapsed-time) "00:00")] + [player/time-slider !player !elapsed-time service-color] + [:span.ml-4 (if (and player-ready? @!player) (utils/format-duration (.-duration @!player)) "00:00")]] + [:div.flex.justify-center.items-center + [player/loop-button loop-playback service-color true] + [player/button + [:i.fa-solid.fa-backward-step] + #(when (and media-queue (not= media-queue-pos 0)) + (rf/dispatch [::events/change-media-queue-pos + (- media-queue-pos 1)])) + :disabled? (not (and media-queue (not= media-queue-pos 0))) + :extra-classes "text-xl" + :show-on-mobile? true] + [player/button + [:i.fa-solid.fa-backward] + #(set! (.-currentTime @!player) (- @!elapsed-time 5)) + :extra-classes "text-xl" + :show-on-mobile? true] + [player/button + (if (or loading? (not player-ready?)) + [layout/loading-icon service-color "text-3xl"] + (if paused? + [:i.fa-solid.fa-play] + [:i.fa-solid.fa-pause])) + #(rf/dispatch [::events/set-player-paused (not paused?)]) + :extra-classes "text-3xl" + :show-on-mobile? true] + [player/button + [:i.fa-solid.fa-forward] + #(set! (.-currentTime @!player) (+ @!elapsed-time 5)) + :extra-classes "text-xl" + :show-on-mobile? true] + [player/button + [:i.fa-solid.fa-forward-step] + #(when (and media-queue (< (+ media-queue-pos 1) (count media-queue))) + (rf/dispatch [::events/change-media-queue-pos + (+ media-queue-pos 1)])) + :disabled? (not (and media-queue (< (+ media-queue-pos 1) (count media-queue)))) + :extra-classes "text-xl" + :show-on-mobile? true] + [player/button [:i.fa-solid.fa-list] #(rf/dispatch [::events/show-media-queue false]) + :show-on-mobile? true + :extra-classes "pl-4 pr-3"]]]]])) diff --git a/src/frontend/tubo/events.cljs b/src/frontend/tubo/events.cljs index da0657b..401de69 100644 --- a/src/frontend/tubo/events.cljs +++ b/src/frontend/tubo/events.cljs @@ -194,10 +194,10 @@ ::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))})) + ::show-media-queue + (fn [{:keys [db]} [_ show?]] + {:db (assoc db :show-media-queue show?) + ::body-overflow! show?})) (rf/reg-event-fx ::scroll-into-view @@ -239,12 +239,12 @@ 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 ::body-overflow! false - :fx [[:dispatch [::get-services]] + :fx [[:dispatch [::show-media-queue false]] + [:dispatch [::get-services]] [:dispatch [::get-kiosks (:service-id db)]]]}))) (rf/reg-event-fx -- cgit v1.2.3