From 187d65c126787ee4208c4c295b627f4800863f70 Mon Sep 17 00:00:00 2001 From: Miguel Ángel Moreno Date: Sun, 1 Dec 2024 13:09:25 +0100 Subject: refactor(frontend): split components into separate features --- src/frontend/tubo/bg_player/events.cljs | 190 +++++++++++++++++ src/frontend/tubo/bg_player/subs.cljs | 25 +++ src/frontend/tubo/bg_player/views.cljs | 341 ++++++++++++++++++++++++++++++ src/frontend/tubo/bookmarks/modals.cljs | 2 +- src/frontend/tubo/bookmarks/views.cljs | 7 +- src/frontend/tubo/channel/events.cljs | 2 +- src/frontend/tubo/channel/views.cljs | 4 +- src/frontend/tubo/comments/views.cljs | 2 +- src/frontend/tubo/components/items.cljs | 132 ------------ src/frontend/tubo/components/layout.cljs | 238 --------------------- src/frontend/tubo/components/player.cljs | 244 --------------------- src/frontend/tubo/events.cljs | 65 +----- src/frontend/tubo/items/views.cljs | 131 ++++++++++++ src/frontend/tubo/kiosks/events.cljs | 4 +- src/frontend/tubo/kiosks/views.cljs | 4 +- src/frontend/tubo/layout/views.cljs | 238 +++++++++++++++++++++ src/frontend/tubo/main_player/events.cljs | 53 +++++ src/frontend/tubo/main_player/subs.cljs | 20 ++ src/frontend/tubo/main_player/views.cljs | 33 +++ src/frontend/tubo/modals/views.cljs | 2 +- src/frontend/tubo/navigation/events.cljs | 55 +++++ src/frontend/tubo/navigation/subs.cljs | 13 ++ src/frontend/tubo/navigation/views.cljs | 79 ++++--- src/frontend/tubo/player/events.cljs | 272 ++---------------------- src/frontend/tubo/player/subs.cljs | 57 +---- src/frontend/tubo/player/views.cljs | 251 ++++++---------------- src/frontend/tubo/playlist/views.cljs | 4 +- src/frontend/tubo/queue/events.cljs | 26 +-- src/frontend/tubo/queue/subs.cljs | 12 +- src/frontend/tubo/queue/views.cljs | 24 +-- src/frontend/tubo/routes.cljs | 2 +- src/frontend/tubo/search/events.cljs | 20 +- src/frontend/tubo/search/subs.cljs | 12 +- src/frontend/tubo/search/views.cljs | 6 +- src/frontend/tubo/settings/views.cljs | 2 +- src/frontend/tubo/stream/events.cljs | 2 +- src/frontend/tubo/stream/views.cljs | 10 +- src/frontend/tubo/subs.cljs | 13 +- src/frontend/tubo/views.cljs | 9 +- 39 files changed, 1324 insertions(+), 1282 deletions(-) create mode 100644 src/frontend/tubo/bg_player/events.cljs create mode 100644 src/frontend/tubo/bg_player/subs.cljs create mode 100644 src/frontend/tubo/bg_player/views.cljs delete mode 100644 src/frontend/tubo/components/items.cljs delete mode 100644 src/frontend/tubo/components/layout.cljs delete mode 100644 src/frontend/tubo/components/player.cljs create mode 100644 src/frontend/tubo/items/views.cljs create mode 100644 src/frontend/tubo/layout/views.cljs create mode 100644 src/frontend/tubo/main_player/events.cljs create mode 100644 src/frontend/tubo/main_player/subs.cljs create mode 100644 src/frontend/tubo/main_player/views.cljs create mode 100644 src/frontend/tubo/navigation/events.cljs create mode 100644 src/frontend/tubo/navigation/subs.cljs (limited to 'src/frontend') diff --git a/src/frontend/tubo/bg_player/events.cljs b/src/frontend/tubo/bg_player/events.cljs new file mode 100644 index 0000000..75f3a8c --- /dev/null +++ b/src/frontend/tubo/bg_player/events.cljs @@ -0,0 +1,190 @@ +(ns tubo.bg-player.events + (:require + [re-frame.core :as rf] + [vimsical.re-frame.cofx.inject :as inject])) + +(rf/reg-event-fx + :bg-player/seek + [(rf/inject-cofx ::inject/sub [:bg-player])] + (fn [{:keys [db bg-player]} [_ time]] + (when (:bg-player/ready db) + {:player/time {:time time :player bg-player}}))) + +(rf/reg-event-db + :bg-player/set-paused + (fn [db [_ val]] + (assoc db :player/paused val))) + +(rf/reg-event-fx + :bg-player/pause + [(rf/inject-cofx ::inject/sub [:bg-player])] + (fn [{:keys [bg-player]} [_ paused?]] + {:player/pause {:paused? paused? + :player bg-player}})) + +(rf/reg-event-fx + :bg-player/play + [(rf/inject-cofx ::inject/sub [:elapsed-time]) + (rf/inject-cofx ::inject/sub [:main-player])] + (fn [{:keys [main-player db elapsed-time]}] + {:fx [[:dispatch [:bg-player/set-paused false]] + [:dispatch [:bg-player/seek @elapsed-time]] + (when (and (:main-player/ready db) main-player @main-player) + [:dispatch [:main-player/pause true]])]})) + +(rf/reg-event-fx + :bg-player/stop + (fn [_] + {:fx [[:dispatch [:bg-player/pause true]] + [:dispatch [:bg-player/seek 0]]]})) + +(rf/reg-event-fx + :bg-player/start + [(rf/inject-cofx ::inject/sub [:bg-player]) + (rf/inject-cofx ::inject/sub [:elapsed-time])] + (fn [{:keys [db bg-player]} _] + {:fx [[:dispatch [:bg-player/set-paused true]] + [:dispatch [:bg-player/pause false]] + [:dispatch + [:player/change-volume (:player/volume db) + bg-player]] + ]})) + +(rf/reg-event-fx + :bg-player/mute + [(rf/inject-cofx :store)] + (fn [{:keys [db store]} [_ value player]] + {:db (assoc db :player/muted value) + :store (assoc store :player/muted value) + :player/mute {:player player :muted? value}})) + +(rf/reg-event-fx + :bg-player/hide + [(rf/inject-cofx :store)] + (fn [{:keys [db store]} _] + {:db (assoc db :bg-player/show false) + :store (assoc store :bg-player/show false)})) + +(rf/reg-event-fx + :bg-player/dispose + [(rf/inject-cofx :store)] + (fn [{:keys [db store]} _] + (let [remove-entries + (fn [elem] + (-> elem + (assoc :queue []) + (assoc :queue/position 0)))] + {:db (remove-entries db) + :store (remove-entries store) + :fx [[:dispatch [:bg-player/pause true]] + [:dispatch [:bg-player/seek 0]] + [:dispatch [:bg-player/hide]]]}))) + +(rf/reg-event-db + :bg-player/ready + (fn [db [_ ready]] + (assoc db :bg-player/ready ready))) + +(rf/reg-event-fx + :bg-player/load-related-streams + (fn [_ [_ res]] + (let [{:keys [related-streams]} (js->clj res :keywordize-keys true)] + {:fx [[:dispatch [:queue/add-n related-streams]]]}))) + +(rf/reg-event-fx + :bg-player/fetch-related-streams + (fn [{:keys [db]} [_ url]] + {:fx [[:dispatch + [:stream/fetch url + [:bg-player/load-related-streams]] [:bad-response]]] + :db (assoc db :bg-player/loading true)})) + +(rf/reg-event-fx + :bg-player/show + [(rf/inject-cofx :store)] + (fn [{:keys [db store]} [_ stream notify?]] + (let [updated-db (update db :queue conj stream) + idx (.indexOf (:queue updated-db) stream)] + {:db updated-db + :store (assoc store :queue (:queue updated-db)) + :fx [[:dispatch + [:bg-player/fetch-stream + (:url stream) idx (= (count (:queue db)) 0)]] + (when (and notify? (not (= (count (:queue db)) 0))) + [:dispatch + [:notifications/add + {:status-text "Added stream to queue" + :failure :info}]])]}))) + +(rf/reg-event-fx + :bg-player/start-radio + (fn [{:keys [db]} [_ stream]] + {:fx [[:dispatch [:bg-player/show stream]] + (when (not= (count (:queue db)) 0) + [:dispatch [:queue/change-pos (count (:queue db))]]) + [:dispatch [:bg-player/fetch-related-streams (:url stream)]] + [:dispatch + [:notifications/add + {:status-text "Started stream radio" + :failure :info}]]]})) + +(rf/reg-event-fx + :bg-player/switch-to-main + [(rf/inject-cofx :store)] + (fn [{:keys [db]} _] + {:fx [[:dispatch [:main-player/show true]]] + :db (assoc db :bg-player/show false) + :scroll-to-top nil})) + +(rf/reg-event-fx + :bg-player/switch-from-main + (fn [{:keys [db]} _] + {:db (assoc db :bg-player/show true) + :fx [[:dispatch [:main-player/show false]] + [:dispatch [:main-player/pause true]]]})) + +(rf/reg-event-fx + :bg-player/load-stream + [(rf/inject-cofx :store) + (rf/inject-cofx ::inject/sub [:bg-player])] + (fn [{:keys [db store bg-player]} [_ idx play? res]] + (let [stream-res (js->clj res :keywordize-keys true)] + {:db (assoc db + :bg-player/show (not (:main-player/show db)) + :bg-player/loading false) + :store (assoc store :bg-player/show (not (:main-player/show db))) + :fx (apply conj + [(when play? + [:dispatch [:queue/change-stream stream-res idx]])] + (when (and (:bg-player/ready db) play?) + [[:media-session-metadata + {:title (:name stream-res) + :artist (:uploader-name stream-res) + :artwork [{:src (:thumbnail-url stream-res)}]}] + [:media-session-handlers + {:current-pos idx + :player bg-player}]]))}))) + +(rf/reg-event-fx + :bg-player/bad-response + (fn [{:keys [db]} [_ idx play? res]] + {:db (assoc db + :bg-player/loading + false) + :fx [[:dispatch [:bad-response res]] + (when play? + (if (> (-> db + :queue + count) + 1) + [:dispatch [:queue/change-pos (inc idx)]] + [:dispatch [:bg-player/dispose]]))]})) + +(rf/reg-event-fx + :bg-player/fetch-stream + (fn [{:keys [db]} [_ url idx play?]] + {:fx [[:dispatch + [:stream/fetch url + [:bg-player/load-stream idx play?] + [:bg-player/bad-response idx play?]]]] + :db (assoc db :bg-player/loading play?)})) diff --git a/src/frontend/tubo/bg_player/subs.cljs b/src/frontend/tubo/bg_player/subs.cljs new file mode 100644 index 0000000..3216391 --- /dev/null +++ b/src/frontend/tubo/bg_player/subs.cljs @@ -0,0 +1,25 @@ +(ns tubo.bg-player.subs + (:require + [re-frame.core :as rf])) + +(defonce !player (atom nil)) + +(rf/reg-sub + :bg-player/ready + (fn [db _] + (:bg-player/ready db))) + +(rf/reg-sub + :bg-player/show + (fn [db _] + (:bg-player/show db))) + +(rf/reg-sub + :bg-player/loading + (fn [db _] + (:bg-player/loading db))) + +(rf/reg-sub + :bg-player + (fn [_ _] + !player)) diff --git a/src/frontend/tubo/bg_player/views.cljs b/src/frontend/tubo/bg_player/views.cljs new file mode 100644 index 0000000..c5f336b --- /dev/null +++ b/src/frontend/tubo/bg_player/views.cljs @@ -0,0 +1,341 @@ +(ns tubo.bg-player.views + (:require + [clojure.string :as str] + [re-frame.core :as rf] + [reagent.core :as r] + [reitit.frontend.easy :as rfe] + [tubo.bookmarks.modals :as modals] + [tubo.layout.views :as layout] + [tubo.utils :as utils] + ["@vidstack/react" :refer (MediaPlayer MediaProvider)] + ["@vidstack/react/player/layouts/default" :refer + (defaultLayoutIcons DefaultAudioLayout)])) + +(defonce base-slider-classes + ["h-2" "cursor-pointer" "appearance-none" "bg-neutral-300" + "dark:bg-neutral-600" + "rounded-full" "overflow-hidden" "focus:outline-none" + "[&::-webkit-slider-thumb]:appearance-none" + "[&::-webkit-slider-thumb]:border-0" + "[&::-webkit-slider-thumb]:rounded-full" + "[&::-webkit-slider-thumb]:h-2" + "[&::-webkit-slider-thumb]:w-2" + "[&::-webkit-slider-thumb]:shadow-[-405px_0_0_400px]" + "[&::-moz-range-thumb]:border-0" + "[&::-moz-range-thumb]:rounded-full" + "[&::-moz-range-thumb]:h-2" + "[&::-moz-range-thumb]:w-2" + "[&::-moz-range-thumb]:shadow-[-405px_0_0_400px]"]) + +(defn get-slider-shadow-classes + [service-color] + (case service-color + "#cc0000" ["[&::-webkit-slider-thumb]:shadow-[#cc0000]" + "[&::-moz-range-thumb]:shadow-[#cc0000]"] + "#ff7700" ["[&::-webkit-slider-thumb]:shadow-[#ff7700]" + "[&::-moz-range-thumb]:shadow-[#ff7700]"] + "#333333" ["[&::-webkit-slider-thumb]:shadow-[#333333]" + "[&::-moz-range-thumb]:shadow-[#333333]"] + "#F2690D" ["[&::-webkit-slider-thumb]:shadow-[#F2690D]" + "[&::-moz-range-thumb]:shadow-[#F2690D]"] + "#629aa9" ["[&::-webkit-slider-thumb]:shadow-[#629aa9]" + "[&::-moz-range-thumb]:shadow-[#629aa9]"] + ["[&::-webkit-slider-thumb]:shadow-neutral-300" + "[&::-moz-range-thumb]:shadow-neutral-300"])) + +(defn get-slider-bg-classes + [service-color] + (case service-color + "#cc0000" ["[&::-webkit-slider-thumb]:bg-[#cc0000]" + "[&::-moz-range-thumb]:bg-[#cc0000]"] + "#ff7700" ["[&::-webkit-slider-thumb]:bg-[#ff7700]" + "[&::-moz-range-thumb]:bg-[#ff7700]"] + "#333333" ["[&::-webkit-slider-thumb]:bg-[#333333]" + "[&::-moz-range-thumb]:bg-[#333333]"] + "#F2690D" ["[&::-webkit-slider-thumb]:bg-[#F2690D]" + "[&::-moz-range-thumb]:bg-[#F2690D]"] + "#629aa9" ["[&::-webkit-slider-thumb]:bg-[#629aa9]" + "[&::-moz-range-thumb]:bg-[#629aa9]"] + ["[&::-webkit-slider-thumb]:bg-neutral-300" + "[&::-moz-range-thumb]:bg-neutral-300"])) + +(defn button + [& {:keys [icon on-click disabled? show-on-mobile? extra-classes]}] + [:button.outline-none.focus:ring-transparent.px-2.pt-1 + {:class (into (into (when disabled? [:opacity-50 :cursor-auto]) + (when-not show-on-mobile? [:hidden :lg:block])) + extra-classes) + :on-click on-click} + icon]) + +(defn loop-button + [loop-playback color show-on-mobile?] + [button + :icon + [:div.relative.flex.items-center + [:i.fa-solid.fa-repeat + {:style {:color (when loop-playback color)}}] + (when (= loop-playback :stream) + [:div.absolute.w-full.h-full.flex.justify-center.items-center.font-bold + {:class "text-[6px]" + :style {:color (when loop-playback color)}} + "1"])] + :on-click #(rf/dispatch [:player/loop]) + :extra-classes [:text-sm] + :show-on-mobile? show-on-mobile?]) + +(defn shuffle-button + [shuffle? color show-on-mobile?] + [button + :icon + [:i.fa-solid.fa-shuffle {:style {:color (when shuffle? color)}}] + :on-click #(rf/dispatch [:queue/shuffle (not shuffle?)]) + :extra-classes [:text-sm] + :show-on-mobile? show-on-mobile?]) + +(defn time-slider + [!player !elapsed-time service-color] + (let [styles (concat base-slider-classes + (get-slider-bg-classes service-color) + (get-slider-shadow-classes service-color)) + bg-player-ready? @(rf/subscribe [:bg-player/ready])] + [:input.w-full + {:class styles + :type "range" + :on-input #(reset! !elapsed-time (.. % -target -value)) + :on-change #(when (and bg-player-ready? @!player) + (set! (.-currentTime @!player) @!elapsed-time)) + :max (if (and bg-player-ready? + @!player + (not (js/isNaN (.-duration @!player)))) + (.floor js/Math (.-duration @!player)) + 100) + :value @!elapsed-time}])) + +(defn volume-slider + [_ _ _ _] + (let [show-slider? (r/atom nil)] + (fn [player volume-level muted? service-color] + (let [styles (concat ["rotate-[270deg]"] + base-slider-classes + (get-slider-bg-classes service-color) + (get-slider-shadow-classes service-color))] + [:div.relative.flex.flex-col.justify-center.items-center + {:on-mouse-over #(reset! show-slider? true) + :on-mouse-out #(reset! show-slider? false)} + [button + :icon + (if muted? [:i.fa-solid.fa-volume-xmark] [:i.fa-solid.fa-volume-low]) + :on-click #(rf/dispatch [:bg-player/mute (not muted?) player]) + :extra-classes [:pl-3 :pr-2]] + (when @show-slider? + [:input.absolute.w-24.ml-2.m-1.bottom-16 + {:class (str/join " " styles) + :type "range" + :on-input #(rf/dispatch [:player/change-volume + (.. % -target -value) player]) + :max 100 + :value volume-level}])])))) + +(defn metadata + [{:keys [thumbnail-url url name uploader-url uploader-name]}] + [:div.flex.items-center.lg:flex-1 + [:div + [layout/thumbnail thumbnail-url (rfe/href :stream-page nil {:url url}) + name nil :classes [:h-14 :py-2 "w-[70px]"]]] + [:div.flex.flex-col.px-2 + [:a.text-xs.line-clamp-1 + {:href (rfe/href :stream-page nil {:url url}) + :title name} + name] + [:a.text-xs.pt-2.text-neutral-600.dark:text-neutral-300.line-clamp-1 + {:href (rfe/href :channel-page nil {:url uploader-url}) + :title uploader-name} + uploader-name]]]) + +(defn main-controls + [!player color] + (let [queue @(rf/subscribe [:queue]) + queue-pos @(rf/subscribe [:queue/position]) + loading? @(rf/subscribe [:bg-player/loading]) + loop-playback @(rf/subscribe [:player/loop]) + shuffle? @(rf/subscribe [:player/shuffled]) + bg-player-ready? @(rf/subscribe [:bg-player/ready]) + paused? @(rf/subscribe [:player/paused]) + !elapsed-time @(rf/subscribe [:elapsed-time])] + [:div.flex.flex-col.items-center.ml-auto + [:div.flex.justify-end + [loop-button loop-playback color] + [button + :icon [:i.fa-solid.fa-backward-step] + :on-click #(rf/dispatch [:queue/change-pos (dec queue-pos)]) + :disabled? (not (and queue (not= queue-pos 0)))] + [button + :icon [:i.fa-solid.fa-backward] + :on-click #(rf/dispatch [:bg-player/seek (- @!elapsed-time 5)])] + [button + :icon + (if (and (not loading?) @!player) + (if paused? + [:i.fa-solid.fa-play] + [:i.fa-solid.fa-pause]) + [layout/loading-icon color "lg:text-2xl"]) + :on-click + #(rf/dispatch [:bg-player/pause (not (.-paused @!player))]) + :show-on-mobile? true + :extra-classes ["lg:text-2xl"]] + [button + :icon [:i.fa-solid.fa-forward] + :on-click #(rf/dispatch [:bg-player/seek (+ @!elapsed-time 5)])] + [button + :icon [:i.fa-solid.fa-forward-step] + :on-click #(rf/dispatch [:queue/change-pos (inc queue-pos)]) + :disabled? (not (and queue (< (inc queue-pos) (count queue))))] + [shuffle-button shuffle? color]] + [:div.hidden.lg:flex.items-center.text-sm + [:span.mx-2 + (if (and bg-player-ready? @!player @!elapsed-time) + (utils/format-duration @!elapsed-time) + "--:--")] + [:div.w-20.lg:w-64.mx-2.flex.items-center + [time-slider !player !elapsed-time color]] + [:span.mx-2 + (if (and bg-player-ready? @!player) + (utils/format-duration (.-duration @!player)) + "--:--")]]])) + +(defn extra-controls + [_ _ _] + (let [!menu-active? (r/atom nil)] + (fn [!player {:keys [url uploader-url] :as stream} color] + (let [muted? @(rf/subscribe [:player/muted]) + volume @(rf/subscribe [:player/volume]) + queue @(rf/subscribe [:queue]) + queue-pos @(rf/subscribe [:queue/position]) + bookmarks @(rf/subscribe [:bookmarks]) + liked? (some #(= (:url %) url) + (-> bookmarks + first + :items)) + bookmark #(rf/dispatch [:modals/open [modals/add-to-bookmark %]])] + [:div.flex.lg:justify-end.lg:flex-1 + [volume-slider !player volume muted? color] + [button + :icon [:i.fa-solid.fa-list] + :on-click #(rf/dispatch [:queue/show true]) + :show-on-mobile? true + :extra-classes [:!pl-4 :!pr-3]] + [layout/popover-menu !menu-active? + [{:label (if liked? "Remove favorite" "Favorite") + :icon [:i.fa-solid.fa-heart + (when liked? {:style {:color color}})] + :on-click #(rf/dispatch [(if liked? :likes/remove :likes/add) + stream])} + {:label "Play radio" + :icon [:i.fa-solid.fa-tower-cell] + :on-click #(rf/dispatch [:bg-player/start-radio stream])} + {:label "Add current to playlist" + :icon [:i.fa-solid.fa-plus] + :on-click #(bookmark stream)} + {:label "Add queue to playlist" + :icon [:i.fa-solid.fa-list] + :on-click #(bookmark queue)} + {:label "Remove from queue" + :icon [:i.fa-solid.fa-trash] + :on-click #(rf/dispatch [:queue/remove queue-pos])} + {:label "Switch to main" + :icon [:i.fa-solid.fa-display] + :on-click #(rf/dispatch [:bg-player/switch-to-main])} + {:label "Show channel details" + :icon [:i.fa-solid.fa-user] + :on-click #(rf/dispatch [:navigation/navigate + {:name :channel-page + :params {} + :query {:url uploader-url}}])} + {:label "Close player" + :icon [:i.fa-solid.fa-close] + :on-click #(rf/dispatch [:bg-player/dispose])}] + :menu-styles {:bottom "30px" :top nil :right "10px"} + :extra-classes [:pt-1 :!pl-4 :px-3]]])))) + +(defn get-audio-player-sources + [available-streams] + (if available-streams + (->> available-streams + (filter #(not= (:format %) "OPUS")) + (sort-by :bitrate) + (map (fn [{:keys [content]}] {:src content :type "audio/mpeg"}))) + [])) + +(defn audio-player + [_ _] + (let [!elapsed-time @(rf/subscribe [:elapsed-time]) + !bg-player-first? (r/atom nil)] + (r/create-class + {:component-will-unmount #(rf/dispatch [:bg-player/ready false]) + :reagent-render + (fn [{:keys [name audio-streams]} !player] + [:> MediaPlayer + {:title name + :class "invisible fixed" + :controls [] + :src (get-audio-player-sources audio-streams) + :viewType "audio" + :ref #(reset! !player %) + :loop (= @(rf/subscribe [:player/loop]) :stream) + :onCanPlay #(rf/dispatch [:bg-player/ready true]) + :onSeeked #(reset! !elapsed-time (.-currentTime @!player)) + :onTimeUpdate #(reset! !elapsed-time (.-currentTime @!player)) + :onEnded (fn [] + (rf/dispatch [:queue/change-pos + (inc @(rf/subscribe + [:queue/position]))]) + (reset! !elapsed-time 0)) + :onPlay #(rf/dispatch [:bg-player/play]) + :onReplay (fn [] + (rf/dispatch [:bg-player/set-paused false]) + (reset! !elapsed-time 0)) + :onPause #(rf/dispatch [:bg-player/set-paused true]) + :onLoadedData (fn [] + (rf/dispatch [:bg-player/start]) + (when-not @!bg-player-first? + (reset! !bg-player-first? true))) + :onSourceChange #(when @!bg-player-first? + (reset! !elapsed-time 0))} + [:> MediaProvider] + [:> DefaultAudioLayout {:icons defaultLayoutIcons}]])}))) + +(defn player + [] + (let [!player @(rf/subscribe [:bg-player]) + stream @(rf/subscribe [:queue/current]) + show-queue? @(rf/subscribe [:queue/show]) + show-player? @(rf/subscribe [:bg-player/show]) + dark-theme? @(rf/subscribe [:dark-theme]) + color (-> stream + :service-id + utils/get-service-color) + bg-color (str "rgba(" + (if dark-theme? "23,23,23" "255,255,255") + ",0.95)") + bg-image (str "linear-gradient(" + bg-color + "," + bg-color + "),url(" + (:thumbnail-url stream) + ")")] + (when show-player? + [:div.sticky.absolute.left-0.bottom-0.z-10.p-3.transition-all.ease-in.relative + {:style + {:visibility (when show-queue? "hidden") + :opacity (if show-queue? 0 1) + :background-image bg-image + :background-size "cover" + :background-position "center" + :background-repeat "no-repeat"}} + [:div.flex.items-center + [audio-player stream !player] + [metadata stream] + [main-controls !player color] + [extra-controls !player stream color]]]))) diff --git a/src/frontend/tubo/bookmarks/modals.cljs b/src/frontend/tubo/bookmarks/modals.cljs index f8a1738..b6463a5 100644 --- a/src/frontend/tubo/bookmarks/modals.cljs +++ b/src/frontend/tubo/bookmarks/modals.cljs @@ -2,7 +2,7 @@ (:require [reagent.core :as r] [re-frame.core :as rf] - [tubo.components.layout :as layout] + [tubo.layout.views :as layout] [tubo.modals.views :as modals])) (defn bookmark-item diff --git a/src/frontend/tubo/bookmarks/views.cljs b/src/frontend/tubo/bookmarks/views.cljs index 4bfd96e..2b3bd14 100644 --- a/src/frontend/tubo/bookmarks/views.cljs +++ b/src/frontend/tubo/bookmarks/views.cljs @@ -4,8 +4,8 @@ [re-frame.core :as rf] [reitit.frontend.easy :as rfe] [tubo.bookmarks.modals :as modals] - [tubo.components.items :as items] - [tubo.components.layout :as layout])) + [tubo.items.views :as items] + [tubo.layout.views :as layout])) (defn bookmarks [] @@ -56,7 +56,8 @@ (let [!menu-active? (r/atom nil)] (fn [] (let [bookmarks @(rf/subscribe [:bookmarks]) - {{:keys [id]} :query-params} @(rf/subscribe [:current-match]) + {{:keys [id]} :query-params} @(rf/subscribe + [:navigation/current-match]) {:keys [items name]} (first (filter #(= (:id %) id) bookmarks))] [layout/content-container diff --git a/src/frontend/tubo/channel/events.cljs b/src/frontend/tubo/channel/events.cljs index b8e5fb0..668b21d 100644 --- a/src/frontend/tubo/channel/events.cljs +++ b/src/frontend/tubo/channel/events.cljs @@ -3,7 +3,7 @@ [re-frame.core :as rf] [tubo.api :as api] [tubo.channel.views :as channel] - [tubo.components.layout :as layout])) + [tubo.layout.views :as layout])) (rf/reg-event-fx :channel/fetch diff --git a/src/frontend/tubo/channel/views.cljs b/src/frontend/tubo/channel/views.cljs index 416b787..3ee4518 100644 --- a/src/frontend/tubo/channel/views.cljs +++ b/src/frontend/tubo/channel/views.cljs @@ -3,8 +3,8 @@ [reagent.core :as r] [re-frame.core :as rf] [tubo.bookmarks.modals :as modals] - [tubo.components.items :as items] - [tubo.components.layout :as layout])) + [tubo.items.views :as items] + [tubo.layout.views :as layout])) (defn metadata-popover [_] diff --git a/src/frontend/tubo/comments/views.cljs b/src/frontend/tubo/comments/views.cljs index e1f6472..2b1bf7f 100644 --- a/src/frontend/tubo/comments/views.cljs +++ b/src/frontend/tubo/comments/views.cljs @@ -2,7 +2,7 @@ (:require [re-frame.core :as rf] [reitit.frontend.easy :as rfe] - [tubo.components.layout :as layout] + [tubo.layout.views :as layout] [tubo.utils :as utils])) (defn comment-top-metadata diff --git a/src/frontend/tubo/components/items.cljs b/src/frontend/tubo/components/items.cljs deleted file mode 100644 index cc8f3f1..0000000 --- a/src/frontend/tubo/components/items.cljs +++ /dev/null @@ -1,132 +0,0 @@ -(ns tubo.components.items - (:require - [re-frame.core :as rf] - [reagent.core :as r] - [reitit.frontend.easy :as rfe] - [tubo.bookmarks.modals :as bookmarks] - [tubo.components.layout :as layout] - [tubo.modals.views :as modals] - [tubo.utils :as utils])) - -(defn item-popover - [_ _] - (let [!menu-active? (r/atom nil)] - (fn [{:keys [service-id audio-streams video-streams type url bookmark-id - uploader-url] - :as item} bookmarks] - (let [liked? (some #(= (:url %) url) - (-> bookmarks - first - :items)) - items - (if (or (= type "stream") audio-streams video-streams) - [{:label "Add to queue" - :icon [:i.fa-solid.fa-headphones] - :on-click #(rf/dispatch [:player/switch-to-background item - true])} - {:label "Play radio" - :icon [:i.fa-solid.fa-tower-cell] - :on-click #(rf/dispatch [:player/start-radio item])} - {:label (if liked? "Remove favorite" "Favorite") - :icon [:i.fa-solid.fa-heart - (when (and liked? service-id) - {:style {:color (utils/get-service-color - service-id)}})] - :on-click #(rf/dispatch [(if liked? :likes/remove :likes/add) - item true])} - {:label "Add to playlist" - :icon [:i.fa-solid.fa-plus] - :on-click #(rf/dispatch [:modals/open - [bookmarks/add-to-bookmark item]])} - (when (some #(= (:url %) url) - (:items (first (filter #(= (:id %) bookmark-id) - bookmarks)))) - {:label "Remove from playlist" - :icon [:i.fa-solid.fa-trash] - :on-click #(rf/dispatch [:bookmark/remove item])}) - {:label "Show channel details" - :icon [:i.fa-solid.fa-user] - :on-click #(rf/dispatch [:navigate - {:name :channel-page - :params {} - :query {:url uploader-url}}])}] - [(when (and bookmarks - (some #(= (:id %) bookmark-id) (rest bookmarks))) - {:label "Remove playlist" - :icon [:i.fa-solid.fa-trash] - :on-click #(rf/dispatch [:bookmarks/remove bookmark-id - true])})])] - (when (not-empty (remove nil? items)) - [layout/popover-menu !menu-active? items :extra-classes - [:pr-0 :pl-4] :menu-styles {:right "15px"}]))))) - -(defn item-content - [{:keys [url name uploader-url uploader-name subscriber-count view-count - stream-count verified?] - :as item} route bookmarks] - [:div - (when name - [:div.flex.items-center.my-2 - [:a {:href route :title name} - [:h1.line-clamp-2.my-1 {:class "[overflow-wrap:anywhere]"} name]] - (when (and verified? (not uploader-url)) - [:i.fa-solid.fa-circle-check.pl-2])]) - [:div.flex.justify-between - [:div.flex.items-center.my-2 - (conj - (when uploader-url - [:a - {:href (rfe/href :channel-page nil {:url uploader-url}) - :title uploader-name - :key url}]) - [:h1.text-neutral-800.dark:text-gray-300.font-semibold.pr-2.line-clamp-1.break-all - {:class "[overflow-wrap:anywhere]" :title uploader-name :key url} - uploader-name]) - (when (and uploader-url verified?) - [:i.fa-solid.fa-circle-check])] - [item-popover item bookmarks]] - (when subscriber-count - [:div.flex.items-center - [:i.fa-solid.fa-users.text-xs] - [:span.mx-2 (utils/format-quantity subscriber-count)]]) - (when stream-count - [:div.flex.items-center - [:i.fa-solid.fa-video.text-xs] - [:span.mx-2 (utils/format-quantity stream-count)]]) - [:div.flex.my-1.justify-between - [:span (utils/format-date-ago (:upload-date item))] - (when view-count - [:div.flex.items-center.h-full.pl-2 - [:i.fa-solid.fa-eye.text-xs] - [:p.pl-1.5 (utils/format-quantity view-count)]])]]) - -(defn generic-item - [{:keys [url name thumbnail-url duration] :as item} bookmarks] - (let [item-url (case (:type item) - "stream" (rfe/href :stream-page nil {:url url}) - "channel" (rfe/href :channel-page nil {:url url}) - "playlist" (rfe/href :playlist-page nil {:url url}) - url)] - [:div.w-full - [:div.flex.flex-col.max-w-full.min-h-full.max-h-full - [layout/thumbnail thumbnail-url item-url name duration - :classes [:py-2 :h-44 "xs:h-28"] :rounded? true] - [item-content item item-url bookmarks]]])) - -(defn related-streams - [related-streams next-page-url] - (let [service-color @(rf/subscribe [:service-color]) - pagination-loading? @(rf/subscribe [:show-pagination-loading]) - bookmarks @(rf/subscribe [:bookmarks])] - [:div.flex.flex-col.items-center.flex-auto.my-2.md:my-8 - [modals/modal] - (if (empty? related-streams) - [:div.flex.items-center.flex-auto.flex-col.justify-center.gap-y-4 - [:i.fa-solid.fa-ghost.text-3xl] - [:p.text-lg "No available streams"]] - [:div.grid.w-full.gap-x-10.gap-y-6 - {:class "xs:grid-cols-[repeat(auto-fill,_minmax(165px,_1fr))]"} - (for [[i item] (map-indexed vector related-streams)] - ^{:key i} [generic-item item bookmarks])]) - (when (and pagination-loading? (seq next-page-url)) - [layout/loading-icon service-color :text-md])])) diff --git a/src/frontend/tubo/components/layout.cljs b/src/frontend/tubo/components/layout.cljs deleted file mode 100644 index 48d4259..0000000 --- a/src/frontend/tubo/components/layout.cljs +++ /dev/null @@ -1,238 +0,0 @@ -(ns tubo.components.layout - (:require - [clojure.string :as str] - [re-frame.core :as rf] - [reitit.frontend.easy :as rfe] - [reagent.core :as r] - [svgreq.core :as svgreq] - [tubo.utils :as utils])) - -(defn thumbnail - [thumbnail-url route name duration & {:keys [classes rounded?]}] - [:div.flex.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.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 - [:div.rounded.p-2.absolute - {:style {:bottom 5 :right 5 :background "rgba(0,0,0,.7)" :zIndex "0"}} - [:p.text-white.text-md - (if (= duration 0) - "LIVE" - (utils/format-duration duration))]])]]) - -(defn logo - [& {:keys [height width]}] - (r/create-element - (svgreq/embed "./resources/public/icons" "tubo" nil) - (js-obj "height" width "width" height))) - -(defn loading-icon - [service-color & classes] - [:div.w-full.flex.justify-center.items-center.flex-auto - [:i.fas.fa-circle-notch.fa-spin - {:class classes - :style {:color service-color}}]]) - -(defn focus-overlay - [on-click active? transparent?] - [:div.w-full.fixed.min-h-screen.right-0.top-0.transition-all.delay-75.ease-in-out.z-20 - {:class (when-not transparent? "bg-black") - :style {:visibility (when-not active? "hidden") - :opacity (if active? "0.5" "0")} - :on-click on-click}]) - -(defn content-container - [& children] - (let [page-loading? @(rf/subscribe [:show-page-loading]) - service-color @(rf/subscribe [:service-color])] - [:div.flex.flex-col.flex-auto.items-center.px-5.py-4 - (if page-loading? - [loading-icon service-color "text-5xl"] - [:div.flex.flex-col.flex-auto.w-full {:class ["lg:w-4/5" "xl:w-3/5"]} - (map-indexed #(with-meta %2 {:key %1}) children)])])) - -(defn content-header - [heading & children] - [:div.flex.items-center.justify-between.mt-6 - [:h1.text-3xl.line-clamp-1.mr-6.font-semibold {:title heading} heading] - (map-indexed #(with-meta %2 {:key %1}) children)]) - -(defn uploader-avatar - [{:keys [uploader-avatar uploader-name uploader-url]}] - (when uploader-avatar - [:div.relative.w-12.xs:w-16.h-12.xs:h-16.flex-auto.flex.items-center.shrink-0 - (conj - (when uploader-url - [:a.flex-auto.flex.min-h-full.min-w-full.max-h-full.max-w-full - {:href (rfe/href :channel-page nil {:url uploader-url}) - :title uploader-name - :key uploader-url}]) - [:img.flex-auto.rounded-full.object-cover.max-w-full.min-h-full - {:src uploader-avatar :alt uploader-name :key uploader-name}])])) - -(defn button - [label on-click left-icon right-icon & - {:keys [button-classes label-classes icon-classes]}] - [:button.px-4.rounded-3xl.py-1.outline-none.focus:ring-transparent.whitespace-nowrap - {:on-click on-click :class button-classes} - (when left-icon - (conj left-icon {:class (or icon-classes label-classes)})) - [:span.mx-2.font-bold.text-sm {:class label-classes} label] - (when right-icon - (conj right-icon {:class (or icon-classes label-classes)}))]) - -(defn primary-button - [label on-click left-icon right-icon] - [button label on-click left-icon right-icon - :button-classes ["bg-stone-800" "dark:bg-white"] - :label-classes ["text-neutral-300" "dark:text-neutral-900"]]) - -(defn secondary-button - [label on-click left-icon right-icon] - [button label on-click left-icon right-icon - :button-classes - ["bg-neutral-100" "dark:bg-transparent" "border" "border-neutral-300" - "dark:border-stone-700"] - :label-classes ["text-neutral-500" "dark:text-white"]]) - -(defn generic-input - [label & children] - [:div.w-full.flex.justify-between.items-center.py-2.gap-x-4 - [:label label] - (map-indexed #(with-meta %2 {:key %1}) children)]) - -(defn text-input - [label _key value on-change placeholder] - [generic-input label - [:input.text-black - {:type "text" - :default-value value - :on-change on-change - :placeholder placeholder}]]) - -(defn boolean-input - [label _key value on-change] - [generic-input label - [:input - {:type "checkbox" - :checked value - :value value - :on-change on-change}]]) - -(defn select-input - [label _key value options on-change] - [generic-input label - [:select.focus:ring-transparent.bg-transparent.font-bold - {:value value - :on-change on-change} - (for [[i option] (map-indexed vector options)] - ^{:key i} - [:option.dark:bg-neutral-900.border-none {:value option :key i} - option])]]) - -(defn menu-item - [{:keys [label icon on-click link] :as item}] - (let [content [:<> - (when icon - [:span.text-xs.min-w-4.w-4.flex.justify-center.items-center - icon]) - [:span.whitespace-nowrap label]] - classes ["relative" "flex" "items-center" "gap-x-3" - "hover:bg-neutral-200" - "dark:hover:bg-stone-800" "py-2" "px-3" "rounded"]] - (if link - [:a - {:href (:route link) - :target (when (:external? link) "_blank") - :class (str/join " " classes)} - content] - [:li.font-semibold - {:on-click on-click - :class (str/join " " classes)} - (if (vector? item) item content)]))) - -(defn menu - [active? items & {:keys [right top bottom left] :or {right "15px" top "0px"}}] - (when-not (empty? (remove nil? items)) - [:ul.absolute.bg-neutral-100.dark:bg-neutral-900.border.border-neutral-300.dark:border-stone-700.rounded-t.rounded-b.z-20.p-2.flex.flex-col.text-neutral-800.dark:text-white - {:class (when-not active? "hidden") - :style {:right right :left left :top top :bottom bottom}} - (for [[i item] (map-indexed vector (remove nil? items))] - ^{:key i} [menu-item item])])) - -(defn popover-menu - [!menu-active? items & - {:keys [menu-styles extra-classes] - :or {menu-styles {:right "25px"} extra-classes [:p-3]}}] - [:div.flex.items-center - [focus-overlay #(reset! !menu-active? false) @!menu-active? true] - [:button.focus:outline-none.relative - {:on-click #(reset! !menu-active? (not @!menu-active?)) - :class extra-classes} - [:i.fa-solid.fa-ellipsis-vertical] - [menu @!menu-active? items menu-styles]]]) - -(defn accordeon - [{:keys [label on-open open? left-icon right-button]} & content] - [:div.py-4 - [:div.flex.justify-between - [:div.flex.items-center.text-sm.sm:text-base - (when left-icon - [:i.w-6 {:class left-icon}]) - [:h2.mx-4.text-lg.w-24 label] - [:i.fa-solid.fa-chevron-up.cursor-pointer.text-sm - {:class (if open? :fa-chevron-up :fa-chevron-down) - :on-click on-open}]] - right-button] - (when open? - (map-indexed #(with-meta %2 {:key %1}) content))]) - -(defn show-more-container - [_open? _text _on-open] - (let [!text-container (atom nil) - !resize-observer (atom nil) - text-clamped? (r/atom nil)] - (r/create-class - {:component-did-mount - (fn [_] - (when @!text-container - (.observe - (reset! !resize-observer - (js/ResizeObserver. - #(let [target (.-target (first %))] - (reset! text-clamped? - (> (.-scrollHeight target) - (.-clientHeight target)))))) - @!text-container))) - :component-will-unmount - #(when (and @!resize-observer @!text-container) - (.unobserve @!resize-observer @!text-container)) - :reagent-render - (fn [open? text on-open] - [:div.py-3.min-w-full - [:span.text-clip.pr-2 - {:dangerouslySetInnerHTML {:__html text} - :class (when-not open? "line-clamp-2") - :ref #(reset! !text-container %)}] - (when (or @text-clamped? open?) - [:button.font-bold {:on-click on-open} - (str "show " (if open? "less" "more"))])])}))) - -(defn error - [{:keys [_failure parse-error status status-text]} cb] - [:div.flex.flex-auto.h-full.items-center.justify-center.p-5 - [:div.flex.flex-col.gap-y-6.border-border-neutral-300.rounded.dark:border-stone-700.bg-neutral-300.dark:bg-neutral-800.p-5 - [:div.flex.items-center.gap-2.text-xl - [:i.fa-solid.fa-circle-exclamation] - [:h3.font-bold - (str status (when (and status status-text) ": ") status-text)]] - (when parse-error - [:span (:status-text parse-error)]) - [:div.flex.justify-center.gap-x-3 - [primary-button "Go Back" #(rf/dispatch [:history-go -1])] - [secondary-button "Retry" #(rf/dispatch cb)]]]]) diff --git a/src/frontend/tubo/components/player.cljs b/src/frontend/tubo/components/player.cljs deleted file mode 100644 index a8f3a18..0000000 --- a/src/frontend/tubo/components/player.cljs +++ /dev/null @@ -1,244 +0,0 @@ -(ns tubo.components.player - (:require - [clojure.string :as str] - [reagent.core :as r] - [re-frame.core :as rf] - ["@vidstack/react" :refer (MediaPlayer MediaProvider Poster)] - ["@vidstack/react/player/layouts/default" :refer - (defaultLayoutIcons DefaultVideoLayout DefaultAudioLayout)])) - -(defn get-video-player-sources - [available-streams service-id] - (if available-streams - (if (= service-id 3) - (map (fn [{:keys [content]}] {:src content :type "video/mp4"}) - (reverse available-streams)) - (->> available-streams - (filter #(and (not= (:format %) "WEBMA_OPUS") - (not= (:format %) "OPUS") - (not= (:format %) "M4A"))) - (sort-by :bitrate) - (#(if (empty? (filter (fn [x] (= (:format x) "MP3")) %)) - (reverse %) - %)) - (map (fn [{:keys [content]}] {:src content :type "video/mp4"})) - first)) - [])) - -(defn video-player - [_stream _!player] - (let [!elapsed-time @(rf/subscribe [:elapsed-time]) - !main-player-first? (r/atom true)] - (r/create-class - {:component-will-unmount #(rf/dispatch [:main-player/ready false]) - :reagent-render - (fn [{:keys [name video-streams audio-streams thumbnail-url service-id]} - !player] - (let [show-main-player? @(rf/subscribe [:main-player/show])] - [:> MediaPlayer - {:title name - :src (get-video-player-sources (into video-streams - audio-streams) - service-id) - :poster thumbnail-url - :class "w-full xl:w-3/5 overflow-hidden" - :playsInline true - :ref #(reset! !player %) - :loop (when show-main-player? - (= @(rf/subscribe [:loop-playback]) :stream)) - :onSeeked (when show-main-player? - #(reset! !elapsed-time (.-currentTime @!player))) - :onTimeUpdate (when show-main-player? - #(reset! !elapsed-time (.-currentTime @!player))) - :onEnded #(when show-main-player? - (rf/dispatch [:queue/change-pos - (inc @(rf/subscribe - [:queue-pos]))]) - (reset! !elapsed-time 0)) - :onLoadedData (fn [] - (when show-main-player? - (rf/dispatch [:main-player/start])) - (when (and @!main-player-first? show-main-player?) - (reset! !main-player-first? false))) - :onPlay #(rf/dispatch [:main-player/play]) - :onCanPlay #(rf/dispatch [:main-player/ready true]) - :onSourceChange #(when-not @!main-player-first? - (reset! !elapsed-time 0))} - [:> MediaProvider - [:> Poster - {:src thumbnail-url - :alt name - :class :vds-poster}]] - [:> DefaultVideoLayout {:icons defaultLayoutIcons}]]))}))) - -(defn get-audio-player-sources - [available-streams] - (if available-streams - (->> available-streams - (filter #(not= (:format %) "OPUS")) - (sort-by :bitrate) - (map (fn [{:keys [content]}] {:src content :type "video/mp4"}))) - [])) - -(defn audio-player - [_stream _!player] - (let [!elapsed-time @(rf/subscribe [:elapsed-time]) - !bg-player-first? (r/atom nil)] - (r/create-class - {:component-will-unmount #(rf/dispatch [:bg-player/ready false]) - :reagent-render - (fn [{:keys [name audio-streams]} !player] - [:> MediaPlayer - {:title name - :class "invisible fixed" - :controls [] - :src (get-audio-player-sources audio-streams) - :viewType "audio" - :ref #(reset! !player %) - :loop (= @(rf/subscribe [:loop-playback]) :stream) - :onCanPlay #(rf/dispatch [:bg-player/ready true]) - :onSeeked #(reset! !elapsed-time (.-currentTime @!player)) - :onTimeUpdate #(reset! !elapsed-time (.-currentTime @!player)) - :onEnded (fn [] - (rf/dispatch [:queue/change-pos - (inc @(rf/subscribe [:queue-pos]))]) - (reset! !elapsed-time 0)) - :onPlay #(rf/dispatch [:bg-player/play]) - :onReplay (fn [] - (rf/dispatch [:bg-player/set-paused false]) - (reset! !elapsed-time 0)) - :onPause #(rf/dispatch [:bg-player/set-paused true]) - :onLoadedData (fn [] - (rf/dispatch [:bg-player/start]) - (when-not @!bg-player-first? - (reset! !bg-player-first? true))) - :onSourceChange #(when @!bg-player-first? - (reset! !elapsed-time 0))} - [:> MediaProvider] - [:> DefaultAudioLayout {:icons defaultLayoutIcons}]])}))) - -(defonce base-slider-classes - ["h-2" "cursor-pointer" "appearance-none" "bg-neutral-300" - "dark:bg-neutral-600" - "rounded-full" "overflow-hidden" "focus:outline-none" - "[&::-webkit-slider-thumb]:appearance-none" - "[&::-webkit-slider-thumb]:border-0" - "[&::-webkit-slider-thumb]:rounded-full" - "[&::-webkit-slider-thumb]:h-2" - "[&::-webkit-slider-thumb]:w-2" - "[&::-webkit-slider-thumb]:shadow-[-405px_0_0_400px]" - "[&::-moz-range-thumb]:border-0" - "[&::-moz-range-thumb]:rounded-full" - "[&::-moz-range-thumb]:h-2" - "[&::-moz-range-thumb]:w-2" - "[&::-moz-range-thumb]:shadow-[-405px_0_0_400px]"]) - -(defn get-slider-shadow-classes - [service-color] - (case service-color - "#cc0000" ["[&::-webkit-slider-thumb]:shadow-[#cc0000]" - "[&::-moz-range-thumb]:shadow-[#cc0000]"] - "#ff7700" ["[&::-webkit-slider-thumb]:shadow-[#ff7700]" - "[&::-moz-range-thumb]:shadow-[#ff7700]"] - "#333333" ["[&::-webkit-slider-thumb]:shadow-[#333333]" - "[&::-moz-range-thumb]:shadow-[#333333]"] - "#F2690D" ["[&::-webkit-slider-thumb]:shadow-[#F2690D]" - "[&::-moz-range-thumb]:shadow-[#F2690D]"] - "#629aa9" ["[&::-webkit-slider-thumb]:shadow-[#629aa9]" - "[&::-moz-range-thumb]:shadow-[#629aa9]"] - ["[&::-webkit-slider-thumb]:shadow-neutral-300" - "[&::-moz-range-thumb]:shadow-neutral-300"])) - -(defn get-slider-bg-classes - [service-color] - (case service-color - "#cc0000" ["[&::-webkit-slider-thumb]:bg-[#cc0000]" - "[&::-moz-range-thumb]:bg-[#cc0000]"] - "#ff7700" ["[&::-webkit-slider-thumb]:bg-[#ff7700]" - "[&::-moz-range-thumb]:bg-[#ff7700]"] - "#333333" ["[&::-webkit-slider-thumb]:bg-[#333333]" - "[&::-moz-range-thumb]:bg-[#333333]"] - "#F2690D" ["[&::-webkit-slider-thumb]:bg-[#F2690D]" - "[&::-moz-range-thumb]:bg-[#F2690D]"] - "#629aa9" ["[&::-webkit-slider-thumb]:bg-[#629aa9]" - "[&::-moz-range-thumb]:bg-[#629aa9]"] - ["[&::-webkit-slider-thumb]:bg-neutral-300" - "[&::-moz-range-thumb]:bg-neutral-300"])) - -(defn time-slider - [!player !elapsed-time service-color] - (let [styles (concat base-slider-classes - (get-slider-bg-classes service-color) - (get-slider-shadow-classes service-color)) - bg-player-ready? @(rf/subscribe [:bg-player/ready])] - [:input.w-full - {:class styles - :type "range" - :on-input #(reset! !elapsed-time (.. % -target -value)) - :on-change #(when (and bg-player-ready? @!player) - (set! (.-currentTime @!player) @!elapsed-time)) - :max (if (and bg-player-ready? - @!player - (not (js/isNaN (.-duration @!player)))) - (.floor js/Math (.-duration @!player)) - 100) - :value @!elapsed-time}])) - -(defn button - [& {:keys [icon on-click disabled? show-on-mobile? extra-classes]}] - [:button.outline-none.focus:ring-transparent.px-2.pt-1 - {:class (into (into (when disabled? [:opacity-50 :cursor-auto]) - (when-not show-on-mobile? [:hidden :lg:block])) - extra-classes) - :on-click on-click} - icon]) - -(defn loop-button - [loop-playback color show-on-mobile?] - [button - :icon - [:div.relative.flex.items-center - [:i.fa-solid.fa-repeat - {:style {:color (when loop-playback color)}}] - (when (= loop-playback :stream) - [:div.absolute.w-full.h-full.flex.justify-center.items-center.font-bold - {:class "text-[6px]" - :style {:color (when loop-playback color)}} - "1"])] - :on-click #(rf/dispatch [:player/loop]) - :extra-classes [:text-sm] - :show-on-mobile? show-on-mobile?]) - -(defn shuffle-button - [shuffle? color show-on-mobile?] - [button - :icon - [:i.fa-solid.fa-shuffle {:style {:color (when shuffle? color)}}] - :on-click #(rf/dispatch [:queue/shuffle (not shuffle?)]) - :extra-classes [:text-sm] - :show-on-mobile? show-on-mobile?]) - -(defn volume-slider - [_player _volume-level _muted? _service-color] - (let [show-slider? (r/atom nil)] - (fn [player volume-level muted? service-color] - (let [styles (concat ["rotate-[270deg]"] - base-slider-classes - (get-slider-bg-classes service-color) - (get-slider-shadow-classes service-color))] - [:div.relative.flex.flex-col.justify-center.items-center - {:on-mouse-over #(reset! show-slider? true) - :on-mouse-out #(reset! show-slider? false)} - [button - :icon - (if muted? [:i.fa-solid.fa-volume-xmark] [:i.fa-solid.fa-volume-low]) - :on-click #(rf/dispatch [:bg-player/mute (not muted?) player]) - :extra-classes [:pl-3 :pr-2]] - (when @show-slider? - [:input.absolute.w-24.ml-2.m-1.bottom-16 - {:class (str/join " " styles) - :type "range" - :on-input #(rf/dispatch [:player/change-volume - (.. % -target -value) player]) - :max 100 - :value volume-level}])])))) diff --git a/src/frontend/tubo/events.cljs b/src/frontend/tubo/events.cljs index cfa5a78..31ecc8c 100644 --- a/src/frontend/tubo/events.cljs +++ b/src/frontend/tubo/events.cljs @@ -6,13 +6,14 @@ [reagent.core :as r] [re-frame.core :as rf] [re-promise.core] - [reitit.frontend.easy :as rfe] - [reitit.frontend.controllers :as rfc] + [tubo.bg-player.events] [tubo.bookmarks.events] [tubo.channel.events] [tubo.comments.events] [tubo.kiosks.events] + [tubo.main-player.events] [tubo.modals.events] + [tubo.navigation.events] [tubo.notifications.events] [tubo.player.events] [tubo.playlist.events] @@ -30,13 +31,13 @@ (fn [{:keys [store]} _] (let [if-nil #(if (nil? %1) %2 %1)] {:db - {:paused true - :muted (:muted store) + {:player/paused true + :player/muted (:player/muted store) :queue (if-nil (:queue store) []) :service-id (if-nil (:service-id store) 0) - :loop-playback (if-nil (:loop-playback store) :playlist) - :queue-pos (if-nil (:queue-pos store) 0) - :volume-level (if-nil (:volume-level store) 100) + :player/loop (if-nil (:player/loop store) :playlist) + :queue/position (if-nil (:palyer/position store) 0) + :player/volume (if-nil (:player/volume store) 100) :bg-player/show (:bg-player/show store) :bookmarks (if-nil (:bookmarks store) [{:id (nano-id) :name "Liked Streams"}]) @@ -82,54 +83,6 @@ (fn [_ [_ element]] {:scroll-into-view! element})) -(rf/reg-fx - :history-go! - (fn [idx] - (.go js/window.history idx))) - -(rf/reg-event-fx - :history-go - (fn [_ [_ idx]] - {:history-go! idx})) - -(rf/reg-fx - :navigate! - (fn [{:keys [name params query]}] - (rfe/push-state name params query))) - -(rf/reg-event-fx - :navigate - (fn [_ [_ route]] - {:navigate! route})) - -(rf/reg-event-fx - :toggle-mobile-nav - (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 - :navigated - (fn [{:keys [db]} [_ new-match]] - (let [old-match (:current-match db) - controllers (rfc/apply-controllers (:controllers old-match) new-match) - match (assoc new-match :controllers controllers)] - {:db (-> db - (assoc :current-match match) - (assoc :show-mobile-nav false) - (assoc :show-pagination-loading false)) - :scroll-to-top nil - :body-overflow false - :fx [(when (:main-player/show db) - [:dispatch [:player/switch-from-main]]) - [:dispatch [:queue/show false]] - [:dispatch - [:services/fetch-all - [:services/load] [:bad-response]]] - [:dispatch - [:kiosks/fetch-all (:service-id db) - [:kiosks/load] [:bad-response]]]]}))) - (defonce timeouts! (r/atom {})) (rf/reg-fx @@ -183,4 +136,4 @@ (rf/reg-event-fx :change-view (fn [{:keys [db]} [_ view]] - {:db (assoc-in db [:current-match :data :view] view)})) + {:db (assoc-in db [:navigation/current-match :data :view] view)})) diff --git a/src/frontend/tubo/items/views.cljs b/src/frontend/tubo/items/views.cljs new file mode 100644 index 0000000..343b3c2 --- /dev/null +++ b/src/frontend/tubo/items/views.cljs @@ -0,0 +1,131 @@ +(ns tubo.items.views + (:require + [re-frame.core :as rf] + [reagent.core :as r] + [reitit.frontend.easy :as rfe] + [tubo.bookmarks.modals :as bookmarks] + [tubo.layout.views :as layout] + [tubo.modals.views :as modals] + [tubo.utils :as utils])) + +(defn item-popover + [_ _] + (let [!menu-active? (r/atom nil)] + (fn [{:keys [service-id audio-streams video-streams type url bookmark-id + uploader-url] + :as item} bookmarks] + (let [liked? (some #(= (:url %) url) + (-> bookmarks + first + :items)) + items + (if (or (= type "stream") audio-streams video-streams) + [{:label "Add to queue" + :icon [:i.fa-solid.fa-headphones] + :on-click #(rf/dispatch [:bg-player/show item true])} + {:label "Play radio" + :icon [:i.fa-solid.fa-tower-cell] + :on-click #(rf/dispatch [:bg-player/start-radio item])} + {:label (if liked? "Remove favorite" "Favorite") + :icon [:i.fa-solid.fa-heart + (when (and liked? service-id) + {:style {:color (utils/get-service-color + service-id)}})] + :on-click #(rf/dispatch [(if liked? :likes/remove :likes/add) + item true])} + {:label "Add to playlist" + :icon [:i.fa-solid.fa-plus] + :on-click #(rf/dispatch [:modals/open + [bookmarks/add-to-bookmark item]])} + (when (some #(= (:url %) url) + (:items (first (filter #(= (:id %) bookmark-id) + bookmarks)))) + {:label "Remove from playlist" + :icon [:i.fa-solid.fa-trash] + :on-click #(rf/dispatch [:bookmark/remove item])}) + {:label "Show channel details" + :icon [:i.fa-solid.fa-user] + :on-click #(rf/dispatch [:navigation/navigate + {:name :channel-page + :params {} + :query {:url uploader-url}}])}] + [(when (and bookmarks + (some #(= (:id %) bookmark-id) (rest bookmarks))) + {:label "Remove playlist" + :icon [:i.fa-solid.fa-trash] + :on-click #(rf/dispatch [:bookmarks/remove bookmark-id + true])})])] + (when (not-empty (remove nil? items)) + [layout/popover-menu !menu-active? items :extra-classes + [:pr-0 :pl-4] :menu-styles {:right "15px"}]))))) + +(defn item-content + [{:keys [url name uploader-url uploader-name subscriber-count view-count + stream-count verified?] + :as item} route bookmarks] + [:div + (when name + [:div.flex.items-center.my-2 + [:a {:href route :title name} + [:h1.line-clamp-2.my-1 {:class "[overflow-wrap:anywhere]"} name]] + (when (and verified? (not uploader-url)) + [:i.fa-solid.fa-circle-check.pl-2])]) + [:div.flex.justify-between + [:div.flex.items-center.my-2 + (conj + (when uploader-url + [:a + {:href (rfe/href :channel-page nil {:url uploader-url}) + :title uploader-name + :key url}]) + [:h1.text-neutral-800.dark:text-gray-300.font-semibold.pr-2.line-clamp-1.break-all + {:class "[overflow-wrap:anywhere]" :title uploader-name :key url} + uploader-name]) + (when (and uploader-url verified?) + [:i.fa-solid.fa-circle-check])] + [item-popover item bookmarks]] + (when subscriber-count + [:div.flex.items-center + [:i.fa-solid.fa-users.text-xs] + [:span.mx-2 (utils/format-quantity subscriber-count)]]) + (when stream-count + [:div.flex.items-center + [:i.fa-solid.fa-video.text-xs] + [:span.mx-2 (utils/format-quantity stream-count)]]) + [:div.flex.my-1.justify-between + [:span (utils/format-date-ago (:upload-date item))] + (when view-count + [:div.flex.items-center.h-full.pl-2 + [:i.fa-solid.fa-eye.text-xs] + [:p.pl-1.5 (utils/format-quantity view-count)]])]]) + +(defn generic-item + [{:keys [url name thumbnail-url duration] :as item} bookmarks] + (let [item-url (case (:type item) + "stream" (rfe/href :stream-page nil {:url url}) + "channel" (rfe/href :channel-page nil {:url url}) + "playlist" (rfe/href :playlist-page nil {:url url}) + url)] + [:div.w-full + [:div.flex.flex-col.max-w-full.min-h-full.max-h-full + [layout/thumbnail thumbnail-url item-url name duration + :classes [:py-2 :h-44 "xs:h-28"] :rounded? true] + [item-content item item-url bookmarks]]])) + +(defn related-streams + [related-streams next-page-url] + (let [service-color @(rf/subscribe [:service-color]) + pagination-loading? @(rf/subscribe [:show-pagination-loading]) + bookmarks @(rf/subscribe [:bookmarks])] + [:div.flex.flex-col.items-center.flex-auto.my-2.md:my-8 + [modals/modal] + (if (empty? related-streams) + [:div.flex.items-center.flex-auto.flex-col.justify-center.gap-y-4 + [:i.fa-solid.fa-ghost.text-3xl] + [:p.text-lg "No available streams"]] + [:div.grid.w-full.gap-x-10.gap-y-6 + {:class "xs:grid-cols-[repeat(auto-fill,_minmax(165px,_1fr))]"} + (for [[i item] (map-indexed vector related-streams)] + ^{:key i} [generic-item item bookmarks])]) + (when (and pagination-loading? (seq next-page-url)) + [layout/loading-icon service-color :text-md])])) diff --git a/src/frontend/tubo/kiosks/events.cljs b/src/frontend/tubo/kiosks/events.cljs index 720715e..75fc9a2 100644 --- a/src/frontend/tubo/kiosks/events.cljs +++ b/src/frontend/tubo/kiosks/events.cljs @@ -2,7 +2,7 @@ (:require [re-frame.core :as rf] [tubo.api :as api] - [tubo.components.layout :as layout])) + [tubo.layout.views :as layout])) (rf/reg-event-db :kiosks/load @@ -93,7 +93,7 @@ (fn [_ [_ service-id]] {:fx [[:dispatch [:services/change-id service-id]] [:dispatch - [:navigate + [:navigation/navigate {:name :kiosk-page :params {} :query {:serviceId service-id}}]]]})) diff --git a/src/frontend/tubo/kiosks/views.cljs b/src/frontend/tubo/kiosks/views.cljs index bd6c284..e42719c 100644 --- a/src/frontend/tubo/kiosks/views.cljs +++ b/src/frontend/tubo/kiosks/views.cljs @@ -2,8 +2,8 @@ (:require [re-frame.core :as rf] [reitit.frontend.easy :as rfe] - [tubo.components.items :as items] - [tubo.components.layout :as layout])) + [tubo.items.views :as items] + [tubo.layout.views :as layout])) (defn kiosk-active? [& {:keys [kiosk kiosk-id service-id default-service default-kiosk path]}] diff --git a/src/frontend/tubo/layout/views.cljs b/src/frontend/tubo/layout/views.cljs new file mode 100644 index 0000000..90dc3ef --- /dev/null +++ b/src/frontend/tubo/layout/views.cljs @@ -0,0 +1,238 @@ +(ns tubo.layout.views + (:require + [clojure.string :as str] + [re-frame.core :as rf] + [reitit.frontend.easy :as rfe] + [reagent.core :as r] + [svgreq.core :as svgreq] + [tubo.utils :as utils])) + +(defn thumbnail + [thumbnail-url route name duration & {:keys [classes rounded?]}] + [:div.flex.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.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 + [:div.rounded.p-2.absolute + {:style {:bottom 5 :right 5 :background "rgba(0,0,0,.7)" :zIndex "0"}} + [:p.text-white.text-md + (if (= duration 0) + "LIVE" + (utils/format-duration duration))]])]]) + +(defn logo + [& {:keys [height width]}] + (r/create-element + (svgreq/embed "./resources/public/icons" "tubo" nil) + (js-obj "height" width "width" height))) + +(defn loading-icon + [service-color & classes] + [:div.w-full.flex.justify-center.items-center.flex-auto + [:i.fas.fa-circle-notch.fa-spin + {:class classes + :style {:color service-color}}]]) + +(defn focus-overlay + [on-click active? transparent?] + [:div.w-full.fixed.min-h-screen.right-0.top-0.transition-all.delay-75.ease-in-out.z-20 + {:class (when-not transparent? "bg-black") + :style {:visibility (when-not active? "hidden") + :opacity (if active? "0.5" "0")} + :on-click on-click}]) + +(defn content-container + [& children] + (let [page-loading? @(rf/subscribe [:show-page-loading]) + service-color @(rf/subscribe [:service-color])] + [:div.flex.flex-col.flex-auto.items-center.px-5.py-4 + (if page-loading? + [loading-icon service-color "text-5xl"] + [:div.flex.flex-col.flex-auto.w-full {:class ["lg:w-4/5" "xl:w-3/5"]} + (map-indexed #(with-meta %2 {:key %1}) children)])])) + +(defn content-header + [heading & children] + [:div.flex.items-center.justify-between.mt-6 + [:h1.text-3xl.line-clamp-1.mr-6.font-semibold {:title heading} heading] + (map-indexed #(with-meta %2 {:key %1}) children)]) + +(defn uploader-avatar + [{:keys [uploader-avatar uploader-name uploader-url]}] + (when uploader-avatar + [:div.relative.w-12.xs:w-16.h-12.xs:h-16.flex-auto.flex.items-center.shrink-0 + (conj + (when uploader-url + [:a.flex-auto.flex.min-h-full.min-w-full.max-h-full.max-w-full + {:href (rfe/href :channel-page nil {:url uploader-url}) + :title uploader-name + :key uploader-url}]) + [:img.flex-auto.rounded-full.object-cover.max-w-full.min-h-full + {:src uploader-avatar :alt uploader-name :key uploader-name}])])) + +(defn button + [label on-click left-icon right-icon & + {:keys [button-classes label-classes icon-classes]}] + [:button.px-4.rounded-3xl.py-1.outline-none.focus:ring-transparent.whitespace-nowrap + {:on-click on-click :class button-classes} + (when left-icon + (conj left-icon {:class (or icon-classes label-classes)})) + [:span.mx-2.font-bold.text-sm {:class label-classes} label] + (when right-icon + (conj right-icon {:class (or icon-classes label-classes)}))]) + +(defn primary-button + [label on-click left-icon right-icon] + [button label on-click left-icon right-icon + :button-classes ["bg-stone-800" "dark:bg-white"] + :label-classes ["text-neutral-300" "dark:text-neutral-900"]]) + +(defn secondary-button + [label on-click left-icon right-icon] + [button label on-click left-icon right-icon + :button-classes + ["bg-neutral-100" "dark:bg-transparent" "border" "border-neutral-300" + "dark:border-stone-700"] + :label-classes ["text-neutral-500" "dark:text-white"]]) + +(defn generic-input + [label & children] + [:div.w-full.flex.justify-between.items-center.py-2.gap-x-4 + [:label label] + (map-indexed #(with-meta %2 {:key %1}) children)]) + +(defn text-input + [label _key value on-change placeholder] + [generic-input label + [:input.text-black + {:type "text" + :default-value value + :on-change on-change + :placeholder placeholder}]]) + +(defn boolean-input + [label _key value on-change] + [generic-input label + [:input + {:type "checkbox" + :checked value + :value value + :on-change on-change}]]) + +(defn select-input + [label _key value options on-change] + [generic-input label + [:select.focus:ring-transparent.bg-transparent.font-bold + {:value value + :on-change on-change} + (for [[i option] (map-indexed vector options)] + ^{:key i} + [:option.dark:bg-neutral-900.border-none {:value option :key i} + option])]]) + +(defn menu-item + [{:keys [label icon on-click link] :as item}] + (let [content [:<> + (when icon + [:span.text-xs.min-w-4.w-4.flex.justify-center.items-center + icon]) + [:span.whitespace-nowrap label]] + classes ["relative" "flex" "items-center" "gap-x-3" + "hover:bg-neutral-200" + "dark:hover:bg-stone-800" "py-2" "px-3" "rounded"]] + (if link + [:a + {:href (:route link) + :target (when (:external? link) "_blank") + :class (str/join " " classes)} + content] + [:li.font-semibold + {:on-click on-click + :class (str/join " " classes)} + (if (vector? item) item content)]))) + +(defn menu + [active? items & {:keys [right top bottom left] :or {right "15px" top "0px"}}] + (when-not (empty? (remove nil? items)) + [:ul.absolute.bg-neutral-100.dark:bg-neutral-900.border.border-neutral-300.dark:border-stone-700.rounded-t.rounded-b.z-20.p-2.flex.flex-col.text-neutral-800.dark:text-white + {:class (when-not active? "hidden") + :style {:right right :left left :top top :bottom bottom}} + (for [[i item] (map-indexed vector (remove nil? items))] + ^{:key i} [menu-item item])])) + +(defn popover-menu + [!menu-active? items & + {:keys [menu-styles extra-classes] + :or {menu-styles {:right "25px"} extra-classes [:p-3]}}] + [:div.flex.items-center + [focus-overlay #(reset! !menu-active? false) @!menu-active? true] + [:button.focus:outline-none.relative + {:on-click #(reset! !menu-active? (not @!menu-active?)) + :class extra-classes} + [:i.fa-solid.fa-ellipsis-vertical] + [menu @!menu-active? items menu-styles]]]) + +(defn accordeon + [{:keys [label on-open open? left-icon right-button]} & content] + [:div.py-4 + [:div.flex.justify-between + [:div.flex.items-center.text-sm.sm:text-base + (when left-icon + [:i.w-6 {:class left-icon}]) + [:h2.mx-4.text-lg.w-24 label] + [:i.fa-solid.fa-chevron-up.cursor-pointer.text-sm + {:class (if open? :fa-chevron-up :fa-chevron-down) + :on-click on-open}]] + right-button] + (when open? + (map-indexed #(with-meta %2 {:key %1}) content))]) + +(defn show-more-container + [_open? _text _on-open] + (let [!text-container (atom nil) + !resize-observer (atom nil) + text-clamped? (r/atom nil)] + (r/create-class + {:component-did-mount + (fn [_] + (when @!text-container + (.observe + (reset! !resize-observer + (js/ResizeObserver. + #(let [target (.-target (first %))] + (reset! text-clamped? + (> (.-scrollHeight target) + (.-clientHeight target)))))) + @!text-container))) + :component-will-unmount + #(when (and @!resize-observer @!text-container) + (.unobserve @!resize-observer @!text-container)) + :reagent-render + (fn [open? text on-open] + [:div.py-3.min-w-full + [:span.text-clip.pr-2 + {:dangerouslySetInnerHTML {:__html text} + :class (when-not open? "line-clamp-2") + :ref #(reset! !text-container %)}] + (when (or @text-clamped? open?) + [:button.font-bold {:on-click on-open} + (str "show " (if open? "less" "more"))])])}))) + +(defn error + [{:keys [_failure parse-error status status-text]} cb] + [:div.flex.flex-auto.h-full.items-center.justify-center.p-5 + [:div.flex.flex-col.gap-y-6.border-border-neutral-300.rounded.dark:border-stone-700.bg-neutral-300.dark:bg-neutral-800.p-5 + [:div.flex.items-center.gap-2.text-xl + [:i.fa-solid.fa-circle-exclamation] + [:h3.font-bold + (str status (when (and status status-text) ": ") status-text)]] + (when parse-error + [:span (:status-text parse-error)]) + [:div.flex.justify-center.gap-x-3 + [primary-button "Go Back" #(rf/dispatch [:navigation/history-go -1])] + [secondary-button "Retry" #(rf/dispatch cb)]]]]) diff --git a/src/frontend/tubo/main_player/events.cljs b/src/frontend/tubo/main_player/events.cljs new file mode 100644 index 0000000..a5e8414 --- /dev/null +++ b/src/frontend/tubo/main_player/events.cljs @@ -0,0 +1,53 @@ +(ns tubo.main-player.events + (:require + [re-frame.core :as rf] + [vimsical.re-frame.cofx.inject :as inject])) + +(rf/reg-event-fx + :main-player/seek + [(rf/inject-cofx ::inject/sub [:main-player])] + (fn [{:keys [main-player]} [_ time]] + {:player/time {:time time :player main-player}})) + +(rf/reg-event-fx + :main-player/pause + [(rf/inject-cofx ::inject/sub [:main-player])] + (fn [{:keys [db main-player]} [_ paused?]] + (when (:main-player/ready db) + {:player/pause {:paused? paused? + :player main-player}}))) + +(rf/reg-event-fx + :main-player/play + [(rf/inject-cofx ::inject/sub [:main-player])] + (fn [{:keys [db main-player]}] + {:fx [(when (and (:bg-player/ready db) main-player @main-player) + [:dispatch [:bg-player/pause true]])]})) + +(rf/reg-event-fx + :main-player/start + [(rf/inject-cofx ::inject/sub [:elapsed-time])] + (fn [{:keys [db elapsed-time]} _] + {:fx [[:dispatch [:main-player/pause false]] + (when (and (:main-player/show db) (not (:bg-player/ready db))) + [:dispatch [:main-player/seek @elapsed-time]])]})) + +(rf/reg-event-db + :main-player/ready + (fn [db [_ ready]] + (assoc db :main-player/ready ready))) + +(rf/reg-event-db + :main-player/toggle-layout + (fn [db [_ layout]] + (assoc-in db + [:queue (:queue-pos db) layout] + (not (get-in db [:queue (:queue/position db) layout]))))) + +(rf/reg-event-fx + :main-player/show + (fn [{:keys [db]} [_ val]] + {:db (apply assoc + (assoc db :main-player/show val) + (when val [:search/show-form false])) + :body-overflow val})) diff --git a/src/frontend/tubo/main_player/subs.cljs b/src/frontend/tubo/main_player/subs.cljs new file mode 100644 index 0000000..eca94db --- /dev/null +++ b/src/frontend/tubo/main_player/subs.cljs @@ -0,0 +1,20 @@ +(ns tubo.main-player.subs + (:require + [re-frame.core :as rf])) + +(defonce !player (atom nil)) + +(rf/reg-sub + :main-player/ready + (fn [db _] + (:main-player/ready db))) + +(rf/reg-sub + :main-player/show + (fn [db _] + (:main-player/show db))) + +(rf/reg-sub + :main-player + (fn [_ _] + !player)) diff --git a/src/frontend/tubo/main_player/views.cljs b/src/frontend/tubo/main_player/views.cljs new file mode 100644 index 0000000..be4c223 --- /dev/null +++ b/src/frontend/tubo/main_player/views.cljs @@ -0,0 +1,33 @@ +(ns tubo.main-player.views + (:require + [re-frame.core :as rf] + [tubo.layout.views :as layout] + [tubo.player.views :as player] + [tubo.queue.views :as queue] + [tubo.stream.views :as stream])) + +(defn player + [] + (let [queue @(rf/subscribe [:queue]) + queue-pos @(rf/subscribe [:queue/position]) + bookmarks @(rf/subscribe [:bookmarks]) + !player @(rf/subscribe [:main-player]) + stream @(rf/subscribe [:queue/current]) + show-player? @(rf/subscribe [:main-player/show])] + [:div.fixed.w-full.bg-neutral-100.dark:bg-neutral-900.overflow-auto.z-10.transition-all.ease-in-out + {:class ["h-[calc(100%-56px)]" + (if show-player? "translate-y-0" "translate-y-full")]} + (when (and show-player? stream) + [:div + [:div.flex.flex-col.items-center.w-full.xl:py-6 + [player/video-player stream !player]] + [:div.flex.items-center.justify-center + [:div.flex.flex-col.gap-y-1.w-full.h-fit.max-h-64.overflow-y-auto + {:class ["lg:w-4/5" "xl:w-3/5"]} + (for [[i item] (map-indexed vector queue)] + ^{:key i} [queue/queue-item item queue queue-pos i bookmarks])]] + [layout/content-container + [stream/metadata stream] + [stream/description stream] + [stream/comments stream] + [stream/suggested stream]]])])) diff --git a/src/frontend/tubo/modals/views.cljs b/src/frontend/tubo/modals/views.cljs index df59034..1ff4bb1 100644 --- a/src/frontend/tubo/modals/views.cljs +++ b/src/frontend/tubo/modals/views.cljs @@ -1,7 +1,7 @@ (ns tubo.modals.views (:require [re-frame.core :as rf] - [tubo.components.layout :as layout])) + [tubo.layout.views :as layout])) (defn modal-content [title body & extra-buttons] diff --git a/src/frontend/tubo/navigation/events.cljs b/src/frontend/tubo/navigation/events.cljs new file mode 100644 index 0000000..a4c961d --- /dev/null +++ b/src/frontend/tubo/navigation/events.cljs @@ -0,0 +1,55 @@ +(ns tubo.navigation.events + (:require + [re-frame.core :as rf] + [reitit.frontend.easy :as rfe] + [reitit.frontend.controllers :as rfc])) + +(rf/reg-fx + :history-go! + (fn [idx] + (.go js/window.history idx))) + +(rf/reg-event-fx + :navigation/history-go + (fn [_ [_ idx]] + {:history-go! idx})) + +(rf/reg-fx + :navigate! + (fn [{:keys [name params query]}] + (rfe/push-state name params query))) + +(rf/reg-event-fx + :navigation/navigate + (fn [_ [_ route]] + {:navigate! route})) + +(rf/reg-event-fx + :navigation/toggle-mobile-menu + (fn [{:keys [db]} _] + {:db (assoc db + :navigation/show-mobile-menu + (not (:navigation/show-mobile-menu db))) + :body-overflow (not (:navigation/show-mobile-menu db))})) + +(rf/reg-event-fx + :navigation/navigated + (fn [{:keys [db]} [_ new-match]] + (let [old-match (:navigation/current-match db) + controllers (rfc/apply-controllers (:controllers old-match) new-match) + match (assoc new-match :controllers controllers)] + {:db (-> db + (assoc :navigation/current-match match) + (assoc :navigation/show-mobile-menu false) + (assoc :layout/show-pagination-loading false)) + :scroll-to-top nil + :body-overflow false + :fx [(when (:main-player/show db) + [:dispatch [:bg-player/switch-from-main]]) + [:dispatch [:queue/show false]] + [:dispatch + [:services/fetch-all + [:services/load] [:bad-response]]] + [:dispatch + [:kiosks/fetch-all (:service-id db) + [:kiosks/load] [:bad-response]]]]}))) diff --git a/src/frontend/tubo/navigation/subs.cljs b/src/frontend/tubo/navigation/subs.cljs new file mode 100644 index 0000000..fc696fc --- /dev/null +++ b/src/frontend/tubo/navigation/subs.cljs @@ -0,0 +1,13 @@ +(ns tubo.navigation.subs + (:require + [re-frame.core :as rf])) + +(rf/reg-sub + :navigation/show-mobile-menu + (fn [db _] + (:navigation/show-mobile-menu db))) + +(rf/reg-sub + :navigation/current-match + (fn [db _] + (:navigation/current-match db))) diff --git a/src/frontend/tubo/navigation/views.cljs b/src/frontend/tubo/navigation/views.cljs index b899be7..a158420 100644 --- a/src/frontend/tubo/navigation/views.cljs +++ b/src/frontend/tubo/navigation/views.cljs @@ -3,52 +3,51 @@ [re-frame.core :as rf] [reagent.core :as r] [reitit.frontend.easy :as rfe] - [tubo.components.layout :as layout] + [tubo.channel.views :as channel] [tubo.kiosks.views :as kiosks] + [tubo.layout.views :as layout] [tubo.services.views :as services] - [tubo.stream.views :as stream] - [tubo.channel.views :as channel])) + [tubo.stream.views :as stream])) (defn search-form [] (let [!query (r/atom "") !input (r/atom nil)] (fn [] - (let [search-query @(rf/subscribe [:search-query]) - show-search-form? @(rf/subscribe [:show-search-form]) + (let [search-query @(rf/subscribe [:search/query]) + show-search-form? @(rf/subscribe [:search/show-form]) service-id @(rf/subscribe [:service-id])] - [:form.relative.text-white.flex.items-center.flex-auto.lg:flex-1 + [:form.relative.text-white.flex.items-center.justify-center.flex-auto.lg:flex-1 {:class (when-not show-search-form? "hidden") :on-submit #(do (.preventDefault %) (when-not (empty? @!query) - (rf/dispatch [:navigate + (rf/dispatch [:navigation/navigate {:name :search-page :params {} :query {:q search-query :serviceId service-id}}])))} - [:div.flex.justify-center.flex-auto.lg:flex-1 - [:button.mx-2 - {:on-click #(rf/dispatch [:search/show-form false])} - [:i.fa-solid.fa-arrow-left]] - [:input.w-full.lg:w-96.bg-transparent.py-2.pl-0.pr-6.mx-2.border-none.focus:ring-transparent.placeholder-white - {:type "text" - :ref #(do (reset! !input %) - (when % - (.focus %))) - :default-value @!query - :on-change #(let [input (.. % -target -value)] - (when-not (empty? input) - (rf/dispatch [:search/change-query input])) - (reset! !query input)) - :placeholder "Search"}] - [:button.mx-4 {:type "submit"} [:i.fa-solid.fa-search]] - [:button.mx-4.text-xs.absolute.right-8.top-3 - {:on-click #(when @!input - (set! (.-value @!input) "") - (reset! !query "") - (.focus @!input)) - :class (when (empty? @!query) :invisible)} - [:i.fa-solid.fa-circle-xmark]]]])))) + [:button.mx-2 + {:on-click #(rf/dispatch [:search/show-form false])} + [:i.fa-solid.fa-arrow-left]] + [:input.w-full.lg:w-96.bg-transparent.py-2.pl-0.pr-6.mx-2.border-none.focus:ring-transparent.placeholder-white + {:type "text" + :ref #(do (reset! !input %) + (when % + (.focus %))) + :default-value @!query + :on-change #(let [input (.. % -target -value)] + (when-not (empty? input) + (rf/dispatch [:search/change-query input])) + (reset! !query input)) + :placeholder "Search"}] + [:button.mx-4 {:type "submit"} [:i.fa-solid.fa-search]] + [:button.mx-4.text-xs.absolute.right-8.top-3 + {:on-click #(when @!input + (set! (.-value @!input) "") + (reset! !query "") + (.focus @!input)) + :class (when (empty? @!query) :invisible)} + [:i.fa-solid.fa-circle-xmark]]])))) (defn mobile-nav-item [route icon label & {:keys [new-tab? active?]}] @@ -62,7 +61,8 @@ [show-mobile-nav? service-color services available-kiosks & {:keys [service-id] :as kiosk-args}] [:<> - [layout/focus-overlay #(rf/dispatch [:toggle-mobile-nav]) show-mobile-nav?] + [layout/focus-overlay #(rf/dispatch [:navigation/toggle-mobile-menu]) + show-mobile-nav?] [:div.fixed.overflow-x-hidden.min-h-screen.w-60.top-0.transition-all.ease-in-out.delay-75.bg-white.dark:bg-neutral-900.z-20 {:class [(if show-mobile-nav? "left-0" "left-[-245px]")]} [:div.flex.justify-center.py-4.items-center.text-white @@ -88,8 +88,8 @@ (defn nav-left-content [title] - (let [show-search-form? @(rf/subscribe [:show-search-form]) - show-queue? @(rf/subscribe [:show-queue]) + (let [show-search-form? @(rf/subscribe [:search/show-form]) + show-queue? @(rf/subscribe [:queue/show]) show-main-player? @(rf/subscribe [:main-player/show])] [:div.flex.items-center.gap-x-4 (when-not (or show-queue? show-main-player?) @@ -98,17 +98,16 @@ [layout/logo :height 35 :width 35]]]) (when (and show-queue? (not show-search-form?)) [:button.text-white.mx-2 - {:on-click #(rf/dispatch [:queue/show - false])} + {:on-click #(rf/dispatch [:queue/show false])} [:i.fa-solid.fa-arrow-left]]) (when (and show-main-player? (not show-search-form?)) [:button.text-white.mx-2 - {:on-click #(rf/dispatch [:player/switch-from-main nil])} + {:on-click #(rf/dispatch [:bg-player/switch-from-main nil])} [:i.fa-solid.fa-arrow-left]]) (when-not (or show-queue? show-main-player? show-search-form?) [:button.text-white.mx-3.lg:hidden {:on-click #(rf/dispatch - [:toggle-mobile-nav])} + [:navigation/toggle-mobile-menu])} [:i.fa-solid.fa-bars]]) (when-not (or show-queue? show-main-player? show-search-form?) [:h1.text-white.text-lg.sm:text-xl.font-bold.line-clamp-1.lg:hidden @@ -122,9 +121,9 @@ (defn nav-right-content [{{:keys [kioskId]} :query-params path :path :as match}] - (let [show-search-form? @(rf/subscribe [:show-search-form]) + (let [show-search-form? @(rf/subscribe [:search/show-form]) show-main-player? @(rf/subscribe [:main-player/show]) - show-queue? @(rf/subscribe [:show-queue]) + show-queue? @(rf/subscribe [:queue/show]) service-id @(rf/subscribe [:service-id]) service-color @(rf/subscribe [:service-color]) services @(rf/subscribe [:services]) @@ -167,7 +166,7 @@ (let [service-id @(rf/subscribe [:service-id]) service-color @(rf/subscribe [:service-color]) services @(rf/subscribe [:services]) - show-mobile-nav? @(rf/subscribe [:show-mobile-nav]) + show-mobile-nav? @(rf/subscribe [:navigation/show-mobile-menu]) settings @(rf/subscribe [:settings]) kiosks @(rf/subscribe [:kiosks])] [:nav.sticky.flex.items-center.px-2.h-14.top-0.z-20 diff --git a/src/frontend/tubo/player/events.cljs b/src/frontend/tubo/player/events.cljs index 10195b5..7246a7f 100644 --- a/src/frontend/tubo/player/events.cljs +++ b/src/frontend/tubo/player/events.cljs @@ -1,116 +1,42 @@ (ns tubo.player.events (:require [goog.object :as gobj] - [re-frame.core :as rf] - [vimsical.re-frame.cofx.inject :as inject])) + [re-frame.core :as rf])) (rf/reg-fx - :volume + :player/volume (fn [{:keys [player volume]}] - (when @player + (when (and player @player) (set! (.-volume @player) (/ volume 100))))) (rf/reg-fx - :mute + :player/mute (fn [{:keys [player muted?]}] - (when @player + (when (and player @player) (set! (.-muted @player) muted?)))) (rf/reg-fx - :src + :player/src (fn [{:keys [player src]}] (set! (.-source @player) (clj->js src)))) (rf/reg-fx - :loop + :player/loop (fn [{:keys [player loop]}] (set! (.-loop @player) loop))) (rf/reg-fx - :current-time + :player/time (fn [{:keys [time player]}] - (set! (.-currentTime @player) time))) - -(rf/reg-event-fx - :bg-player/seek - [(rf/inject-cofx ::inject/sub [:player])] - (fn [{:keys [db player]} [_ time]] - (when (:bg-player/ready db) - {:current-time {:time time :player player}}))) - -(rf/reg-event-fx - :main-player/seek - [(rf/inject-cofx ::inject/sub [:main-player])] - (fn [{:keys [main-player]} [_ time]] - {:current-time {:time time :player main-player}})) + (when (and player @player) + (set! (.-currentTime @player) time)))) (rf/reg-fx - :pause! + :player/pause (fn [{:keys [paused? player]}] - (when @player + (when (and player @player) (set! (.-paused @player) paused?)))) -(rf/reg-event-db - :bg-player/set-paused - (fn [db [_ val]] - (assoc db :paused val))) - -(rf/reg-event-fx - :bg-player/pause - [(rf/inject-cofx ::inject/sub [:player])] - (fn [{:keys [player]} [_ paused?]] - {:pause! {:paused? paused? - :player player}})) - -(rf/reg-event-fx - :main-player/pause - [(rf/inject-cofx ::inject/sub [:main-player])] - (fn [{:keys [db main-player]} [_ paused?]] - (when (:main-player/ready db) - {:pause! {:paused? paused? - :player main-player}}))) - -(rf/reg-event-fx - :bg-player/play - [(rf/inject-cofx ::inject/sub [:elapsed-time]) - (rf/inject-cofx ::inject/sub [:main-player])] - (fn [{:keys [db elapsed-time main-player]}] - {:fx [[:dispatch [:bg-player/set-paused false]] - [:dispatch [:bg-player/seek @elapsed-time]] - (when (and (:main-player/ready db) @main-player) - [:dispatch [:main-player/pause true]])]})) - -(rf/reg-event-fx - :main-player/play - [(rf/inject-cofx ::inject/sub [:elapsed-time]) - (rf/inject-cofx ::inject/sub [:player])] - (fn [{:keys [db player]}] - {:fx [(when (and (:bg-player/ready db) @player) - [:dispatch [:bg-player/pause true]])]})) - -(rf/reg-event-fx - :bg-player/stop - (fn [_] - {:fx [[:dispatch [:bg-player/pause true]] - [:dispatch [:bg-player/seek 0]]]})) - -(rf/reg-event-fx - :bg-player/start - [(rf/inject-cofx ::inject/sub [:player]) - (rf/inject-cofx ::inject/sub [:elapsed-time])] - (fn [{:keys [db player]} _] - {:fx [[:dispatch [:bg-player/set-paused true]] - [:dispatch [:bg-player/pause false]] - [:dispatch [:player/change-volume (:volume-level db) player]]]})) - -(rf/reg-event-fx - :main-player/start - [(rf/inject-cofx ::inject/sub [:elapsed-time])] - (fn [{:keys [db elapsed-time]} _] - {:fx [[:dispatch [:main-player/pause false]] - (when (and (:main-player/show db) (not (:bg-player/ready db))) - [:dispatch [:main-player/seek @elapsed-time]])]})) - (rf/reg-fx :media-session-metadata (fn [metadata] @@ -122,7 +48,7 @@ :media-session-handlers (fn [{:keys [current-pos player]}] (when (gobj/containsKey js/navigator "mediaSession") - (let [current-time (and @player (.-currentTime @player)) + (let [current-time (and player @player (.-currentTime @player)) update-position #(.setPositionState js/navigator.mediaSession {:duration (.-duration @player) @@ -153,177 +79,17 @@ :player/change-volume [(rf/inject-cofx :store)] (fn [{:keys [db store]} [_ value player]] - {:db (assoc db :volume-level value) - :store (assoc store :volume-level value) - :volume {:player player :volume value}})) - -(rf/reg-event-fx - :bg-player/mute - [(rf/inject-cofx :store)] - (fn [{:keys [db store]} [_ value player]] - {:db (assoc db :muted value) - :store (assoc store :muted value) - :mute {:player player :muted? value}})) - -(rf/reg-event-fx - :bg-player/hide - [(rf/inject-cofx :store)] - (fn [{:keys [db store]} _] - {:db (assoc db :bg-player/show false) - :store (assoc store :bg-player/show false)})) + {:db (assoc db :player/volume value) + :store (assoc store :player/volume value) + :player/volume {:player player :volume value}})) (rf/reg-event-fx :player/loop [(rf/inject-cofx :store)] (fn [{:keys [db store]} _] - (let [loop-state (case (:loop-playback db) + (let [loop-state (case (:player/loop db) :stream false :playlist :stream :playlist)] - {:db (assoc db :loop-playback loop-state) - :store (assoc store :loop-playback loop-state)}))) - -(rf/reg-event-fx - :bg-player/dispose - [(rf/inject-cofx :store)] - (fn [{:keys [db store]} _] - (let [remove-entries - (fn [elem] - (-> elem - (assoc :queue []) - (assoc :queue-pos 0)))] - {:db (remove-entries db) - :store (remove-entries store) - :fx [[:dispatch [:bg-player/pause true]] - [:dispatch [:bg-player/seek 0]] - [:dispatch [:bg-player/hide]]]}))) - -(rf/reg-event-db - :bg-player/ready - (fn [db [_ ready]] - (assoc db :bg-player/ready ready))) - -(rf/reg-event-db - :main-player/ready - (fn [db [_ ready]] - (assoc db :main-player/ready ready))) - -(rf/reg-event-fx - :player/switch-to-background - [(rf/inject-cofx :store)] - (fn [{:keys [db store]} [_ stream notify?]] - (let [updated-db (update db :queue conj stream) - idx (.indexOf (:queue updated-db) stream)] - {:db updated-db - :store (assoc store :queue (:queue updated-db)) - :fx [[:dispatch - [:player/fetch-stream - (:url stream) idx (= (count (:queue db)) 0)]] - (when (and notify? (not (= (count (:queue db)) 0))) - [:dispatch - [:notifications/add - {:status-text "Added stream to queue" - :failure :info}]])]}))) - -(rf/reg-event-fx - :player/show-main-player - (fn [{:keys [db]} [_ val]] - {:db (apply assoc - (assoc db :main-player/show val) - (when val [:show-search-form false])) - :body-overflow val})) - -(rf/reg-event-fx - :player/switch-from-main - [(rf/inject-cofx ::inject/sub [:elapsed-time])] - (fn [{:keys [db]} _] - {:db (assoc db :bg-player/show true) - :fx [[:dispatch [:player/show-main-player false]] - [:dispatch [:main-player/pause true]]]})) - -(rf/reg-event-fx - :player/switch-to-main - [(rf/inject-cofx :store)] - (fn [{:keys [db]} _] - {:fx [[:dispatch [:player/show-main-player true]]] - :db (assoc db :bg-player/show false) - :scroll-to-top nil})) - -(rf/reg-event-fx - :player/load-related-streams - (fn [_ [_ res]] - (let [{:keys [related-streams]} (js->clj res :keywordize-keys true)] - {:fx [[:dispatch [:queue/add-n related-streams]]]}))) - -(rf/reg-event-fx - :player/load-stream - [(rf/inject-cofx :store) - (rf/inject-cofx ::inject/sub [:player])] - (fn [{:keys [db store player]} [_ idx play? res]] - (let [stream-res (js->clj res :keywordize-keys true)] - {:db (assoc db - :bg-player/show (not (:main-player/show db)) - :bg-player/loading false) - :store (assoc store :bg-player/show (not (:main-player/show db))) - :fx (apply conj - [(when play? - [:dispatch [:queue/change-stream stream-res idx]])] - (when (and (:bg-player/ready db) play?) - [[:media-session-metadata - {:title (:name stream-res) - :artist (:uploader-name stream-res) - :artwork [{:src (:thumbnail-url stream-res)}]}] - [:media-session-handlers - {:current-pos idx - :player player}]]))}))) - -(rf/reg-event-fx - :player/bad-response - (fn [{:keys [db]} [_ idx play? res]] - {:db (assoc db - :bg-player/loading - false) - :fx [[:dispatch [:bad-response res]] - (when play? - (if (> (-> db - :queue - count) - 1) - [:dispatch [:queue/change-pos (inc idx)]] - [:dispatch [:bg-player/dispose]]))]})) - -(rf/reg-event-fx - :player/fetch-related-streams - (fn [{:keys [db]} [_ url]] - {:fx [[:dispatch - [:stream/fetch url - [:player/load-related-streams]] [:bad-response]]] - :db (assoc db :bg-player/loading true)})) - -(rf/reg-event-fx - :player/fetch-stream - (fn [{:keys [db]} [_ url idx play?]] - {:fx [[:dispatch - [:stream/fetch url - [:player/load-stream idx play?] - [:player/bad-response idx play?]]]] - :db (assoc db :bg-player/loading play?)})) - -(rf/reg-event-fx - :player/start-radio - (fn [{:keys [db]} [_ stream]] - {:fx [[:dispatch [:player/switch-to-background stream]] - (when (not= (count (:queue db)) 0) - [:dispatch [:queue/change-pos (count (:queue db))]]) - [:dispatch [:player/fetch-related-streams (:url stream)]] - [:dispatch - [:notifications/add - {:status-text "Started stream radio" - :failure :info}]]]})) - -(rf/reg-event-db - :main-player/toggle-layout - (fn [db [_ layout]] - (assoc-in db - [:queue (:queue-pos db) layout] - (not (get-in db [:queue (:queue-pos db) layout]))))) + {:db (assoc db :player/loop loop-state) + :store (assoc store :player/loop loop-state)}))) diff --git a/src/frontend/tubo/player/subs.cljs b/src/frontend/tubo/player/subs.cljs index 312451d..a4f726d 100644 --- a/src/frontend/tubo/player/subs.cljs +++ b/src/frontend/tubo/player/subs.cljs @@ -3,71 +3,34 @@ [re-frame.core :as rf] [reagent.core :as r])) -(defonce !player (atom nil)) -(defonce !main-player (atom nil)) (defonce !elapsed-time (r/atom 0)) (rf/reg-sub - :player - (fn [_ _] - !player)) - -(rf/reg-sub - :main-player - (fn [_ _] - !main-player)) - -(rf/reg-sub - :bg-player/ready + :player/loop (fn [db _] - (:bg-player/ready db))) + (:player/loop db))) (rf/reg-sub - :main-player/ready + :player/shuffled (fn [db _] - (:main-player/ready db))) + (:player/shuffled db))) (rf/reg-sub - :bg-player/show + :player/paused (fn [db _] - (:bg-player/show db))) + (:player/paused db))) (rf/reg-sub - :bg-player/loading + :player/volume (fn [db _] - (:bg-player/loading db))) + (:player/volume db))) (rf/reg-sub - :loop-playback + :player/muted (fn [db _] - (:loop-playback db))) - -(rf/reg-sub - :shuffle - (fn [db _] - (:shuffle db))) - -(rf/reg-sub - :paused - (fn [db _] - (:paused db))) - -(rf/reg-sub - :volume-level - (fn [db _] - (:volume-level db))) - -(rf/reg-sub - :muted - (fn [db _] - (:muted db))) + (:player/muted db))) (rf/reg-sub :elapsed-time (fn [_ _] !elapsed-time)) - -(rf/reg-sub - :main-player/show - (fn [db _] - (:main-player/show db))) diff --git a/src/frontend/tubo/player/views.cljs b/src/frontend/tubo/player/views.cljs index 022d882..e1c5549 100644 --- a/src/frontend/tubo/player/views.cljs +++ b/src/frontend/tubo/player/views.cljs @@ -2,191 +2,70 @@ (:require [re-frame.core :as rf] [reagent.core :as r] - [reitit.frontend.easy :as rfe] - [tubo.bookmarks.modals :as modals] - [tubo.components.layout :as layout] - [tubo.components.player :as player] - [tubo.queue.views :as queue] - [tubo.stream.views :as stream] - [tubo.utils :as utils])) + ["@vidstack/react" :refer (MediaPlayer MediaProvider Poster)] + ["@vidstack/react/player/layouts/default" :refer + (defaultLayoutIcons DefaultVideoLayout)])) -(defn stream-metadata - [{:keys [thumbnail-url url name uploader-url uploader-name]}] - [:div.flex.items-center.lg:flex-1 - [:div - [layout/thumbnail thumbnail-url (rfe/href :stream-page nil {:url url}) - name nil :classes [:h-14 :py-2 "w-[70px]"]]] - [:div.flex.flex-col.px-2 - [:a.text-xs.line-clamp-1 - {:href (rfe/href :stream-page nil {:url url}) - :title name} - name] - [:a.text-xs.pt-2.text-neutral-600.dark:text-neutral-300.line-clamp-1 - {:href (rfe/href :channel-page nil {:url uploader-url}) - :title uploader-name} - uploader-name]]]) +(defn get-video-player-sources + [available-streams service-id] + (if available-streams + (if (= service-id 3) + (map (fn [{:keys [content]}] {:src content :type "video/mp4"}) + (reverse available-streams)) + (->> available-streams + (filter #(and (not= (:format %) "WEBMA_OPUS") + (not= (:format %) "OPUS") + (not= (:format %) "M4A"))) + (sort-by :bitrate) + (#(if (empty? (filter (fn [x] (= (:format x) "MP3")) %)) + (reverse %) + %)) + (map (fn [{:keys [content]}] {:src content :type "video/mp4"})) + first)) + [])) -(defn main-controls - [!player color] - (let [queue @(rf/subscribe [:queue]) - queue-pos @(rf/subscribe [:queue-pos]) - loading? @(rf/subscribe [:bg-player/loading]) - loop-playback @(rf/subscribe [:loop-playback]) - shuffle? @(rf/subscribe [:shuffle]) - bg-player-ready? @(rf/subscribe [:bg-player/ready]) - paused? @(rf/subscribe [:paused]) - !elapsed-time @(rf/subscribe [:elapsed-time])] - [:div.flex.flex-col.items-center.ml-auto - [:div.flex.justify-end - [player/loop-button loop-playback color] - [player/button - :icon [:i.fa-solid.fa-backward-step] - :on-click #(rf/dispatch [:queue/change-pos (dec queue-pos)]) - :disabled? (not (and queue (not= queue-pos 0)))] - [player/button - :icon [:i.fa-solid.fa-backward] - :on-click #(rf/dispatch [:bg-player/seek (- @!elapsed-time 5)])] - [player/button - :icon - (if (and (not loading?) @!player) - (if paused? - [:i.fa-solid.fa-play] - [:i.fa-solid.fa-pause]) - [layout/loading-icon color "lg:text-2xl"]) - :on-click - #(rf/dispatch [:bg-player/pause (not (.-paused @!player))]) - :show-on-mobile? true - :extra-classes ["lg:text-2xl"]] - [player/button - :icon [:i.fa-solid.fa-forward] - :on-click #(rf/dispatch [:bg-player/seek (+ @!elapsed-time 5)])] - [player/button - :icon [:i.fa-solid.fa-forward-step] - :on-click #(rf/dispatch [:queue/change-pos (inc queue-pos)]) - :disabled? (not (and queue (< (inc queue-pos) (count queue))))] - [player/shuffle-button shuffle? color]] - [:div.hidden.lg:flex.items-center.text-sm - [:span.mx-2 - (if (and bg-player-ready? @!player @!elapsed-time) - (utils/format-duration @!elapsed-time) - "--:--")] - [:div.w-20.lg:w-64.mx-2.flex.items-center - [player/time-slider !player !elapsed-time color]] - [:span.mx-2 - (if (and bg-player-ready? @!player) - (utils/format-duration (.-duration @!player)) - "--:--")]]])) - -(defn extra-controls - [_!player _stream _color] - (let [!menu-active? (r/atom nil)] - (fn [!player {:keys [url uploader-url] :as stream} color] - (let [muted? @(rf/subscribe [:muted]) - volume @(rf/subscribe [:volume-level]) - queue @(rf/subscribe [:queue]) - queue-pos @(rf/subscribe [:queue-pos]) - bookmarks @(rf/subscribe [:bookmarks]) - liked? (some #(= (:url %) url) - (-> bookmarks - first - :items)) - bookmark #(rf/dispatch [:modals/open [modals/add-to-bookmark %]])] - [:div.flex.lg:justify-end.lg:flex-1 - [player/volume-slider !player volume muted? color] - [player/button - :icon [:i.fa-solid.fa-list] - :on-click #(rf/dispatch [:queue/show true]) - :show-on-mobile? true - :extra-classes [:!pl-4 :!pr-3]] - [layout/popover-menu !menu-active? - [{:label (if liked? "Remove favorite" "Favorite") - :icon [:i.fa-solid.fa-heart - (when liked? {:style {:color color}})] - :on-click #(rf/dispatch [(if liked? :likes/remove :likes/add) - stream])} - {:label "Play radio" - :icon [:i.fa-solid.fa-tower-cell] - :on-click #(rf/dispatch [:player/start-radio stream])} - {:label "Add current to playlist" - :icon [:i.fa-solid.fa-plus] - :on-click #(bookmark stream)} - {:label "Add queue to playlist" - :icon [:i.fa-solid.fa-list] - :on-click #(bookmark queue)} - {:label "Remove from queue" - :icon [:i.fa-solid.fa-trash] - :on-click #(rf/dispatch [:queue/remove queue-pos])} - {:label "Switch to main" - :icon [:i.fa-solid.fa-display] - :on-click #(rf/dispatch [:player/switch-to-main])} - {:label "Show channel details" - :icon [:i.fa-solid.fa-user] - :on-click #(rf/dispatch [:navigate - {:name :channel-page - :params {} - :query {:url uploader-url}}])} - {:label "Close player" - :icon [:i.fa-solid.fa-close] - :on-click #(rf/dispatch [:bg-player/dispose])}] - :menu-styles {:bottom "30px" :top nil :right "10px"} - :extra-classes [:pt-1 :!pl-4 :px-3]]])))) - -(defn background-player - [] - (let [!player @(rf/subscribe [:player]) - stream @(rf/subscribe [:queue-stream]) - show-queue? @(rf/subscribe [:show-queue]) - show-player? @(rf/subscribe [:bg-player/show]) - dark-theme? @(rf/subscribe [:dark-theme]) - color (-> stream - :service-id - utils/get-service-color) - bg-color (str "rgba(" - (if dark-theme? "23,23,23" "255,255,255") - ",0.95)") - bg-image (str "linear-gradient(" - bg-color - "," - bg-color - "),url(" - (:thumbnail-url stream) - ")")] - [:div.sticky.absolute.left-0.bottom-0.z-10.p-3.transition-all.ease-in.relative - {:style - {:visibility (when (or (not show-player?) show-queue?) "hidden") - :opacity (if (or (not show-player?) show-queue?) 0 1) - :background-image bg-image - :background-size "cover" - :background-position "center" - :background-repeat "no-repeat"}} - [:div.flex.items-center - [player/audio-player stream !player] - [stream-metadata stream] - [main-controls !player color] - [extra-controls !player stream color]]])) - -(defn main-player - [] - (let [queue @(rf/subscribe [:queue]) - queue-pos @(rf/subscribe [:queue-pos]) - bookmarks @(rf/subscribe [:bookmarks]) - !player @(rf/subscribe [:main-player]) - stream @(rf/subscribe [:queue-stream]) - show-player? @(rf/subscribe [:main-player/show])] - [:div.fixed.w-full.bg-neutral-100.dark:bg-neutral-900.overflow-auto.z-10.transition-all.ease-in-out - {:class ["h-[calc(100%-56px)]" - (if show-player? "translate-y-0" "translate-y-full")]} - (when (and show-player? stream) - [:div - [:div.flex.flex-col.items-center.w-full.xl:py-6 - [player/video-player stream !player]] - [:div.flex.items-center.justify-center - [:div.flex.flex-col.gap-y-1.w-full.h-fit.max-h-64.overflow-y-auto - {:class ["lg:w-4/5" "xl:w-3/5"]} - (for [[i item] (map-indexed vector queue)] - ^{:key i} [queue/queue-item item queue queue-pos i bookmarks])]] - [layout/content-container - [stream/metadata stream] - [stream/description stream] - [stream/comments stream] - [stream/suggested stream]]])])) +(defn video-player + [_stream _!player] + (let [!elapsed-time @(rf/subscribe [:elapsed-time]) + !main-player-first? (r/atom true)] + (r/create-class + {:component-will-unmount #(rf/dispatch [:main-player/ready false]) + :reagent-render + (fn [{:keys [name video-streams audio-streams thumbnail-url service-id]} + !player] + (let [show-main-player? @(rf/subscribe [:main-player/show])] + [:> MediaPlayer + {:title name + :src (get-video-player-sources (into video-streams + audio-streams) + service-id) + :poster thumbnail-url + :class "w-full xl:w-3/5 overflow-hidden" + :playsInline true + :ref #(reset! !player %) + :loop (when show-main-player? + (= @(rf/subscribe [:player/loop]) :stream)) + :onSeeked (when show-main-player? + #(reset! !elapsed-time (.-currentTime @!player))) + :onTimeUpdate (when show-main-player? + #(reset! !elapsed-time (.-currentTime @!player))) + :onEnded #(when show-main-player? + (rf/dispatch [:queue/change-pos + (inc @(rf/subscribe + [:queue/position]))]) + (reset! !elapsed-time 0)) + :onLoadedData (fn [] + (when show-main-player? + (rf/dispatch [:main-player/start])) + (when (and @!main-player-first? show-main-player?) + (reset! !main-player-first? false))) + :onPlay #(rf/dispatch [:main-player/play]) + :onCanPlay #(rf/dispatch [:main-player/ready true]) + :onSourceChange #(when-not @!main-player-first? + (reset! !elapsed-time 0))} + [:> MediaProvider + [:> Poster + {:src thumbnail-url + :alt name + :class :vds-poster}]] + [:> DefaultVideoLayout {:icons defaultLayoutIcons}]]))}))) diff --git a/src/frontend/tubo/playlist/views.cljs b/src/frontend/tubo/playlist/views.cljs index cd31b72..ad2be19 100644 --- a/src/frontend/tubo/playlist/views.cljs +++ b/src/frontend/tubo/playlist/views.cljs @@ -4,8 +4,8 @@ [re-frame.core :as rf] [reitit.frontend.easy :as rfe] [tubo.bookmarks.modals :as modals] - [tubo.components.items :as items] - [tubo.components.layout :as layout])) + [tubo.items.views :as items] + [tubo.layout.views :as layout])) (defn playlist [{{:keys [url]} :query-params}] diff --git a/src/frontend/tubo/queue/events.cljs b/src/frontend/tubo/queue/events.cljs index 1cc189f..04502fd 100644 --- a/src/frontend/tubo/queue/events.cljs +++ b/src/frontend/tubo/queue/events.cljs @@ -6,8 +6,8 @@ :queue/show (fn [{:keys [db]} [_ show?]] {:db (apply assoc - (assoc db :show-queue show?) - (when show? [:show-search-form false])) + (assoc db :queue/show show?) + (when show? [:search/show-form false])) :body-overflow show?})) (rf/reg-event-fx @@ -15,7 +15,7 @@ [(rf/inject-cofx :store)] (fn [{:keys [db store]} [_ val]] (let [queue (:queue db) - queue-pos (+ 1 (:queue-pos db)) + queue-pos (+ 1 (:queue/position db)) queue-to-end (subvec queue queue-pos) shuffled-to-end (shuffle queue-to-end) unshuffled-to-end (into (subvec queue 0 queue-pos) queue-to-end) @@ -26,8 +26,10 @@ updated-db (assoc db :queue (if val shuffled-queue unshuffled-queue))] - {:db (assoc updated-db :shuffle val :queue/unshuffled unshuffled-queue) - :store (assoc store :queue (:queue updated-db) :shuffle val)}))) + {:db (assoc updated-db + :player/shuffled val + :queue/unshuffled unshuffled-queue) + :store (assoc store :queue (:queue updated-db) :player/shuffled val)}))) (rf/reg-event-fx :queue/add @@ -49,7 +51,7 @@ (fn [{:keys [db]} [_ streams notify?]] {:fx (into (map (fn [stream] [:dispatch [:queue/add stream]]) streams) [[:dispatch - [:player/fetch-stream + [:bg-player/fetch-stream (-> streams first :url) @@ -69,7 +71,7 @@ (let [updated-db (update db :queue #(into (subvec % 0 pos) (subvec % (inc pos)))) - queue-pos (:queue-pos db) + queue-pos (:queue/position db) queue-length (count (:queue updated-db))] {:db updated-db :store (assoc store :queue (:queue updated-db)) @@ -85,7 +87,7 @@ (= pos queue-pos) pos :else (dec queue-pos))]]] (= (count (:queue updated-db)) 0) - [[:dispatch [:player/dispose]] + [[:dispatch [:bg-player/dispose]] [:dispatch [:queue/show false]]] :else [])}))) @@ -95,15 +97,15 @@ (fn [{:keys [db]} [_ i]] (let [idx (if (< i (count (:queue db))) i - (when (= (:loop-playback db) :playlist) 0)) + (when (= (:player/loop db) :playlist) 0)) stream (get (:queue db) idx)] (when stream - {:fx [[:dispatch [:player/fetch-stream (:url stream) idx true]]]})))) + {:fx [[:dispatch [:bg-player/fetch-stream (:url stream) idx true]]]})))) (rf/reg-event-fx :queue/change-stream [(rf/inject-cofx :store)] (fn [{:keys [db store]} [_ stream idx]] (let [update-entry (fn [x] (update-in x [:queue idx] #(merge % stream)))] - {:db (assoc (update-entry db) :queue-pos idx) - :store (assoc (update-entry store) :queue-pos idx)}))) + {:db (assoc (update-entry db) :queue/position idx) + :store (assoc (update-entry store) :queue/position idx)}))) diff --git a/src/frontend/tubo/queue/subs.cljs b/src/frontend/tubo/queue/subs.cljs index fbec406..5b37de5 100644 --- a/src/frontend/tubo/queue/subs.cljs +++ b/src/frontend/tubo/queue/subs.cljs @@ -13,18 +13,18 @@ (:queue/unshuffled db))) (rf/reg-sub - :queue-pos + :queue/position (fn [db _] - (:queue-pos db))) + (:queue/position db))) (rf/reg-sub - :show-queue + :queue/show (fn [db _] - (:show-queue db))) + (:queue/show db))) (rf/reg-sub - :queue-stream + :queue/current (fn [_] - [(rf/subscribe [:queue]) (rf/subscribe [:queue-pos])]) + [(rf/subscribe [:queue]) (rf/subscribe [:queue/position])]) (fn [[queue pos] _] (and (not-empty queue) (< pos (count queue)) (nth queue pos)))) diff --git a/src/frontend/tubo/queue/views.cljs b/src/frontend/tubo/queue/views.cljs index 0947a74..df924a5 100644 --- a/src/frontend/tubo/queue/views.cljs +++ b/src/frontend/tubo/queue/views.cljs @@ -4,8 +4,8 @@ [re-frame.core :as rf] [reitit.frontend.easy :as rfe] [tubo.bookmarks.modals :as modals] - [tubo.components.layout :as layout] - [tubo.components.player :as player] + [tubo.bg-player.views :as player] + [tubo.layout.views :as layout] [tubo.utils :as utils])) (defn item-metadata @@ -41,7 +41,7 @@ :on-click #(rf/dispatch [(if liked? :likes/remove :likes/add) item])} {:label "Play radio" :icon [:i.fa-solid.fa-tower-cell] - :on-click #(rf/dispatch [:player/start-radio item])} + :on-click #(rf/dispatch [:bg-player/start-radio item])} {:label "Add to playlist" :icon [:i.fa-solid.fa-plus] :on-click #(rf/dispatch [:modals/open [modals/add-to-bookmark item]])} @@ -50,7 +50,7 @@ :on-click #(rf/dispatch [:queue/remove i])} {:label "Show channel details" :icon [:i.fa-solid.fa-user] - :on-click #(rf/dispatch [:navigate + :on-click #(rf/dispatch [:navigation/navigate {:name :channel-page :params {} :query {:url uploader-url}}])}] @@ -82,15 +82,15 @@ (defn main-controls [color] - (let [loop-playback @(rf/subscribe [:loop-playback]) - shuffle? @(rf/subscribe [:shuffle]) - !player @(rf/subscribe [:player]) + (let [loop-playback @(rf/subscribe [:player/loop]) + shuffle? @(rf/subscribe [:player/shuffled]) + !player @(rf/subscribe [:bg-player]) loading? @(rf/subscribe [:bg-player/loading]) bg-player-ready? @(rf/subscribe [:bg-player/ready]) - paused? @(rf/subscribe [:paused]) + paused? @(rf/subscribe [:player/paused]) !elapsed-time @(rf/subscribe [:elapsed-time]) queue @(rf/subscribe [:queue]) - queue-pos @(rf/subscribe [:queue-pos])] + queue-pos @(rf/subscribe [:queue/position])] [:<> [:div.flex.flex-auto.py-2.w-full.items-center.text-sm [:span.mr-4.whitespace-nowrap @@ -141,10 +141,10 @@ (defn queue [] - (let [show-queue @(rf/subscribe [:show-queue]) - stream @(rf/subscribe [:queue-stream]) + (let [show-queue @(rf/subscribe [:queue/show]) + stream @(rf/subscribe [:queue/current]) bookmarks @(rf/subscribe [:bookmarks]) - queue-pos @(rf/subscribe [:queue-pos]) + queue-pos @(rf/subscribe [:queue/position]) queue @(rf/subscribe [:queue]) color (-> stream :service-id diff --git a/src/frontend/tubo/routes.cljs b/src/frontend/tubo/routes.cljs index 67de20e..2086a27 100644 --- a/src/frontend/tubo/routes.cljs +++ b/src/frontend/tubo/routes.cljs @@ -68,7 +68,7 @@ (defn on-navigate [new-match] (when new-match - (rf/dispatch [:navigated new-match]))) + (rf/dispatch [:navigation/navigated new-match]))) (defn start-routes! [] diff --git a/src/frontend/tubo/search/events.cljs b/src/frontend/tubo/search/events.cljs index 320e5ec..a40948a 100644 --- a/src/frontend/tubo/search/events.cljs +++ b/src/frontend/tubo/search/events.cljs @@ -2,7 +2,7 @@ (:require [re-frame.core :as rf] [tubo.api :as api] - [tubo.components.layout :as layout])) + [tubo.layout.views :as layout])) (rf/reg-event-fx :search/fetch @@ -17,7 +17,7 @@ (fn [{:keys [db]} [_ res]] (let [search-res (js->clj res :keywordize-keys true)] {:db (assoc db - :search-results search-res + :search/results search-res :show-page-loading false) :fx [[:dispatch [:services/fetch search-res]]]}))) @@ -34,8 +34,8 @@ (fn [{:keys [db]} [_ service-id query]] {:db (assoc db :show-page-loading true - :show-search-form true - :search-results nil) + :search/show-form true + :search/results nil) :fx [[:dispatch [:search/fetch service-id [:search/load-page] [:search/bad-page-response service-id query] @@ -48,13 +48,13 @@ (let [search-res (js->clj res :keywordize-keys true)] (if (empty? (:items search-res)) (-> db - (assoc-in [:search-results :next-page] nil) + (assoc-in [:search/results :next-page] nil) (assoc :show-pagination-loading false)) (-> db - (update-in [:search-results :items] + (update-in [:search/results :items] #(apply conj %1 %2) (:items search-res)) - (assoc-in [:search-results :next-page] (:next-page search-res)) + (assoc-in [:search/results :next-page] (:next-page search-res)) (assoc :show-pagination-loading false)))))) (rf/reg-event-fx @@ -73,12 +73,12 @@ :search/show-form (fn [db [_ show?]] (when-not (= (-> db - :current-match + :navigation/current-match :path) "search") - (assoc db :show-search-form show?)))) + (assoc db :search/show-form show?)))) (rf/reg-event-db :search/change-query (fn [db [_ res]] - (assoc db :search-query res))) + (assoc db :search/query res))) diff --git a/src/frontend/tubo/search/subs.cljs b/src/frontend/tubo/search/subs.cljs index 59c69f5..c64f09e 100644 --- a/src/frontend/tubo/search/subs.cljs +++ b/src/frontend/tubo/search/subs.cljs @@ -3,16 +3,16 @@ [re-frame.core :as rf])) (rf/reg-sub - :search-results + :search/results (fn [db _] - (:search-results db))) + (:search/results db))) (rf/reg-sub - :search-query + :search/query (fn [db _] - (:search-query db))) + (:search/query db))) (rf/reg-sub - :show-search-form + :search/show-form (fn [db _] - (:show-search-form db))) + (:search/show-form db))) diff --git a/src/frontend/tubo/search/views.cljs b/src/frontend/tubo/search/views.cljs index 4fdc6d6..e97a627 100644 --- a/src/frontend/tubo/search/views.cljs +++ b/src/frontend/tubo/search/views.cljs @@ -1,12 +1,12 @@ (ns tubo.search.views (:require [re-frame.core :as rf] - [tubo.components.items :as items] - [tubo.components.layout :as layout])) + [tubo.items.views :as items] + [tubo.layout.views :as layout])) (defn search [{{:keys [q serviceId]} :query-params}] - (let [{:keys [items next-page]} @(rf/subscribe [:search-results]) + (let [{:keys [items next-page]} @(rf/subscribe [:search/results]) next-page-url (:url next-page) service-id (or @(rf/subscribe [:service-id]) serviceId) scrolled-to-bottom? @(rf/subscribe [:scrolled-to-bottom])] diff --git a/src/frontend/tubo/settings/views.cljs b/src/frontend/tubo/settings/views.cljs index 0658bbb..51520bb 100644 --- a/src/frontend/tubo/settings/views.cljs +++ b/src/frontend/tubo/settings/views.cljs @@ -1,7 +1,7 @@ (ns tubo.settings.views (:require [re-frame.core :as rf] - [tubo.components.layout :as layout])) + [tubo.layout.views :as layout])) (defn boolean-input [label key value] diff --git a/src/frontend/tubo/stream/events.cljs b/src/frontend/tubo/stream/events.cljs index 7b55e7c..709c68a 100644 --- a/src/frontend/tubo/stream/events.cljs +++ b/src/frontend/tubo/stream/events.cljs @@ -2,7 +2,7 @@ (:require [re-frame.core :as rf] [tubo.api :as api] - [tubo.components.layout :as layout])) + [tubo.layout.views :as layout])) (rf/reg-event-fx :stream/fetch diff --git a/src/frontend/tubo/stream/views.cljs b/src/frontend/tubo/stream/views.cljs index 26148fc..c78cfc9 100644 --- a/src/frontend/tubo/stream/views.cljs +++ b/src/frontend/tubo/stream/views.cljs @@ -5,9 +5,9 @@ [reitit.frontend.easy :as rfe] [tubo.bookmarks.modals :as modals] [tubo.comments.views :as comments] - [tubo.components.items :as items] - [tubo.components.layout :as layout] - [tubo.components.player :as player] + [tubo.items.views :as items] + [tubo.layout.views :as layout] + [tubo.player.views :as player] [tubo.utils :as utils])) (defn metadata-popover @@ -22,10 +22,10 @@ [layout/popover-menu !menu-active? [{:label "Add to queue" :icon [:i.fa-solid.fa-headphones] - :on-click #(rf/dispatch [:player/switch-to-background stream true])} + :on-click #(rf/dispatch [:bg-player/show stream true])} {:label "Play radio" :icon [:i.fa-solid.fa-tower-cell] - :on-click #(rf/dispatch [:player/start-radio stream])} + :on-click #(rf/dispatch [:bg-player/start-radio stream])} {:label (if liked? "Remove favorite" "Favorite") :icon (if liked? [:i.fa-solid.fa-heart diff --git a/src/frontend/tubo/subs.cljs b/src/frontend/tubo/subs.cljs index a789ccb..9b952e1 100644 --- a/src/frontend/tubo/subs.cljs +++ b/src/frontend/tubo/subs.cljs @@ -2,10 +2,13 @@ (:require [reagent.core :as r] [re-frame.core :as rf] + [tubo.bg-player.subs] [tubo.bookmarks.subs] [tubo.channel.subs] [tubo.kiosks.subs] + [tubo.main-player.subs] [tubo.modals.subs] + [tubo.navigation.subs] [tubo.notifications.subs] [tubo.player.subs] [tubo.playlist.subs] @@ -58,11 +61,6 @@ (fn [{:keys [theme]} _] (or (and (= theme "auto") (= @!auto-theme "dark")) (= theme "dark")))) -(rf/reg-sub - :current-match - (fn [db _] - (:current-match db))) - (rf/reg-sub :page-scroll (fn [db _] @@ -77,8 +75,3 @@ :show-pagination-loading (fn [db _] (:show-pagination-loading db))) - -(rf/reg-sub - :show-mobile-nav - (fn [db _] - (:show-mobile-nav db))) diff --git a/src/frontend/tubo/views.cljs b/src/frontend/tubo/views.cljs index a0bc34c..85f85d5 100644 --- a/src/frontend/tubo/views.cljs +++ b/src/frontend/tubo/views.cljs @@ -1,14 +1,15 @@ (ns tubo.views (:require [re-frame.core :as rf] + [tubo.bg-player.views :as bg-player] + [tubo.main-player.views :as main-player] [tubo.navigation.views :as navigation] [tubo.notifications.views :as notifications] - [tubo.player.views :as player] [tubo.queue.views :as queue])) (defn app [] - (let [current-match @(rf/subscribe [:current-match]) + (let [current-match @(rf/subscribe [:navigation/current-match]) dark-theme? @(rf/subscribe [:dark-theme])] [:div {:class (when dark-theme? :dark)} [:div.font-nunito-sans.min-h-screen.h-full.relative.flex.flex-col.dark:text-white.bg-neutral-100.dark:bg-neutral-900 @@ -20,5 +21,5 @@ :view)] [view current-match]) [queue/queue] - [player/main-player] - [player/background-player]]]])) + [main-player/player] + [bg-player/player]]]])) -- cgit v1.2.3