diff options
38 files changed, 962 insertions, 925 deletions
diff --git a/manifest.scm b/manifest.scm deleted file mode 100644 index 69f263a..0000000 --- a/manifest.scm +++ /dev/null @@ -1,5 +0,0 @@ -(specifications->manifest - '("clojure" - "node" - "clojure-tools" - "openjdk:jdk")) 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/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/components/items.cljs b/src/frontend/tubo/items/views.cljs index cc8f3f1..343b3c2 100644 --- a/src/frontend/tubo/components/items.cljs +++ b/src/frontend/tubo/items/views.cljs @@ -1,10 +1,10 @@ -(ns tubo.components.items +(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.components.layout :as layout] + [tubo.layout.views :as layout] [tubo.modals.views :as modals] [tubo.utils :as utils])) @@ -22,11 +22,10 @@ (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])} + :on-click #(rf/dispatch [:bg-player/show item true])} {: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 (if liked? "Remove favorite" "Favorite") :icon [:i.fa-solid.fa-heart (when (and liked? service-id) @@ -46,7 +45,7 @@ :on-click #(rf/dispatch [:bookmark/remove item])}) {: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}}])}] 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/components/layout.cljs b/src/frontend/tubo/layout/views.cljs index 48d4259..90dc3ef 100644 --- a/src/frontend/tubo/components/layout.cljs +++ b/src/frontend/tubo/layout/views.cljs @@ -1,4 +1,4 @@ -(ns tubo.components.layout +(ns tubo.layout.views (:require [clojure.string :as str] [re-frame.core :as rf] @@ -234,5 +234,5 @@ (when parse-error [:span (:status-text parse-error)]) [:div.flex.justify-center.gap-x-3 - [primary-button "Go Back" #(rf/dispatch [:history-go -1])] + [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] @@ -59,11 +62,6 @@ (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 _] (:page-scroll 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]]]])) |