From b5404ac06a3a09d83bef66552083254fdff12196 Mon Sep 17 00:00:00 2001 From: Miguel Ángel Moreno Date: Mon, 26 Dec 2022 22:02:33 +0100 Subject: feat(frontend): Modularize components and add pagination --- src/frontend/tau/components/items.cljs | 74 ++++++++++ src/frontend/tau/components/loading.cljs | 16 +++ src/frontend/tau/components/navigation.cljs | 13 ++ src/frontend/tau/core.cljs | 7 +- src/frontend/tau/events.cljs | 208 +++++++++++++++++++++++----- src/frontend/tau/routes.cljs | 31 ++++- src/frontend/tau/subs.cljs | 35 +++++ src/frontend/tau/views.cljs | 74 ++++++---- src/frontend/tau/views/channel.cljs | 41 ++++++ src/frontend/tau/views/home.cljs | 2 +- src/frontend/tau/views/kiosk.cljs | 9 ++ src/frontend/tau/views/playlist.cljs | 5 + src/frontend/tau/views/search.cljs | 64 +++++---- src/frontend/tau/views/stream.cljs | 98 ++++++++++--- 14 files changed, 555 insertions(+), 122 deletions(-) create mode 100644 src/frontend/tau/components/items.cljs create mode 100644 src/frontend/tau/components/loading.cljs create mode 100644 src/frontend/tau/components/navigation.cljs create mode 100644 src/frontend/tau/views/channel.cljs create mode 100644 src/frontend/tau/views/kiosk.cljs create mode 100644 src/frontend/tau/views/playlist.cljs diff --git a/src/frontend/tau/components/items.cljs b/src/frontend/tau/components/items.cljs new file mode 100644 index 0000000..5d26749 --- /dev/null +++ b/src/frontend/tau/components/items.cljs @@ -0,0 +1,74 @@ +(ns tau.components.items + (:require + [reitit.frontend.easy :as rfe])) + +(defn stream-item + [id {:keys [url name thumbnail-url upload-author upload-url + upload-avatar upload-date short-description + duration view-count uploaded verified?]}] + [:div.w-56.h-66.my-2 {:key id} + [:div.px-5.py-2.m-2.flex.flex-col.max-w-full.min-h-full.max-h-full + [:a.overflow-hidden {:href (rfe/href :tau.routes/stream nil {:url url}) :title name} + [:div.flex.py-3.box-border.h-28 + [:div.relative.min-w-full + [:img.rounded.object-cover.max-h-full.min-w-full {:src thumbnail-url}] + [:div.rounded.p-2.absolute {:style {:bottom 5 :right 5 :background "rgba(0,0,0,.7)"}} + [:p {:style {:fontSize "14px"}} + (let [duration (js/Date. (* duration 1000)) + slice (if (> (.getHours duration) 1) + #(.slice % 11 19) + #(.slice % 14 19))] + (-> duration (.toISOString) slice))]]]] + [:div.my-2 + [:h1.line-clamp-2.my-1 name]] + [:a {:href (rfe/href :tau.routes/channel nil {:url upload-url}) :title upload-author} + [:div.flex.items-center.my-2 + [:h1.line-clamp-1.text-gray-300.font-bold.pr-2 upload-author] + (when verified? + [:i.fa-solid.fa-circle-check])]] + [:div.flex.my-1.justify-between + [:p (if (-> upload-date js/Date.parse js/isNaN) + upload-date + (-> upload-date + js/Date.parse + js/Date. + .toDateString))] + [:div.flex.items-center.h-full.pl-2 + [:i.fa-solid.fa-eye.text-xs] + [:p.pl-1.5 (.toLocaleString view-count)]]]]]]) + +(defn channel-item + [id {:keys [url name thumbnail-url description subscriber-count stream-count verified?]}] + [:div.w-56.h-64.my-2 {:key id} + [:div.px-5.py-2.m-2.flex.flex-col.max-w-full.min-h-full.max-h-full + [:a.overflow-hidden {:href (rfe/href :tau.routes/channel nil {:url url}) :title name} + [:div.flex.min-w-full.py-3.box-border.h-28 + [:div.min-w-full + [:img.rounded.object-cover.max-h-full.min-w-full {:src thumbnail-url}]]] + [:div.overflow-hidden + [:div.flex.items-center.my-2 + [:h1.line-clamp-1.text-gray-300.font-bold.pr-2 name] + (when verified? + [:i.fa-solid.fa-circle-check])] + [:div.flex.items-center + [:i.fa-solid.fa-users.text-xs] + [:p.mx-2 subscriber-count]] + [:div.flex.items-center + [:i.fa-solid.fa-video.text-xs] + [:p.mx-2 stream-count]]]]]]) + +(defn playlist-item + [id {:keys [url name thumbnail-url upload-author stream-count]}] + [:div.w-56.h-64.my-2 {:key id} + [:div.px-5.py-2.m-2.flex.flex-col.max-w-full.min-h-full.max-h-full + [:a.overflow-hidden {:href (rfe/href :tau.routes/playlist nil {:url url}) :title name} + [:div.flex.min-w-full.py-3.box-border.h-28 + [:div.min-w-full + [:img.rounded.object-cover.max-h-full.min-w-full {:src thumbnail-url}]]] + [:div.overflow-hidden + [:h1.line-clamp-2 name] + [:h1.text-gray-300.font-bold upload-author] + [:p (condp >= stream-count + 0 "No streams" + 1 (str stream-count " stream") + (str stream-count " streams"))]]]]]) diff --git a/src/frontend/tau/components/loading.cljs b/src/frontend/tau/components/loading.cljs new file mode 100644 index 0000000..66954a1 --- /dev/null +++ b/src/frontend/tau/components/loading.cljs @@ -0,0 +1,16 @@ +(ns tau.components.loading + (:require + [re-frame.core :as rf])) + +(defn page-loading-icon + [service-color] + [:div.w-full.flex.justify-center.items-center.flex-auto + [:i.fas.fa-circle-notch.fa-spin.text-8xl + {:style {:color service-color}}]]) + +(defn pagination-loading-icon + [service-color loading?] + [:div.w-full.flex.items-center.justify-center.py-4 + {:class (when-not loading? "invisible")} + [:i.fas.fa-circle-notch.fa-spin.text-2xl + {:style {:color service-color}}]]) diff --git a/src/frontend/tau/components/navigation.cljs b/src/frontend/tau/components/navigation.cljs new file mode 100644 index 0000000..a0d25e2 --- /dev/null +++ b/src/frontend/tau/components/navigation.cljs @@ -0,0 +1,13 @@ +(ns tau.components.navigation + (:require + [re-frame.core :as rf] + [tau.events :as events])) + +(defn back-button [] + (let [service-color @(rf/subscribe [:service-color])] + [:div.flex {:class "w-4/5"} + [:button.p-2 + {:on-click #(rf/dispatch [::events/history-back])} + [:i.fa-solid.fa-chevron-left + {:style {:color service-color}}] + [:span " Back"]]])) diff --git a/src/frontend/tau/core.cljs b/src/frontend/tau/core.cljs index 3cad0ba..ab538f3 100644 --- a/src/frontend/tau/core.cljs +++ b/src/frontend/tau/core.cljs @@ -1,9 +1,8 @@ (ns tau.core (:require - [day8.re-frame.http-fx] [reagent.dom :as rdom] [re-frame.core :as rf] - [tau.events] + [tau.events :as events] [tau.routes :as routes] [tau.subs] [tau.views :as views])) @@ -11,12 +10,12 @@ (defn ^:dev/after-load mount-root [] (rf/clear-subscription-cache!) + (routes/start-routes!) (rdom/render [views/app] (.querySelector js/document "#app"))) (defn ^:export init [] - (routes/start-routes!) - (rf/dispatch-sync [:initialize-db]) + (rf/dispatch-sync [::events/initialize-db]) (mount-root)) diff --git a/src/frontend/tau/events.cljs b/src/frontend/tau/events.cljs index dec087b..f52ebd2 100644 --- a/src/frontend/tau/events.cljs +++ b/src/frontend/tau/events.cljs @@ -1,81 +1,227 @@ (ns tau.events (:require + [day8.re-frame.http-fx] [re-frame.core :as rf] + [reitit.frontend.easy :as rfe] [tau.api :as api])) (rf/reg-event-db - :initialize-db + ::initialize-db (fn [_ _] {:global-search "" :service-id 0 + :service-color "#cc0000" :stream {} :search-results [] :services [] - :current-match nil})) + :current-match nil + :page-scroll 0})) -(rf/reg-event-db - :navigated - (fn [db [_ new-match]] - (assoc db :current-match new-match))) +(rf/reg-fx + ::scroll-to-top + (fn [_] + (.scrollTo js/window #js {"top" 0 "behavior" "smooth"}))) + +(rf/reg-fx + ::history-back! + (fn [_] + (.back js/window.history))) + +(rf/reg-event-fx + ::history-back + (fn [_ _] + {::history-back! nil})) + +(rf/reg-event-fx + ::navigated + (fn [{:keys [db]} [_ new-match]] + {::scroll-to-top nil + :db (assoc db :current-match new-match) + :fx [[:dispatch [::reset-page-scroll]]]})) + +(rf/reg-event-fx + ::navigate + (fn [_ [_ route]] + {::navigate! route})) + +(rf/reg-fx + ::navigate! + (fn [{:keys [name params query]}] + (rfe/push-state name params query))) (rf/reg-event-db - :bad-response + ::bad-response (fn [db [_ res]] (assoc db :http-response (get-in res [:response :error])))) (rf/reg-event-db - :change-global-search + ::change-global-search (fn [db [_ res]] (assoc db :global-search res))) (rf/reg-event-db - :change-service-id + ::change-service-color + (fn [db [_ id]] + (assoc db :service-color + (case id + 0 "#cc0000" + 1 "#ff7700" + 2 "#333333" + 3 "#F2690D" + 4 "#629aa9")))) + +(rf/reg-event-fx + ::change-service-id + (fn [{:keys [db]} [_ id]] + {:db (assoc db :service-id id) + :fx [[:dispatch [::change-service-color id]]]})) + +(rf/reg-event-db + ::load-paginated-channel-results (fn [db [_ res]] - (assoc db :service-id res))) + (-> db + (update-in [:channel :related-streams] #(apply conj %1 %2) + (:related-streams (js->clj res :keywordize-keys true))) + (assoc-in [:channel :next-page] + (:next-page (js->clj res :keywordize-keys true))) + (assoc :show-pagination-loading false)))) (rf/reg-event-fx - :switch-to-global-player + ::scroll-channel-pagination + (fn [{:keys [db]} [_ uri next-page-url]] + (assoc + (api/get-request + (str "/api/channels/" (js/encodeURIComponent uri) ) + [::load-paginated-channel-results] [::bad-response] + {:nextPage (js/encodeURIComponent next-page-url)}) + :db (assoc db :show-pagination-loading true)))) + +(rf/reg-event-db + ::load-paginated-search-results + (fn [db [_ res]] + (-> db + (update-in [:search-results :items] #(apply conj %1 %2) + (:items (js->clj res :keywordize-keys true))) + (assoc-in [:search-results :next-page] + (:next-page (js->clj res :keywordize-keys true))) + (assoc :show-pagination-loading false)))) + +(rf/reg-event-db + ::reset-page-scroll + (fn [db _] + (assoc db :page-scroll 0))) + +(rf/reg-event-db + ::page-scroll + (fn [db _] + (assoc db :page-scroll (+ (.-scrollY js/window) (.-innerHeight js/window))))) + +(rf/reg-event-fx + ::scroll-search-pagination + (fn [{:keys [db]} [_ query id next-page-url]] + (assoc + (api/get-request + (str "/api/services/" id "/search") + [::load-paginated-search-results] [::bad-response] + {:q query + :nextPage (js/encodeURIComponent next-page-url)}) + :db (assoc db :show-pagination-loading true)))) + +(rf/reg-event-fx + ::switch-to-global-player (fn [{:keys [db]} [_ res]] {:db (assoc db :show-global-player true) - :dispatch [:change-global-search res]})) + :fx [[:dispatch [::change-global-search res]]]})) (rf/reg-event-db - :load-services + ::load-services (fn [db [_ res]] - (assoc db :services (js->clj res :keywordize-keys true) - :show-loading false))) + (assoc db :services (js->clj res :keywordize-keys true)))) (rf/reg-event-fx - :get-services + ::get-services (fn [{:keys [db]} _] + (api/get-request "/api/services" [::load-services] [::bad-response]))) + +(rf/reg-event-db + ::load-kiosks + (fn [db [_ res]] + (assoc db :kiosks (js->clj res :keywordize-keys true)))) + +(rf/reg-event-fx + ::get-kiosks + (fn [{:keys [db]} [_ id]] + (api/get-request (str "/api/services/" id "/kiosks") [::load-kiosks] [::bad-response]))) + +(rf/reg-event-db + ::load-kiosk + (fn [db [_ res]] + (assoc db :kiosk (js->clj res :keywordize-keys true) + :show-page-loading false))) + +(rf/reg-event-fx + ::get-kiosk + (fn [{:keys [db]} [_ {:keys [service-id kiosk-id]}]] (assoc - (api/get-request "/api/services" [:load-services] [:bad-response]) - :db (assoc db :show-loading true)))) + (api/get-request (str "/api/services/" service-id "/kiosks/" + (js/encodeURIComponent kiosk-id)) + [::load-kiosk] [::bad-response]) + :db (assoc db :show-page-loading true)))) (rf/reg-event-db - :load-stream + ::load-stream (fn [db [_ res]] (assoc db :stream (js->clj res :keywordize-keys true) - :show-loading false))) + :show-page-loading false))) + +(rf/reg-event-fx + ::get-stream + (fn [{:keys [db]} [_ uri]] + (assoc + (api/get-request (str "/api/streams/" (js/encodeURIComponent uri)) + [::load-stream] [::bad-response]) + :db (assoc db :show-page-loading true)))) + +(rf/reg-event-db + ::load-channel + (fn [db [_ res]] + (assoc db :channel (js->clj res :keywordize-keys true) + :show-page-loading false))) + +(rf/reg-event-fx + ::get-channel + (fn [{:keys [db]} [_ uri]] + (assoc + (api/get-request + (str "/api/channels/" (js/encodeURIComponent uri)) + [::load-channel] [::bad-response]) + :db (assoc db :show-page-loading true)))) + +(rf/reg-event-db + ::load-playlist + (fn [db [_ res]] + (assoc db :playlist (js->clj res :keywordize-keys true) + :show-page-loading false))) (rf/reg-event-fx - :get-stream + ::get-playlist (fn [{:keys [db]} [_ uri]] (assoc - (api/get-request "/api/stream" [:load-stream] [:bad-response] {:url uri}) - :db (assoc db :show-loading true)))) + (api/get-request (str "/api/playlists/" (js/encodeURIComponent uri)) + [::load-playlist] [::bad-response]) + :db (assoc db :show-page-loading true)))) (rf/reg-event-db - :load-search-results + ::load-search-results (fn [db [_ res]] (assoc db :search-results (js->clj res :keywordize-keys true) - :show-loading false))) + :show-page-loading false))) (rf/reg-event-fx - :get-search-results - (fn [{:keys [db]} [_ {:keys [id query]}]] + ::get-search-results + (fn [{:keys [db]} [_ {:keys [service-id query]}]] (assoc - (api/get-request "/api/search" - [:load-search-results] [:bad-response] - {:serviceId id :q query}) - :db (assoc db :show-loading true)))) + (api/get-request (str "/api/services/" service-id "/search") + [::load-search-results] [::bad-response] + {:q query}) + :db (assoc db :show-page-loading true)))) diff --git a/src/frontend/tau/routes.cljs b/src/frontend/tau/routes.cljs index 1742aaa..af309c9 100644 --- a/src/frontend/tau/routes.cljs +++ b/src/frontend/tau/routes.cljs @@ -4,7 +4,11 @@ [reitit.frontend.easy :as rfe] [reitit.frontend.controllers :as rfc] [re-frame.core :as rf] + [tau.events :as events] + [tau.views.channel :as channel] [tau.views.home :as home] + [tau.views.kiosk :as kiosk] + [tau.views.playlist :as playlist] [tau.views.search :as search] [tau.views.stream :as stream])) @@ -16,14 +20,33 @@ :name ::search :controllers [{:parameters {:query [:q :serviceId]} :start (fn [parameters] - (rf/dispatch [:get-search-results - {:id (-> parameters :query :serviceId) + (rf/dispatch [::events/change-service-id + (js/parseInt (-> parameters :query :serviceId))]) + (rf/dispatch [::events/get-search-results + {:service-id (-> parameters :query :serviceId) :query (-> parameters :query :q)}]))}]}] ["/stream" {:view stream/stream :name ::stream :controllers [{:parameters {:query [:url]} :start (fn [parameters] - (rf/dispatch [:get-stream (-> parameters :query :url)]))}]}]])) + (rf/dispatch [::events/get-stream (-> parameters :query :url)]))}]}] + ["/channel" {:view channel/channel + :name ::channel + :controllers [{:parameters {:query [:url]} + :start (fn [parameters] + (rf/dispatch [::events/get-channel (-> parameters :query :url)]))}]}] + ["/playlist" {:view playlist/playlist + :name ::playlist + :controllers [{:parameters {:query [:url]} + :start (fn [parameters] + (rf/dispatch [::events/get-playlist (-> parameters :query :url)]))}]}] + ["/kiosk" {:view kiosk/kiosk + :name ::kiosk + :controllers [{:parameters {:query [:kioskId :serviceId]} + :start (fn [parameters] + (rf/dispatch [::events/get-kiosk + {:service-id (-> parameters :query :serviceId) + :kiosk-id (-> parameters :query :kioskId)}]))}]}]])) (defn on-navigate [new-match] @@ -31,7 +54,7 @@ (when new-match (let [controllers (rfc/apply-controllers (:controllers @old-match) new-match) match (assoc new-match :controllers controllers)] - (rf/dispatch [:navigated match]))))) + (rf/dispatch [::events/navigated match]))))) (defn start-routes! [] diff --git a/src/frontend/tau/subs.cljs b/src/frontend/tau/subs.cljs index 53d6ff9..35c6fad 100644 --- a/src/frontend/tau/subs.cljs +++ b/src/frontend/tau/subs.cljs @@ -12,6 +12,11 @@ (fn [db _] (:stream db))) +(rf/reg-sub + :channel + (fn [db _] + (:channel db))) + (rf/reg-sub :global-search (fn [db _] @@ -22,16 +27,36 @@ (fn [db _] (:service-id db))) +(rf/reg-sub + :service-color + (fn [db _] + (:service-color db))) + (rf/reg-sub :services (fn [db _] (:services db))) +(rf/reg-sub + :kiosks + (fn [db _] + (:kiosks db))) + +(rf/reg-sub + :kiosk + (fn [db _] + (:kiosk db))) + (rf/reg-sub :current-match (fn [db _] (:current-match db))) +(rf/reg-sub + :page-scroll + (fn [db _] + (:page-scroll db))) + (rf/reg-sub :global-stream (fn [db _] @@ -41,3 +66,13 @@ :show-global-player (fn [db _] (:show-global-player db))) + +(rf/reg-sub + :show-page-loading + (fn [db _] + (:show-page-loading db))) + +(rf/reg-sub + :show-pagination-loading + (fn [db _] + (:show-pagination-loading db))) diff --git a/src/frontend/tau/views.cljs b/src/frontend/tau/views.cljs index 139bb83..81aaa40 100644 --- a/src/frontend/tau/views.cljs +++ b/src/frontend/tau/views.cljs @@ -1,53 +1,73 @@ (ns tau.views (:require - [tau.views.player :as player] [reitit.frontend.easy :as rfe] - [re-frame.core :as rf])) + [re-frame.core :as rf] + [reagent.ratom :as ratom] + [tau.components.navigation :as navigation] + [tau.events :as events] + [tau.routes :as routes] + [tau.views.player :as player])) + +(defonce scroll-hook (.addEventListener js/window "scroll" #(rf/dispatch [::events/page-scroll]))) +(defonce services (rf/dispatch [::events/get-services])) (defn footer [] - [:footer.bg-slate-900.text-gray-300.p-5.text-center - [:div + [:footer + [:div.bg-black.text-gray-300.p-5.text-center.w-full [:p (str "Tau " (.getFullYear (js/Date.)))]]]) (defn search-bar - [] + [{{:keys [serviceId]} :query-params}] (let [global-search @(rf/subscribe [:global-search]) services @(rf/subscribe [:services]) - service-id @(rf/subscribe [:service-id])] + service-id @(rf/subscribe [:service-id]) + id (js/parseInt (or serviceId service-id)) ] [:div.flex [:form {:on-submit (fn [e] (.preventDefault e) - (rfe/push-state :tau.routes/search {} {:q global-search :serviceId service-id}))} - [:input.bg-slate-900.border.border-solid.border-black.rounded.py-2.px-1.mx-2.text-gray-500 + (rf/dispatch [::events/navigate + {:name ::routes/search + :params {} + :query {:q global-search :serviceId service-id}}]))} + [:input.bg-neutral-900.border.border-solid.border-black.rounded.py-2.px-1.mx-2.text-gray-500 {:type "text" :value global-search - :on-change #(rf/dispatch [:change-global-search (.. % -target -value)]) + :on-change #(rf/dispatch [::events/change-global-search (.. % -target -value)]) :placeholder "Search for something"}] [:select.mx-2.bg-gray-50.border.border-gray-900.text-gray-900 - {:on-change #(rf/dispatch [:change-service-id (js/parseInt (.. % -target -value))])} - (for [service services] - [:option {:value (:id service) :key (:id service) :selected (= (:id service) service-id)} - (-> service :info :name)])] - [:button..bg-slate-900.border.border-black.rounded.border-solid.text-gray-500.p-2.mx-2 - {:type "submit"} "Search"]]])) + {:on-change #(rf/dispatch [::events/change-service-id (js/parseInt (.. % -target -value))])} + (when services + (for [service services] + [:option {:value (:id service) :key (:id service) :selected (= id (:id service))} + (-> service :info :name)]))] + [:button.text-white.mx-2 + {:type "submit"} + [:i.fas.fa-search]]]])) -(defn navbar [] - [:nav.bg-slate-800.flex.p-2.content-center.sticky.top-0.z-50 - [:div.px-5.text-white.p-2 - [:a {:href (rfe/href :tau.routes/home) :dangerouslySetInnerHTML {:__html "τ"}}]] - [:ul.flex.content-center.text-white.p-2 - [:li.px-5 [:a {:href (rfe/href :tau.routes/home)} "Home"]] - [:li.px-5 [:a {:href (rfe/href :tau.routes/search)} "Search"]]] - [search-bar]]) +(defn navbar + [match] + (let [service-id @(rf/subscribe [:service-id]) + service-color @(rf/subscribe [:service-color]) + {:keys [default-kiosk available-kiosks]} @(rf/subscribe [:kiosks])] + (rf/dispatch [::events/get-kiosks service-id]) + [:nav.flex.p-2.content-center.sticky.top-0.z-50 + {:style {:background service-color}} + [:div.px-5.text-white.p-2.font-bold + [:a {:href (rfe/href ::routes/home) :dangerouslySetInnerHTML {:__html "τ"}}]] + [:ul.flex.content-center.p-2.text-white + (for [kiosk available-kiosks] + [:li.px-5 [:a {:href (rfe/href ::routes/kiosk nil {:serviceId service-id + :kioskId kiosk})} + kiosk]])] + [search-bar match]])) (defn app [] - (rf/dispatch [:get-services]) (let [current-match @(rf/subscribe [:current-match])] - [:div.font-sans.bg-slate-700.min-h-screen.flex.flex-col.h-full - [navbar] - [:div.flex.flex-col.justify-between {:class "min-h-[calc(100vh-58px)]"} + [:div.font-sans.min-h-screen.flex.flex-col.h-full {:style {:background "rgba(23, 23, 23)"}} + [navbar current-match] + [:div.flex.flex-col.justify-between.relative {:class "min-h-[calc(100vh-58px)]"} (when-let [view (-> current-match :data :view)] [view current-match]) [player/global-player] diff --git a/src/frontend/tau/views/channel.cljs b/src/frontend/tau/views/channel.cljs new file mode 100644 index 0000000..abdbb54 --- /dev/null +++ b/src/frontend/tau/views/channel.cljs @@ -0,0 +1,41 @@ +(ns tau.views.channel + (:require + [re-frame.core :as rf] + [tau.components.items :as items] + [tau.components.loading :as loading] + [tau.components.navigation :as navigation] + [tau.events :as events])) + +(defn channel + [{{:keys [url]} :query-params}] + (let [{:keys [banner avatar name description subscriber-count + related-streams next-page]} @(rf/subscribe [:channel]) + next-page-url (:url next-page) + service-color @(rf/subscribe [:service-color]) + page-scroll @(rf/subscribe [:page-scroll]) + page-loading? @(rf/subscribe [:show-page-loading]) + pagination-loading? @(rf/subscribe [:show-pagination-loading]) + scrolled-to-bottom? (= page-scroll (.-scrollHeight js/document.body))] + (when scrolled-to-bottom? + (rf/dispatch [::events/scroll-channel-pagination url next-page-url])) + [:div.flex.flex-col.items-center.px-5.py-2.text-white.flex-auto + (if page-loading? + [loading/page-loading-icon service-color] + [:div {:class "w-4/5"} + [navigation/back-button] + [:div [:img {:src banner}]] + [:div.flex.items-center.my-4.mx-2 + [:div + [:img.rounded-full {:src avatar}]] + [:div.m-4 + [:h1.text-xl name] + [:div.flex.my-2.items-center + [:i.fa-solid.fa-users] + [:span.mx-2 subscriber-count]]]] + [:div.my-2 + [:p description]] + [:div.flex.justify-center.align-center.flex-wrap.my-2 + (for [[i result] (map-indexed vector related-streams)] + [items/stream-item i result])] + (when-not (empty? next-page-url) + [loading/pagination-loading-icon service-color pagination-loading?])])])) diff --git a/src/frontend/tau/views/home.cljs b/src/frontend/tau/views/home.cljs index 0d42d7d..00d2e1e 100644 --- a/src/frontend/tau/views/home.cljs +++ b/src/frontend/tau/views/home.cljs @@ -1,7 +1,7 @@ (ns tau.views.home) (defn home-page - [] + [match] [:div.flex.justify-center.content-center.flex-col.text-center.text-white.text-lg.flex-auto [:p.text-5xl.p-5 "Welcome to Tau"] [:p.text-2xl "A web front-end for Newpipe"]]) diff --git a/src/frontend/tau/views/kiosk.cljs b/src/frontend/tau/views/kiosk.cljs new file mode 100644 index 0000000..70caacf --- /dev/null +++ b/src/frontend/tau/views/kiosk.cljs @@ -0,0 +1,9 @@ +(ns tau.views.kiosk + (:require + [re-frame.core :as rf])) + +(defn kiosk + [match] + (let [{:keys [id url related-streams]} @(rf/subscribe [:kiosk])] + [:div + [:h1 id]])) diff --git a/src/frontend/tau/views/playlist.cljs b/src/frontend/tau/views/playlist.cljs new file mode 100644 index 0000000..b82cc24 --- /dev/null +++ b/src/frontend/tau/views/playlist.cljs @@ -0,0 +1,5 @@ +(ns tau.views.playlist) + +(defn playlist + [match] + [:div]) diff --git a/src/frontend/tau/views/search.cljs b/src/frontend/tau/views/search.cljs index 64bddf3..69f1353 100644 --- a/src/frontend/tau/views/search.cljs +++ b/src/frontend/tau/views/search.cljs @@ -1,38 +1,36 @@ (ns tau.views.search (:require [re-frame.core :as rf] - [reitit.frontend.easy :as rfe])) - -(defn search-result - [title author url thumbnail id] - [:div.w-56.h-64.my-2 {:key id} - [:div.p-5.border.rounded.border-slate-900.m-2.bg-slate-600.flex.flex-col.max-w-full.min-h-full.max-h-full - [:a.overflow-hidden {:href (rfe/href :tau.routes/stream {} {:url url}) :title title} - [:div.flex.justify-center.min-w-full.py-3.box-border - [:div.h-28.min-w-full.flex.justify-center - [:img.rounded.object-cover.max-h-full {:src thumbnail}]]] - [:div.overflow-hidden - [:h1.text-gray-300.font-bold author] - [:h1 title]]]]]) + [reitit.frontend.easy :as rfe] + [tau.components.items :as items] + [tau.components.loading :as loading] + [tau.events :as events])) (defn search - [m] - (let [search-results (rf/subscribe [:search-results]) - services (rf/subscribe [:services]) - service-id (rf/subscribe [:service-id])] - [:div.text-gray-300.text-center.py-5.relative - [:h2 (str "Showing search results for: \"" (-> m :query-params :q) "\"")] - [:h1 (str "Number of search results: " (count (:items @search-results)))] - ;; TODO: Create loadable component that wraps other components that need to fetch from API - ;; or use a :loading key to show a spinner component instead - (if (empty? @search-results) - [:p "Loading"] - [:div.flex.justify-center.align-center.flex-wrap - (for [[i result] (map-indexed vector (:items @search-results))] - ;; TODO: Add a component per result type - [search-result - (:name result) - (:upload-author result) - (:url result) - (:thumbnail-url result) - i])])])) + [{{:keys [q serviceId]} :query-params}] + (let [{:keys [items next-page] :as search-results} @(rf/subscribe [:search-results]) + next-page-url (:url next-page) + services @(rf/subscribe [:services]) + service-id @(rf/subscribe [:service-id]) + service-color @(rf/subscribe [:service-color]) + page-scroll @(rf/subscribe [:page-scroll]) + page-loading? @(rf/subscribe [:show-page-loading]) + pagination-loading? @(rf/subscribe [:show-pagination-loading]) + scrolled-to-bottom? (= page-scroll (.-scrollHeight js/document.body))] + (when scrolled-to-bottom? + (rf/dispatch [::events/scroll-search-pagination q serviceId next-page-url])) + [:div.flex.flex-col.text-gray-300.h-box-border.flex-auto + [:div.flex.flex-col.items-center.w-full.pt-4.flex-initial + [:h2 (str "Showing search results for: \"" q "\"")] + [:h1 (str "Number of search results: " (count items))]] + (if page-loading? + [loading/page-loading-icon service-color] + [:div.flex.flex-col + [:div.flex.justify-center.align-center.flex-wrap.flex-auto + (for [[i item] (map-indexed vector items)] + (cond + (:duration item) [items/stream-item i item] + (:subscriber-count item) [items/channel-item i item] + (:stream-count item) [items/playlist-item i item])) + (when-not (empty? next-page-url) + [loading/pagination-loading-icon service-color pagination-loading?])]])])) diff --git a/src/frontend/tau/views/stream.cljs b/src/frontend/tau/views/stream.cljs index ccc53eb..47039e6 100644 --- a/src/frontend/tau/views/stream.cljs +++ b/src/frontend/tau/views/stream.cljs @@ -1,27 +1,81 @@ (ns tau.views.stream (:require - [re-frame.core :as rf])) + [re-frame.core :as rf] + [reitit.frontend.easy :as rfe] + [tau.events :as events] + [tau.components.items :as items] + [tau.components.loading :as loading] + [tau.components.navigation :as navigation])) (defn stream - [m] - (let [current-stream @(rf/subscribe [:stream]) - stream-type (-> (if (empty? (:video-streams current-stream)) - (:audio-streams current-stream) - (:video-streams current-stream)) + [match] + (let [{:keys [name url video-streams audio-streams view-count + subscriber-count like-count dislike-count + description upload-avatar upload-author + upload-url upload-date related-streams + thumbnail-url] :as stream} @(rf/subscribe [:stream]) + stream-type (-> (if (empty? video-streams) audio-streams video-streams) last - :content)] - [:div.flex.flex-col.justify-center.p-5.items-center - [:div.flex.justify-center.py-2 - [:div.flex.justify-center {:class "w-4/5"} - [:video.min-w-full.h-auto {:src stream-type :controls true}]]] - [:div.flex.text-white - [:button.border.rounded.border-slate-900.p-2.bg-slate-800 - {:on-click #(rf/dispatch [:switch-to-global-player current-stream])} - "Add to global stream"] - [:a {:href (:url current-stream)} - "Open original source"]] - [:div.flex.flex-col.items-center.py-2 {:class "w-4/5"} - [:div.min-w-full.py-2 - [:h1.text-xl.font-extrabold (:name current-stream)]] - [:div.min-w-full.py-2 - [:p (:description current-stream)]]]])) + :content) + page-loading? @(rf/subscribe [:show-page-loading]) + service-color @(rf/subscribe [:service-color])] + [:div.flex.flex-col.p-5.items-center.justify-center.text-white.flex-auto + (if page-loading? + [loading/page-loading-icon service-color] + [:div {:class "w-4/5"} + [navigation/back-button] + [:div.flex.justify-center.relative.my-2 + {:style {:background (str "center / cover no-repeat url('" thumbnail-url"')") + :height "450px"}} + [:video.min-h-full.absolute.bottom-0.object-cover {:src stream-type :controls true :width "100%"}]] + [:div.flex.text-white.flex.w-full.my-1 + [:button.border.rounded.border-black.p-2.bg-stone-800 + {:on-click #(rf/dispatch [::events/switch-to-global-player stream])} + [:i.fa-solid.fa-headphones]] + [:a {:href (:url stream)} + [:button.border.rounded.border-black.p-2.bg-stone-800.mx-2 + [:i.fa-solid.fa-external-link-alt]]]] + [:div.flex.flex-col.py-1 + [:div.min-w-full.py-3 + [:h1.text-xl.font-extrabold name]] + [:div.flex.justify-between.py-2 + [:div.flex.items-center.flex-auto + (when upload-avatar + [:div + [:img.rounded-full {:src upload-avatar :alt upload-author}]]) + [:div.mx-2 + [:a {:href (rfe/href :tau.routes/channel nil {:url upload-url})} upload-author] + (when subscriber-count + [:div.flex.my-2 + [:i.fa-solid.fa-users] + [:p.mx-2 (.toLocaleString subscriber-count)]])]] + [:div + (when view-count + [:p + [:i.fa-solid.fa-eye] + [:span.mx-2 (.toLocaleString view-count)]]) + [:div + (when like-count + [:p + [:i.fa-solid.fa-thumbs-up] + [:span.mx-2 like-count]]) + (when dislike-count + [:p + [:i.fa-solid.fa-thumbs-down] + [:span.mx-2 dislike-count]])] + (when upload-date + [:p (-> upload-date + js/Date.parse + js/Date. + .toDateString)])]] + [:div.min-w-full.py-3 + [:h1 name] + [:p description]] + [:div.py-3 + [:h1.text-lg.bold "Related Results"] + [:div.flex.justify-center.align-center.flex-wrap + (for [[i item] (map-indexed vector related-streams)] + (cond + (:duration item) [items/stream-item i item] + (:subscriber-count item) [items/channel-item i item] + (:stream-count item) [items/playlist-item i item]))]]]])])) -- cgit v1.2.3