From 8c46de38348c421c0c9f102d604fcfc18807c0bb Mon Sep 17 00:00:00 2001 From: Miguel Ángel Moreno Date: Thu, 29 Dec 2022 02:26:20 +0100 Subject: feat(frontend): Add further features and address quirks --- src/frontend/tau/components/comments.cljs | 59 +++++++++ src/frontend/tau/components/items.cljs | 128 ++++++++++--------- src/frontend/tau/components/loading.cljs | 14 +- src/frontend/tau/components/navigation.cljs | 15 +-- src/frontend/tau/events.cljs | 190 ++++++++++++++++++++++------ src/frontend/tau/routes.cljs | 39 +++--- src/frontend/tau/subs.cljs | 10 ++ src/frontend/tau/util.cljs | 16 +++ src/frontend/tau/views.cljs | 98 +++++++------- src/frontend/tau/views/channel.cljs | 33 +++-- src/frontend/tau/views/home.cljs | 7 - src/frontend/tau/views/kiosk.cljs | 36 +++++- src/frontend/tau/views/playlist.cljs | 49 ++++++- src/frontend/tau/views/search.cljs | 21 +-- src/frontend/tau/views/stream.cljs | 111 ++++++++++------ 15 files changed, 570 insertions(+), 256 deletions(-) create mode 100644 src/frontend/tau/components/comments.cljs create mode 100644 src/frontend/tau/util.cljs delete mode 100644 src/frontend/tau/views/home.cljs (limited to 'src') diff --git a/src/frontend/tau/components/comments.cljs b/src/frontend/tau/components/comments.cljs new file mode 100644 index 0000000..8f9300b --- /dev/null +++ b/src/frontend/tau/components/comments.cljs @@ -0,0 +1,59 @@ +(ns tau.components.comments + (:require + [re-frame.core :as rf] + [reitit.frontend.easy :as rfe] + [tau.components.loading :as loading] + [tau.events :as events] + ["timeago.js" :as timeago])) + +(defn comment-item + [{:keys [id text uploader-name uploader-avatar uploader-url + upload-date uploader-verified? like-count hearted-by-uploader? + pinned? replies key]} author-name author-avatar] + [:div.flex.my-4 + (when uploader-avatar + [:div.flex.items-center.py-3.box-border.h-12 + [:div.w-12 + [:a {:href (rfe/href :tau.routes/channel nil {:url uploader-url}) :title name} + [:img.rounded-full.object-cover.min-w-full.min-h-full {:src uploader-avatar}]]]]) + [:div.ml-2 + [:div.flex.items-center + (when pinned? + [:i.fa-solid.fa-thumbtack.mr-2]) + [:a {:href (rfe/href :tau.routes/channel nil {:url uploader-url}) :title name} + [:h1.text-gray-300.font-bold uploader-name]] + (when uploader-verified? + [:i.fa-solid.fa-circle-check.ml-2])] + [:div.my-2 + [:p text]] + [:div..flex.items-center.my-2 + [:div.mr-4 + [:p (if (-> upload-date js/Date.parse js/isNaN) + upload-date + (timeago/format upload-date))]] + (when like-count + [:div.flex.items-center.my-2 + [:i.fa-solid.fa-thumbs-up] + [:p.mx-1 like-count]]) + (when hearted-by-uploader? + [:div.relative.w-4.h-4.mx-2 + [:i.fa-solid.fa-heart.absolute.-bottom-1.-right-1.text-xs.text-red-500] + [:img.rounded-full.object-covermax-w-full.min-h-full + {:src author-avatar :title (str author-name " hearted this comment")}]])]]]) + +(defn comments + [{:keys [comments next-page disabled?]} author-name author-avatar url] + (let [pagination-loading? @(rf/subscribe [:show-pagination-loading]) + service-color @(rf/subscribe [:service-color])] + [:div.flex.flex-col + [:div + (for [[i comment] (map-indexed vector comments)] + [comment-item (assoc comment :key i) author-name author-avatar])] + (when (:url next-page) + (if pagination-loading? + (loading/comments-pagination-loading-icon service-color) + [:div.flex.items-center.justify-center + {:style {:cursor "pointer"} + :on-click #(rf/dispatch [::events/comments-pagination url (:url next-page)])} + [:i.fa-solid.fa-plus] + [:p.px-2 "Show more comments"]]))])) diff --git a/src/frontend/tau/components/items.cljs b/src/frontend/tau/components/items.cljs index 5d26749..0a8bad6 100644 --- a/src/frontend/tau/components/items.cljs +++ b/src/frontend/tau/components/items.cljs @@ -1,74 +1,80 @@ (ns tau.components.items (:require - [reitit.frontend.easy :as rfe])) + [reitit.frontend.easy :as rfe] + [tau.util :as util] + ["timeago.js" :as timeago])) (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} + [{:keys [url name thumbnail-url upload-author upload-url + upload-avatar upload-date short-description + duration view-count uploaded verified? key]}] + [:div.w-56.h-66.my-2 {:key key} [: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.py-2.box-border.h-28 + [:div.relative.min-w-full + [:a.absolute.min-w-full.min-h-full.z-50 {:href (rfe/href :tau.routes/stream nil {:url url}) :title name}] + [: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)" :zIndex "0"}} + [:p {:style {:fontSize "14px"}} + (if (= duration 0) + "LIVE" + (util/format-duration duration))]]]] + [:div.my-2 + [:a {:href (rfe/href :tau.routes/stream nil {:url url}) :title name} + [:h1.line-clamp-2.my-1 name]]] + (when-not (empty? upload-author) [:div.flex.items-center.my-2 - [:h1.line-clamp-1.text-gray-300.font-bold.pr-2 upload-author] + [:a {:href (rfe/href :tau.routes/channel nil {:url upload-url}) :title upload-author} + [: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)]]]]]]) + [:i.fa-solid.fa-circle-check])]) + [:div.flex.my-1.justify-between + [:p (if (-> upload-date js/Date.parse js/isNaN) + upload-date + (timeago/format upload-date))] + (when view-count + [:div.flex.items-center.h-full.pl-2 + [:i.fa-solid.fa-eye.text-xs] + [:p.pl-1.5 (util/format-quantity 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} + [{:keys [url name thumbnail-url description subscriber-count + stream-count verified? key]}] + [:div.w-56.h-64.my-2 {:key key} [: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]]]]]]) + [:div.flex.min-w-full.py-3.box-border.h-28 + [:div.relative.min-w-full + [:a.absolute.min-w-full.min-h-full {:href (rfe/href :tau.routes/channel nil {:url url}) :title name}] + [:img.rounded.object-cover.max-h-full.min-w-full {:src thumbnail-url}]]] + [:div.overflow-hidden + [:div.flex.items-center.py-2.box-border + [:a {:href (rfe/href :tau.routes/channel nil {:url url}) :title name} + [:h1.line-clamp-1.text-gray-300.font-bold.pr-2 name]] + (when verified? + [:i.fa-solid.fa-circle-check])] + (when subscriber-count + [:div.flex.items-center + [:i.fa-solid.fa-users.text-xs] + [:p.mx-2 subscriber-count]]) + (when stream-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} + [{:keys [url name thumbnail-url upload-author stream-count key]}] + [:div.w-56.h-64.my-2 {:key key} [: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"))]]]]]) + [:div.flex.min-w-full.py-3.box-border.h-28 + [:div.relative.min-w-full + [:a.absolute.min-w-full.min-h-full.z-50 {:href (rfe/href :tau.routes/playlist nil {:url url}) :title name}] + [:img.rounded.object-cover.max-h-full.min-w-full {:src thumbnail-url}]]] + [:div.overflow-hidden + [:div + [:a {:href (rfe/href :tau.routes/playlist nil {:url url}) :title name} + [:h1.line-clamp-2 name]]] + [:div.my-2 + [:h1.text-gray-300.font-bold upload-author]] + [:div.flex.items-center + [:i.fa-solid.fa-video.text-xs] + [:p.mx-2 stream-count]]]]]) diff --git a/src/frontend/tau/components/loading.cljs b/src/frontend/tau/components/loading.cljs index 66954a1..9b1fee8 100644 --- a/src/frontend/tau/components/loading.cljs +++ b/src/frontend/tau/components/loading.cljs @@ -1,16 +1,20 @@ -(ns tau.components.loading - (:require - [re-frame.core :as rf])) +(ns tau.components.loading) (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 + [:i.fas.fa-circle-notch.fa-spin.text-5xl {:style {:color service-color}}]]) -(defn pagination-loading-icon +(defn items-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}}]]) + +(defn comments-pagination-loading-icon + [service-color] + [:div.w-full.flex.justify-center.items-center.flex-auto + [:i.fas.fa-circle-notch.fa-spin + {:style {:color service-color}}]]) diff --git a/src/frontend/tau/components/navigation.cljs b/src/frontend/tau/components/navigation.cljs index a0d25e2..c8f0b5a 100644 --- a/src/frontend/tau/components/navigation.cljs +++ b/src/frontend/tau/components/navigation.cljs @@ -3,11 +3,10 @@ [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"]]])) +(defn back-button [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/events.cljs b/src/frontend/tau/events.cljs index f52ebd2..ed01467 100644 --- a/src/frontend/tau/events.cljs +++ b/src/frontend/tau/events.cljs @@ -32,12 +32,24 @@ (fn [_ _] {::history-back! nil})) +(rf/reg-event-db + ::page-scroll + (fn [db _] + (when (> (.-scrollY js/window) 0) + (assoc db :page-scroll (+ (.-scrollY js/window) (.-innerHeight js/window)))))) + +(rf/reg-event-db + ::reset-page-scroll + (fn [db _] + (assoc db :page-scroll 0))) + (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]]]})) + {:db (-> db + (assoc :current-match new-match) + (assoc :show-pagination-loading false)) + ::scroll-to-top nil})) (rf/reg-event-fx ::navigate @@ -61,9 +73,9 @@ (rf/reg-event-db ::change-service-color - (fn [db [_ id]] + (fn [db [_ service-id]] (assoc db :service-color - (case id + (case service-id 0 "#cc0000" 1 "#ff7700" 2 "#333333" @@ -72,9 +84,9 @@ (rf/reg-event-fx ::change-service-id - (fn [{:keys [db]} [_ id]] - {:db (assoc db :service-id id) - :fx [[:dispatch [::change-service-color id]]]})) + (fn [{:keys [db]} [_ service-id]] + {:db (assoc db :service-id service-id) + :fx [[:dispatch [::change-service-color service-id]]]})) (rf/reg-event-db ::load-paginated-channel-results @@ -87,14 +99,38 @@ (assoc :show-pagination-loading false)))) (rf/reg-event-fx - ::scroll-channel-pagination + ::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)))) + (if (empty? next-page-url) + {:db (assoc db :show-pagination-loading false)} + (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-playlist-results + (fn [db [_ res]] + (-> db + (update-in [:playlist :related-streams] #(apply conj %1 %2) + (:related-streams (js->clj res :keywordize-keys true))) + (assoc-in [:playlist :next-page] + (:next-page (js->clj res :keywordize-keys true))) + (assoc :show-pagination-loading false)))) + +(rf/reg-event-fx + ::playlist-pagination + (fn [{:keys [db]} [_ uri next-page-url]] + (if (empty? next-page-url) + {:db (assoc db :show-pagination-loading false)} + (assoc + (api/get-request + (str "/api/playlists/" (js/encodeURIComponent uri)) + [::load-paginated-playlist-results] [::bad-response] + {:nextPage (js/encodeURIComponent next-page-url)}) + :db (assoc db :show-pagination-loading true))))) (rf/reg-event-db ::load-paginated-search-results @@ -106,26 +142,18 @@ (: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 + ::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)))) + (if (empty? next-page-url) + {:db (assoc db :show-pagination-loading false)} + (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 @@ -143,6 +171,49 @@ (fn [{:keys [db]} _] (api/get-request "/api/services" [::load-services] [::bad-response]))) +(rf/reg-event-db + ::load-comments + (fn [db [_ res]] + (-> db + (assoc-in [:stream :comments-page] (js->clj res :keywordize-keys true)) + (assoc-in [:stream :show-comments-loading] false)))) + +(rf/reg-event-fx + ::get-comments + (fn [{:keys [db]} [_ url]] + (assoc + (api/get-request (str "/api/comments/" (js/encodeURIComponent url)) + [::load-comments] [::bad-response]) + :db (-> db + (assoc-in [:stream :show-comments-loading] true) + (assoc-in [:stream :show-comments] true))))) + +(rf/reg-event-db + ::toggle-comments + (fn [db [_ res]] + (assoc-in db [:stream :show-comments] (not (-> db :stream :show-comments))))) + +(rf/reg-event-db + ::load-paginated-comments + (fn [db [_ res]] + (-> db + (update-in [:stream :comments-page :comments] #(apply conj %1 %2) + (:comments (js->clj res :keywordize-keys true))) + (assoc-in [:stream :comments-page :next-page] + (:next-page (js->clj res :keywordize-keys true))) + (assoc :show-pagination-loading false)))) + +(rf/reg-event-fx + ::comments-pagination + (fn [{:keys [db]} [_ url next-page-url]] + (if (empty? next-page-url) + {:db (assoc db :show-pagination-loading false)} + (assoc + (api/get-request (str "/api/comments/" (js/encodeURIComponent url)) + [::load-paginated-comments] [::bad-response] + {:nextPage (js/encodeURIComponent next-page-url)}) + :db (assoc db :show-pagination-loading true))))) + (rf/reg-event-db ::load-kiosks (fn [db [_ res]] @@ -160,14 +231,54 @@ :show-page-loading false))) (rf/reg-event-fx - ::get-kiosk - (fn [{:keys [db]} [_ {:keys [service-id kiosk-id]}]] + ::get-default-kiosk + (fn [{:keys [db]} [_ service-id]] (assoc - (api/get-request (str "/api/services/" service-id "/kiosks/" - (js/encodeURIComponent kiosk-id)) + (api/get-request (str "/api/services/" service-id "/default-kiosk") [::load-kiosk] [::bad-response]) :db (assoc db :show-page-loading true)))) +(rf/reg-event-fx + ::get-kiosk + (fn [{:keys [db]} [_ service-id kiosk-id]] + (if kiosk-id + (assoc + (api/get-request (str "/api/services/" service-id "/kiosks/" + (js/encodeURIComponent kiosk-id)) + [::load-kiosk] [::bad-response]) + :db (assoc db :show-page-loading true)) + {:fx [[:dispatch [::get-default-kiosk service-id]]]}))) + +(rf/reg-event-fx + ::change-service + (fn [{:keys [db]} [_ service-id]] + {:fx [[:dispatch + [::navigate {:name :tau.routes/kiosk + :params {} + :query {:serviceId service-id}}]]]})) + +(rf/reg-event-db + ::load-paginated-kiosk-results + (fn [db [_ res]] + (-> db + (update-in [:kiosk :related-streams] #(apply conj %1 %2) + (:related-streams (js->clj res :keywordize-keys true))) + (assoc-in [:kiosk :next-page] + (:next-page (js->clj res :keywordize-keys true))) + (assoc :show-pagination-loading false)))) + +(rf/reg-event-fx + ::kiosk-pagination + (fn [{:keys [db]} [_ service-id kiosk-id next-page-url]] + (if (empty? next-page-url) + {:db (assoc db :show-pagination-loading false)} + (assoc + (api/get-request + (str "/api/services/" service-id "/kiosks/" (js/encodeURIComponent kiosk-id)) + [::load-paginated-kiosk-results] [::bad-response] + {:nextPage (js/encodeURIComponent next-page-url)}) + :db (assoc db :show-pagination-loading true))))) + (rf/reg-event-db ::load-stream (fn [db [_ res]] @@ -215,11 +326,12 @@ ::load-search-results (fn [db [_ res]] (assoc db :search-results (js->clj res :keywordize-keys true) - :show-page-loading false))) + :show-page-loading false + :global-search ""))) (rf/reg-event-fx ::get-search-results - (fn [{:keys [db]} [_ {:keys [service-id query]}]] + (fn [{:keys [db]} [_ service-id query]] (assoc (api/get-request (str "/api/services/" service-id "/search") [::load-search-results] [::bad-response] diff --git a/src/frontend/tau/routes.cljs b/src/frontend/tau/routes.cljs index af309c9..8cb2beb 100644 --- a/src/frontend/tau/routes.cljs +++ b/src/frontend/tau/routes.cljs @@ -6,7 +6,6 @@ [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] @@ -14,43 +13,45 @@ (def routes (ref/router - [["/" {:view home/home-page - :name ::home}] + [["/" {:view kiosk/kiosk + :name ::home + :controllers [{:start (fn [_] + (rf/dispatch [::events/change-service-id 0]) + (rf/dispatch [::events/get-default-kiosk 0]) + (rf/dispatch [::events/get-kiosks 0]))}]}] ["/search" {:view search/search :name ::search :controllers [{:parameters {:query [:q :serviceId]} - :start (fn [parameters] - (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)}]))}]}] + :start (fn [{{:keys [serviceId q]} :query}] + (rf/dispatch [::events/change-service-id (js/parseInt serviceId)]) + (rf/dispatch [::events/get-search-results serviceId q]))}]}] ["/stream" {:view stream/stream :name ::stream :controllers [{:parameters {:query [:url]} - :start (fn [parameters] - (rf/dispatch [::events/get-stream (-> parameters :query :url)]))}]}] + :start (fn [{{:keys [url]} :query}] + (rf/dispatch [::events/get-stream url]))}]}] ["/channel" {:view channel/channel :name ::channel :controllers [{:parameters {:query [:url]} - :start (fn [parameters] - (rf/dispatch [::events/get-channel (-> parameters :query :url)]))}]}] + :start (fn [{{:keys [url]} :query}] + (rf/dispatch [::events/get-channel url]))}]}] ["/playlist" {:view playlist/playlist :name ::playlist :controllers [{:parameters {:query [:url]} - :start (fn [parameters] - (rf/dispatch [::events/get-playlist (-> parameters :query :url)]))}]}] + :start (fn [{{:keys [url]} :query}] + (rf/dispatch [::events/get-playlist 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)}]))}]}]])) + :start (fn [{{:keys [serviceId kioskId]} :query}] + (rf/dispatch [::events/change-service-id (js/parseInt serviceId)]) + (rf/dispatch [::events/get-kiosk serviceId kioskId]) + (rf/dispatch [::events/get-kiosks serviceId]))}]}]])) (defn on-navigate [new-match] (let [old-match (rf/subscribe [:current-match])] + (rf/dispatch [::events/reset-page-scroll]) (when new-match (let [controllers (rfc/apply-controllers (:controllers @old-match) new-match) match (assoc new-match :controllers controllers)] diff --git a/src/frontend/tau/subs.cljs b/src/frontend/tau/subs.cljs index 35c6fad..8fd6452 100644 --- a/src/frontend/tau/subs.cljs +++ b/src/frontend/tau/subs.cljs @@ -2,6 +2,11 @@ (:require [re-frame.core :as rf])) +(rf/reg-sub + :http-response + (fn [db _] + (:http-response db))) + (rf/reg-sub :search-results (fn [db _] @@ -12,6 +17,11 @@ (fn [db _] (:stream db))) +(rf/reg-sub + :playlist + (fn [db _] + (:playlist db))) + (rf/reg-sub :channel (fn [db _] diff --git a/src/frontend/tau/util.cljs b/src/frontend/tau/util.cljs new file mode 100644 index 0000000..1a3e243 --- /dev/null +++ b/src/frontend/tau/util.cljs @@ -0,0 +1,16 @@ +(ns tau.util) + +(defn format-quantity + [num] + (.format + (js/Intl.NumberFormat + "en-US" #js {"notation" "compact" "maximumFractionDigits" 1}) + num)) + +(defn format-duration + [num] + (let [duration (js/Date. (* num 1000)) + slice (if (> (.getHours duration) 1) + #(.slice % 11 19) + #(.slice % 14 19))] + (-> duration (.toISOString) slice))) diff --git a/src/frontend/tau/views.cljs b/src/frontend/tau/views.cljs index 81aaa40..4f89fef 100644 --- a/src/frontend/tau/views.cljs +++ b/src/frontend/tau/views.cljs @@ -10,64 +10,76 @@ (defonce scroll-hook (.addEventListener js/window "scroll" #(rf/dispatch [::events/page-scroll]))) (defonce services (rf/dispatch [::events/get-services])) +(defonce kiosks (rf/dispatch [::events/get-kiosks 0])) (defn footer [] [: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]) - id (js/parseInt (or serviceId service-id)) ] - [:div.flex - [:form {:on-submit (fn [e] - (.preventDefault e) - (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 [::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 [::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]]]])) + [:div.flex.flex-col.justify-center + [:div + [:p.px-2 (str "Tau " (.getFullYear (js/Date.)))]] + [:div.pt-4 + [:a {:href "https://sr.ht/~conses/tau"} + [:i.fa-solid.fa-code]]]]]]) (defn navbar - [match] + [{{:keys [serviceId]} :query-params}] (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 + global-search @(rf/subscribe [:global-search]) + services @(rf/subscribe [:services]) + id (js/parseInt (or serviceId service-id)) + {:keys [available-kiosks default-kiosk]} @(rf/subscribe [:kiosks])] + [:nav.flex.p-2.content-center.sticky.top-0.z-50.font-nunito {: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]])) + [:div.flex + [:form.flex.items-center + {:on-submit (fn [e] + (.preventDefault e) + (rf/dispatch [::events/navigate + {:name ::routes/search + :params {} + :query {:q global-search :serviceId service-id}}]))} + [:div + [:a.px-5.text-white.font-bold.font-nunito + {:href (rfe/href ::routes/home) :dangerouslySetInnerHTML {:__html "τ"}}]] + [:div.relative + [:select.border-none.focus:ring-transparent.bg-blend-color-dodge.font-bold.font-nunito + {:on-change #(rf/dispatch [::events/change-service (js/parseInt (.. % -target -value))]) + :value service-id + :style {:background "transparent"}} + (when services + (for [service services] + [:option.bg-neutral-900.border-none {:value (:id service) :key (:id service)} + (-> service :info :name)]))] + [:div.flex.absolute.min-h-full.min-w-full.top-0.right-0.items-center.justify-end + {:style {:zIndex "-1"}} + [:i.fa-solid.fa-caret-down.mr-4]]] + [:div + [:input.bg-transparent.border-none.rounded.py-2.px-1.mx-2.focus:ring-transparent.placeholder-white + {:type "text" + :value global-search + :on-change #(rf/dispatch [::events/change-global-search (.. % -target -value)]) + :placeholder "Search for something"}]] + [:div + [:button.text-white.mx-2 + {:type "submit"} + [:i.fas.fa-search]]]] + [:div + [:ul.flex.content-center.p-2.text-white.font-roboto + (for [kiosk available-kiosks] + [:li.px-5 {:key kiosk} + [:a {:href (rfe/href ::routes/kiosk nil {:serviceId service-id + :kioskId kiosk})} + kiosk]])]]]])) (defn app [] (let [current-match @(rf/subscribe [:current-match])] - [:div.font-sans.min-h-screen.flex.flex-col.h-full {:style {:background "rgba(23, 23, 23)"}} + [:div.min-h-screen.flex.flex-col.h-full.text-white.bg-neutral-900 [navbar current-match] - [:div.flex.flex-col.justify-between.relative {:class "min-h-[calc(100vh-58px)]"} + [:div.flex.flex-col.justify-between.relative.font-nunito {: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 index abdbb54..db8c221 100644 --- a/src/frontend/tau/views/channel.cljs +++ b/src/frontend/tau/views/channel.cljs @@ -12,30 +12,35 @@ 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]) + page-scroll @(rf/subscribe [:page-scroll]) 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 + (rf/dispatch [::events/channel-pagination url next-page-url])) + [:div.flex.flex-col.items-center.px-5.py-2.flex-auto (if page-loading? [loading/page-loading-icon service-color] [:div {:class "w-4/5"} - [navigation/back-button] - [:div [:img {:src banner}]] + [navigation/back-button service-color] + (when banner + [:div + [:img {:src banner}]]) [:div.flex.items-center.my-4.mx-2 - [:div - [:img.rounded-full {:src avatar}]] + (when avatar + [:div.relative.w-16.h-16 + [:img.rounded-full.object-cover.max-w-full.min-h-full {:src avatar :alt name}]]) [:div.m-4 [:h1.text-xl name] - [:div.flex.my-2.items-center - [:i.fa-solid.fa-users] - [:span.mx-2 subscriber-count]]]] + (when subscriber-count + [:div.flex.my-2.items-center + [:i.fa-solid.fa-users.text-xs] + [:span.mx-2 (.toLocaleString 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])] + [:div.flex.justify-center.items-center.align-center + [:div.flex.justify-start.flex-wrap + (for [[i result] (map-indexed vector related-streams)] + [items/stream-item (assoc result :key i)])]] (when-not (empty? next-page-url) - [loading/pagination-loading-icon service-color pagination-loading?])])])) + [loading/items-pagination-loading-icon service-color pagination-loading?])])])) diff --git a/src/frontend/tau/views/home.cljs b/src/frontend/tau/views/home.cljs deleted file mode 100644 index 00d2e1e..0000000 --- a/src/frontend/tau/views/home.cljs +++ /dev/null @@ -1,7 +0,0 @@ -(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 index 70caacf..6aea258 100644 --- a/src/frontend/tau/views/kiosk.cljs +++ b/src/frontend/tau/views/kiosk.cljs @@ -1,9 +1,35 @@ (ns tau.views.kiosk (:require - [re-frame.core :as rf])) + [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 kiosk - [match] - (let [{:keys [id url related-streams]} @(rf/subscribe [:kiosk])] - [:div - [:h1 id]])) + [{{:keys [serviceId kioskId]} :query-params}] + (let [{:keys [id url related-streams next-page]} @(rf/subscribe [:kiosk]) + next-page-url (:url next-page) + service-color @(rf/subscribe [:service-color]) + page-loading? @(rf/subscribe [:show-page-loading]) + pagination-loading? @(rf/subscribe [:show-pagination-loading]) + page-scroll @(rf/subscribe [:page-scroll]) + scrolled-to-bottom? (= page-scroll (.-scrollHeight js/document.body))] + (when scrolled-to-bottom? + (rf/dispatch [::events/kiosk-pagination serviceId id next-page-url])) + [:div.flex.flex-col.items-center.px-5.py-2.flex-auto + (if page-loading? + [loading/page-loading-icon service-color] + [:div + [:div.flex.justify-center.items-center.my-4.mx-2 + [:div.m-4 + [:h1.text-2xl id]]] + [:div.flex.justify-center.items-center.align-center + [:div.flex.justify-start.flex-wrap + (for [[i item] (map-indexed vector related-streams)] + (case (:type item) + "stream" [items/stream-item (assoc item :key i)] + "channel" [items/channel-item (assoc item :key i)] + "playlist" [items/playlist-item (assoc item :key i)]))]] + (when-not (empty? next-page-url) + [loading/items-pagination-loading-icon service-color pagination-loading?])])])) diff --git a/src/frontend/tau/views/playlist.cljs b/src/frontend/tau/views/playlist.cljs index b82cc24..48f18e4 100644 --- a/src/frontend/tau/views/playlist.cljs +++ b/src/frontend/tau/views/playlist.cljs @@ -1,5 +1,48 @@ -(ns tau.views.playlist) +(ns tau.views.playlist + (:require + [re-frame.core :as rf] + [reitit.frontend.easy :as rfe] + [tau.components.items :as items] + [tau.components.loading :as loading] + [tau.components.navigation :as navigation] + [tau.events :as events])) (defn playlist - [match] - [:div]) + [{{:keys [url]} :query-params}] + (let [{:keys [id name playlist-type thumbnail-url banner-url + uploader-name uploader-url uploader-avatar stream-count + next-page related-streams]} @(rf/subscribe [:playlist]) + next-page-url (:url next-page) + service-color @(rf/subscribe [:service-color]) + page-loading? @(rf/subscribe [:show-page-loading]) + pagination-loading? @(rf/subscribe [:show-pagination-loading]) + page-scroll @(rf/subscribe [:page-scroll]) + scrolled-to-bottom? (= page-scroll (.-scrollHeight js/document.body))] + (when scrolled-to-bottom? + (rf/dispatch [::events/playlist-pagination url next-page-url])) + [:div.flex.flex-col.items-center.px-5.pt-4.flex-auto + (if page-loading? + [loading/page-loading-icon service-color] + [:div.flex.flex-col.flex-auto + [navigation/back-button service-color] + (when banner-url + [:div + [:img {:src banner-url}]]) + [:div.flex.items-center.justify-center.my-4.mx-2 + [:div.flex.flex-col.justify-center.items-center + [:h1.text-2xl.font-bold name] + [:div.flex.items-center.pt-4 + [:span.mr-2 "By"] + [:div.flex.items-center.py-3.box-border.h-12 + [:div.w-12 + [:a {:href (rfe/href :tau.routes/channel nil {:url uploader-url}) :title uploader-name} + [:img.rounded-full.object-cover.min-h-full.min-w-full {:src uploader-avatar :alt uploader-name}]]]]] + [:p.pt-4 (str stream-count " streams")]]] + (if (empty? related-streams) + [:div.flex.flex-auto.justify-center.items-center + [:p.text-2xl "No streams available"]] + [:div.flex.justify-center.align-center.flex-wrap.my-2 + (for [[i result] (map-indexed vector related-streams)] + [items/stream-item (assoc result :key i)]) + (when-not (empty? next-page-url) + [loading/items-pagination-loading-icon service-color pagination-loading?])])])])) diff --git a/src/frontend/tau/views/search.cljs b/src/frontend/tau/views/search.cljs index 69f1353..b6bc75d 100644 --- a/src/frontend/tau/views/search.cljs +++ b/src/frontend/tau/views/search.cljs @@ -18,19 +18,20 @@ 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])) + (rf/dispatch [::events/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?])]])])) + (when items + [:div.flex.flex-col + [:div.flex.justify-center.align-center.flex-wrap.flex-auto + (for [[i item] (map-indexed vector items)] + (case (:type item) + "stream" [items/stream-item (assoc item :key i)] + "channel" [items/channel-item (assoc item :key i)] + "playlist" [items/playlist-item (assoc item :key i)])) + (when-not (empty? next-page-url) + [loading/items-pagination-loading-icon service-color pagination-loading?])]]))])) diff --git a/src/frontend/tau/views/stream.cljs b/src/frontend/tau/views/stream.cljs index 47039e6..4894a01 100644 --- a/src/frontend/tau/views/stream.cljs +++ b/src/frontend/tau/views/stream.cljs @@ -5,15 +5,18 @@ [tau.events :as events] [tau.components.items :as items] [tau.components.loading :as loading] - [tau.components.navigation :as navigation])) + [tau.components.navigation :as navigation] + [tau.components.comments :as comments] + [tau.util :as util])) (defn 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]) + description uploader-avatar uploader-author + uploader-url upload-date related-streams + thumbnail-url show-comments-loading comments-page + show-comments] :as stream} @(rf/subscribe [:stream]) stream-type (-> (if (empty? video-streams) audio-streams video-streams) last :content) @@ -23,59 +26,83 @@ (if page-loading? [loading/page-loading-icon service-color] [:div {:class "w-4/5"} - [navigation/back-button] + [navigation/back-button service-color] [: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 + [:video.bottom-0.object-cover.max-h-full.min-w-full + {:src stream-type :controls true}]] + [:div.flex.flex.w-full.mt-3.justify-center + [:button.border.rounded.border-black.px-3.py-1.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 + [:a {:href url} + [:button.border.rounded.border-black.px-3.py-1.bg-stone-800.mx-2 [:i.fa-solid.fa-external-link-alt]]]] - [:div.flex.flex-col.py-1 + [:div.flex.flex-col.py-1.comments [:div.min-w-full.py-3 - [:h1.text-xl.font-extrabold name]] + [:h1.text-2xl.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}]]) + (when uploader-avatar + [:div.relative.w-16.h-16 + [:a {:href (rfe/href :tau.routes/channel nil {:url uploader-url}) :title uploader-author} + [:img.rounded-full.object-cover.max-w-full.min-h-full {:src uploader-avatar :alt uploader-author}]]]) [:div.mx-2 - [:a {:href (rfe/href :tau.routes/channel nil {:url upload-url})} upload-author] + [:a {:href (rfe/href :tau.routes/channel nil {:url uploader-url})} uploader-author] (when subscriber-count - [:div.flex.my-2 - [:i.fa-solid.fa-users] - [:p.mx-2 (.toLocaleString subscriber-count)]])]] - [:div + [:div.flex.my-2.items-center + [:i.fa-solid.fa-users.text-xs] + [:p.mx-2 (util/format-quantity subscriber-count)]])]] + [:div.flex.flex-col.items-end (when view-count - [:p - [:i.fa-solid.fa-eye] - [:span.mx-2 (.toLocaleString view-count)]]) - [:div + [:div + [:i.fa-solid.fa-eye.text-xs] + [:span.ml-2 (.toLocaleString view-count)]]) + [:div.flex (when like-count - [:p - [:i.fa-solid.fa-thumbs-up] - [:span.mx-2 like-count]]) + [:div.items-center + [:i.fa-solid.fa-thumbs-up.text-xs] + [:span.ml-2 (.toLocaleString like-count)]]) (when dislike-count - [:p - [:i.fa-solid.fa-thumbs-down] - [:span.mx-2 dislike-count]])] + [:div.ml-2.items-center + [:i.fa-solid.fa-thumbs-down.text-xs] + [:span.ml-2 dislike-count]])] (when upload-date - [:p (-> upload-date - js/Date.parse - js/Date. - .toDateString)])]] + [:div + [:i.fa-solid.fa-calendar.mx-2.text-xs] + [:span + (-> upload-date + js/Date.parse + js/Date. + .toDateString)]])]] [:div.min-w-full.py-3 [:h1 name] - [:p description]] + [:div {:dangerouslySetInnerHTML {:__html 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]))]]]])])) + [:div.flex.items-center + [:i.fa-solid.fa-comments] + [:p.px-2 "Comments"] + (if show-comments + [:i.fa-solid.fa-chevron-up {:on-click #(rf/dispatch [::events/toggle-comments]) + :style {:cursor "pointer"}}] + [:i.fa-solid.fa-chevron-down {:on-click #(if show-comments + (rf/dispatch [::events/toggle-comments]) + (rf/dispatch [::events/get-comments url])) + :style {:cursor "pointer"}}])] + [:div + (if show-comments-loading + [loading/page-loading-icon service-color] + (when (and show-comments comments-page) + [comments/comments comments-page uploader-author uploader-avatar url]))]] + (when-not (empty? related-streams) + [:div.py-3 + [:div.flex.items-center + [:i.fa-solid.fa-list] + [:h1.px-2.text-lg.bold "Related Results"]] + [:div.flex.justify-center.align-center.flex-wrap + (for [[i item] (map-indexed vector related-streams)] + (case (:type item) + "stream" [items/stream-item (assoc item :key i)] + "channel" [items/channel-item (assoc item :key i)] + "playlist" [items/playlist-item (assoc item :key i)]))]])]])])) -- cgit v1.2.3