Sfoglia il codice sorgente

feat(frontend): Add further features and address quirks

Miguel Ángel Moreno 2 anni fa
parent
commit
8c46de3834

+ 59 - 0
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"]]))]))

+ 67 - 61
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]]]]])

+ 9 - 5
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}}]])

+ 7 - 8
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"]]])

+ 151 - 39
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]

+ 20 - 19
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)]

+ 10 - 0
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 _]

+ 16 - 0
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)))

+ 55 - 43
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]

+ 19 - 14
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?])])]))

+ 0 - 7
src/frontend/tau/views/home.cljs

@@ -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"]])

+ 31 - 5
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?])])]))

+ 46 - 3
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?])])])]))

+ 11 - 10
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?])]]))]))

+ 69 - 42
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)]))]])]])]))