aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMiguel Ángel Moreno <mail@migalmoreno.com>2022-12-26 22:02:33 +0100
committerMiguel Ángel Moreno <mail@migalmoreno.com>2022-12-26 22:02:33 +0100
commitb5404ac06a3a09d83bef66552083254fdff12196 (patch)
tree02785ab11a3dcf328e8d1375a7b76abb2edbf382
parentdce9e36b352509665887ea1ca2e6f81904038a11 (diff)
feat(frontend): Modularize components and add pagination
-rw-r--r--src/frontend/tau/components/items.cljs74
-rw-r--r--src/frontend/tau/components/loading.cljs16
-rw-r--r--src/frontend/tau/components/navigation.cljs13
-rw-r--r--src/frontend/tau/core.cljs7
-rw-r--r--src/frontend/tau/events.cljs208
-rw-r--r--src/frontend/tau/routes.cljs31
-rw-r--r--src/frontend/tau/subs.cljs35
-rw-r--r--src/frontend/tau/views.cljs74
-rw-r--r--src/frontend/tau/views/channel.cljs41
-rw-r--r--src/frontend/tau/views/home.cljs2
-rw-r--r--src/frontend/tau/views/kiosk.cljs9
-rw-r--r--src/frontend/tau/views/playlist.cljs5
-rw-r--r--src/frontend/tau/views/search.cljs64
-rw-r--r--src/frontend/tau/views/stream.cljs98
14 files changed, 555 insertions, 122 deletions
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
@@ -13,6 +13,11 @@
(:stream db)))
(rf/reg-sub
+ :channel
+ (fn [db _]
+ (:channel db)))
+
+(rf/reg-sub
:global-search
(fn [db _]
(:global-search db)))
@@ -23,16 +28,36 @@
(: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 _]
(:global-stream 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 "&tau;"}}]]
- [: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 "&tau;"}}]]
+ [: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]))]]]])]))