aboutsummaryrefslogtreecommitdiff
path: root/src/frontend/tau
diff options
context:
space:
mode:
Diffstat (limited to 'src/frontend/tau')
-rw-r--r--src/frontend/tau/components/comments.cljs59
-rw-r--r--src/frontend/tau/components/items.cljs128
-rw-r--r--src/frontend/tau/components/loading.cljs14
-rw-r--r--src/frontend/tau/components/navigation.cljs15
-rw-r--r--src/frontend/tau/events.cljs190
-rw-r--r--src/frontend/tau/routes.cljs39
-rw-r--r--src/frontend/tau/subs.cljs10
-rw-r--r--src/frontend/tau/util.cljs16
-rw-r--r--src/frontend/tau/views.cljs98
-rw-r--r--src/frontend/tau/views/channel.cljs33
-rw-r--r--src/frontend/tau/views/home.cljs7
-rw-r--r--src/frontend/tau/views/kiosk.cljs36
-rw-r--r--src/frontend/tau/views/playlist.cljs49
-rw-r--r--src/frontend/tau/views/search.cljs21
-rw-r--r--src/frontend/tau/views/stream.cljs111
15 files changed, 570 insertions, 256 deletions
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
@@ -144,6 +172,49 @@
(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]]
(assoc db :kiosks (js->clj res :keywordize-keys true))))
@@ -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
@@ -3,6 +3,11 @@
[re-frame.core :as rf]))
(rf/reg-sub
+ :http-response
+ (fn [db _]
+ (:http-response db)))
+
+(rf/reg-sub
:search-results
(fn [db _]
(:search-results db)))
@@ -13,6 +18,11 @@
(:stream db)))
(rf/reg-sub
+ :playlist
+ (fn [db _]
+ (:playlist db)))
+
+(rf/reg-sub
:channel
(fn [db _]
(:channel 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)]))]])]])]))