aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMiguel Ángel Moreno <mail@migalmoreno.com>2024-12-01 13:21:40 +0100
committerMiguel Ángel Moreno <mail@migalmoreno.com>2024-12-01 13:26:44 +0100
commit6ecf3c163b231452ac0bc08e7d2947ee32950339 (patch)
treef1e56a52b50c725a2b9b2201cae678babc6d191f
parent0efab725aa38fc8a2072d41c50801a7df9771a41 (diff)
feat: add layout switcher to related items listing
-rw-r--r--src/frontend/tubo/channel/views.cljs6
-rw-r--r--src/frontend/tubo/items/views.cljs182
-rw-r--r--src/frontend/tubo/kiosks/views.cljs27
-rw-r--r--src/frontend/tubo/playlist/views.cljs36
-rw-r--r--src/frontend/tubo/search/views.cljs23
5 files changed, 182 insertions, 92 deletions
diff --git a/src/frontend/tubo/channel/views.cljs b/src/frontend/tubo/channel/views.cljs
index 3ee4518..bd033f7 100644
--- a/src/frontend/tubo/channel/views.cljs
+++ b/src/frontend/tubo/channel/views.cljs
@@ -38,7 +38,8 @@
(defn channel
[_]
- (let [!show-description? (r/atom false)]
+ (let [!show-description? (r/atom false)
+ !layout (r/atom :list)]
(fn [{{:keys [url]} :query-params}]
(let [{:keys [banner description next-page related-streams] :as channel}
@(rf/subscribe [:channel])
@@ -57,4 +58,5 @@
(when-not (empty? description)
[layout/show-more-container @!show-description? description
#(reset! !show-description? (not @!show-description?))])
- [items/related-streams related-streams next-page-url]]]))))
+ [items/layout-switcher !layout]
+ [items/related-streams related-streams next-page-url !layout]]]))))
diff --git a/src/frontend/tubo/items/views.cljs b/src/frontend/tubo/items/views.cljs
index 343b3c2..7470d65 100644
--- a/src/frontend/tubo/items/views.cljs
+++ b/src/frontend/tubo/items/views.cljs
@@ -59,73 +59,143 @@
[layout/popover-menu !menu-active? items :extra-classes
[:pr-0 :pl-4] :menu-styles {:right "15px"}])))))
-(defn item-content
+(defn grid-item-content
[{:keys [url name uploader-url uploader-name subscriber-count view-count
- stream-count verified?]
+ stream-count verified? thumbnail-url duration]
:as item} route bookmarks]
- [:div
- (when name
- [:div.flex.items-center.my-2
- [:a {:href route :title name}
- [:h1.line-clamp-2.my-1 {:class "[overflow-wrap:anywhere]"} name]]
- (when (and verified? (not uploader-url))
- [:i.fa-solid.fa-circle-check.pl-2])])
- [:div.flex.justify-between
- [:div.flex.items-center.my-2
- (conj
- (when uploader-url
- [:a
- {:href (rfe/href :channel-page nil {:url uploader-url})
- :title uploader-name
- :key url}])
- [:h1.text-neutral-800.dark:text-gray-300.font-semibold.pr-2.line-clamp-1.break-all
- {:class "[overflow-wrap:anywhere]" :title uploader-name :key url}
- uploader-name])
- (when (and uploader-url verified?)
- [:i.fa-solid.fa-circle-check])]
- [item-popover item bookmarks]]
- (when subscriber-count
- [:div.flex.items-center
- [:i.fa-solid.fa-users.text-xs]
- [:span.mx-2 (utils/format-quantity subscriber-count)]])
- (when stream-count
- [:div.flex.items-center
- [:i.fa-solid.fa-video.text-xs]
- [:span.mx-2 (utils/format-quantity stream-count)]])
- [:div.flex.my-1.justify-between
- [:span (utils/format-date-ago (:upload-date item))]
- (when view-count
- [:div.flex.items-center.h-full.pl-2
- [:i.fa-solid.fa-eye.text-xs]
- [:p.pl-1.5 (utils/format-quantity view-count)]])]])
+ [:div.w-full
+ [:div.flex.flex-col.max-w-full.min-h-full.max-h-full
+ [layout/thumbnail thumbnail-url route name duration
+ :classes [:py-2 :h-44 "xs:h-28"] :rounded? true]
+ [:div
+ (when name
+ [:div.flex.items-center.my-2
+ [:a {:href route :title name}
+ [:h1.line-clamp-2.my-1 {:class "[overflow-wrap:anywhere]"} name]]
+ (when (and verified? (not uploader-url))
+ [:i.fa-solid.fa-circle-check.pl-2])])
+ [:div.flex.justify-between
+ [:div.flex.items-center.my-2
+ (conj
+ (when uploader-url
+ [:a
+ {:href (rfe/href :channel-page nil {:url uploader-url})
+ :title uploader-name
+ :key url}])
+ [:h1.text-neutral-800.dark:text-gray-300.font-semibold.pr-2.line-clamp-1.break-all
+ {:class "[overflow-wrap:anywhere]" :title uploader-name :key url}
+ uploader-name])
+ (when (and uploader-url verified?)
+ [:i.fa-solid.fa-circle-check])]
+ [item-popover item bookmarks]]
+ (when subscriber-count
+ [:div.flex.items-center
+ [:i.fa-solid.fa-users.text-xs]
+ [:span.mx-2 (utils/format-quantity subscriber-count)]])
+ (when stream-count
+ [:div.flex.items-center
+ [:i.fa-solid.fa-video.text-xs]
+ [:span.mx-2 (utils/format-quantity stream-count)]])
+ [:div.flex.my-1.justify-between
+ [:span (utils/format-date-ago (:upload-date item))]
+ (when view-count
+ [:div.flex.items-center.h-full.pl-2
+ [:i.fa-solid.fa-eye.text-xs]
+ [:p.pl-1.5 (utils/format-quantity view-count)]])]]]])
-(defn generic-item
- [{:keys [url name thumbnail-url duration] :as item} bookmarks]
- (let [item-url (case (:type item)
- "stream" (rfe/href :stream-page nil {:url url})
- "channel" (rfe/href :channel-page nil {:url url})
- "playlist" (rfe/href :playlist-page nil {:url url})
- url)]
- [:div.w-full
- [:div.flex.flex-col.max-w-full.min-h-full.max-h-full
- [layout/thumbnail thumbnail-url item-url name duration
- :classes [:py-2 :h-44 "xs:h-28"] :rounded? true]
- [item-content item item-url bookmarks]]]))
+(defn list-item-content
+ [{:keys [url name uploader-url uploader-name subscriber-count view-count
+ stream-count verified? thumbnail-url duration]
+ :as item} route bookmarks]
+ [:div.w-full
+ [:div.flex.gap-x-5
+ [layout/thumbnail thumbnail-url route name duration
+ :classes
+ [:py-2 :h-28 "xs:h-36" "min-w-[150px]" "max-w-[150px]" "sm:min-w-[250px]"
+ "sm:max-w-[250px]"] :rounded?
+ true]
+ [:div.flex-auto.mr-2
+ (when name
+ [:div.flex.items-center.justify-between.mt-2
+ [:a {:href route :title name}
+ [:h1.line-clamp-1.text-xl
+ {:class "[overflow-wrap:anywhere]"}
+ name
+ (when (and verified? (not uploader-url))
+ [:i.fa-solid.fa-circle-check.pl-3.text-sm])]]
+ [item-popover item bookmarks]])
+ [:div.flex.justify-between
+ [:div.flex.flex-col
+ [:div.flex.text-neutral-900.dark:text-neutral-400.text-sm.flex-col.xs:flex-row
+ (when view-count
+ [:<>
+ [:div.flex.items-center.h-full
+ [:p (str (utils/format-quantity view-count) " views")]]
+ [:span.px-2.hidden.xs:inline-block
+ {:dangerouslySetInnerHTML {:__html "&bull;"}}]])
+ [:span (utils/format-date-ago (:upload-date item))]]
+ [:div.my-2.xs:my-4.flex.gap-2.items-center
+ [layout/uploader-avatar item :classes ["w-6" "h-6"]]
+ (conj
+ (when uploader-url
+ [:a
+ {:href (rfe/href :channel-page nil {:url uploader-url})
+ :title uploader-name
+ :key url}])
+ [:h1.text-neutral-900.dark:text-neutral-400.font-bold.line-clamp-1.break-all.text-sm
+ {:class "[overflow-wrap:anywhere]" :title uploader-name :key url}
+ uploader-name])
+ (when (and uploader-url verified?)
+ [:i.fa-solid.fa-circle-check.text-sm.text-neutral-400])]]]
+ [:div.flex.text-neutral-800.dark:text-gray-300.text-sm.flex-col.xs:flex-row
+ (when subscriber-count
+ [:<>
+ [:div.flex.items-center.h-full
+ [:p (str (utils/format-quantity subscriber-count) " subscribers")]]
+ [:span.px-2.hidden.xs:inline-block
+ {:dangerouslySetInnerHTML {:__html "&bull;"}}]])
+ (when stream-count
+ [:span (str (utils/format-quantity stream-count) " streams")])]]]])
(defn related-streams
- [related-streams next-page-url]
+ [related-streams next-page-url !layout]
(let [service-color @(rf/subscribe [:service-color])
pagination-loading? @(rf/subscribe [:show-pagination-loading])
- bookmarks @(rf/subscribe [:bookmarks])]
- [:div.flex.flex-col.items-center.flex-auto.my-2.md:my-8
+ bookmarks @(rf/subscribe [:bookmarks])
+ item-url #(rfe/href (case (:type %)
+ "stream" :stream-page
+ "channel" :channel-page
+ "playlist" :playlist-page
+ (:url %))
+ nil
+ {:url (:url %)})]
+ [:div.flex.flex-col.flex-auto.my-2.md:my-8
[modals/modal]
(if (empty? related-streams)
[:div.flex.items-center.flex-auto.flex-col.justify-center.gap-y-4
[:i.fa-solid.fa-ghost.text-3xl]
[:p.text-lg "No available streams"]]
- [:div.grid.w-full.gap-x-10.gap-y-6
- {:class "xs:grid-cols-[repeat(auto-fill,_minmax(165px,_1fr))]"}
- (for [[i item] (map-indexed vector related-streams)]
- ^{:key i} [generic-item item bookmarks])])
+ (if (and !layout (= @!layout :grid))
+ [:div.grid.w-full.gap-x-10.gap-y-6
+ {:class "xs:grid-cols-[repeat(auto-fill,_minmax(165px,_1fr))]"}
+ (for [[i item] (map-indexed vector related-streams)]
+ ^{:key i} [grid-item-content item (item-url item) bookmarks])]
+ [:div.flex.flex-wrap.w-full.gap-x-10.gap-y-6
+ (for [[i item] (map-indexed vector related-streams)]
+ ^{:key i} [list-item-content item (item-url item) bookmarks])]))
(when (and pagination-loading? (seq next-page-url))
[layout/loading-icon service-color :text-md])]))
+
+(defn layout-switcher
+ [!layout]
+ [:div.gap-x-6.text-lg.flex.justify-end
+ [:button
+ {:on-click #(reset! !layout :list)
+ :title "Switch to list layout"}
+ [:i.fa-solid.fa-list
+ {:class (when-not (= @!layout :list) :text-neutral-500)}]]
+ [:button
+ {:on-click #(reset! !layout :grid)
+ :title "Switch to grid layout"}
+ [:i.fa-solid.fa-grip
+ {:class (when-not (= @!layout :grid) :text-neutral-500)}]]])
diff --git a/src/frontend/tubo/kiosks/views.cljs b/src/frontend/tubo/kiosks/views.cljs
index e42719c..fd9b07e 100644
--- a/src/frontend/tubo/kiosks/views.cljs
+++ b/src/frontend/tubo/kiosks/views.cljs
@@ -1,5 +1,6 @@
(ns tubo.kiosks.views
(:require
+ [reagent.core :as r]
[re-frame.core :as rf]
[reitit.frontend.easy :as rfe]
[tubo.items.views :as items]
@@ -32,14 +33,18 @@
kiosk]])])
(defn kiosk
- [{{:keys [serviceId]} :query-params}]
- (let [{:keys [id related-streams next-page]}
- @(rf/subscribe [:kiosk])
- next-page-url (:url next-page)
- service-id (or @(rf/subscribe [:service-id]) serviceId)
- scrolled-to-bottom? @(rf/subscribe [:scrolled-to-bottom])]
- (when scrolled-to-bottom?
- (rf/dispatch [:kiosks/fetch-paginated service-id id next-page-url]))
- [layout/content-container
- [layout/content-header id]
- [items/related-streams related-streams next-page-url]]))
+ [_]
+ (let [!layout (r/atom :list)]
+ (fn [{{:keys [serviceId]} :query-params}]
+ (let [{:keys [id related-streams next-page]}
+ @(rf/subscribe [:kiosk])
+ next-page-url (:url next-page)
+ service-id (or @(rf/subscribe [:service-id]) serviceId)
+ scrolled-to-bottom? @(rf/subscribe [:scrolled-to-bottom])]
+ (when scrolled-to-bottom?
+ (rf/dispatch [:kiosks/fetch-paginated service-id id next-page-url]))
+ [layout/content-container
+ [layout/content-header
+ id
+ [items/layout-switcher !layout]]
+ [items/related-streams related-streams next-page-url !layout]]))))
diff --git a/src/frontend/tubo/playlist/views.cljs b/src/frontend/tubo/playlist/views.cljs
index ad2be19..065a3e0 100644
--- a/src/frontend/tubo/playlist/views.cljs
+++ b/src/frontend/tubo/playlist/views.cljs
@@ -7,10 +7,25 @@
[tubo.items.views :as items]
[tubo.layout.views :as layout]))
-(defn playlist
- [{{:keys [url]} :query-params}]
+(defn metadata-popover
+ [_]
(let [!menu-active? (r/atom nil)]
- (fn []
+ (fn [{:keys [related-streams]}]
+ (when related-streams
+ [layout/popover-menu !menu-active?
+ [{:label "Add to queue"
+ :icon [:i.fa-solid.fa-headphones]
+ :on-click #(rf/dispatch [:queue/add-n related-streams true])}
+ {:label "Add to playlist"
+ :icon [:i.fa-solid.fa-plus]
+ :on-click #(rf/dispatch [:modals/open
+ [modals/add-to-bookmark
+ related-streams]])}]]))))
+
+(defn playlist
+ [_]
+ (let [!layout (r/atom :list)]
+ (fn [{{:keys [url]} :query-params}]
(let [{:keys [name next-page uploader-name uploader-url related-streams
stream-count]
:as playlist}
@@ -22,16 +37,8 @@
[layout/content-container
[:div.flex.flex-col.justify-center
[layout/content-header name
- (when related-streams
- [layout/popover-menu !menu-active?
- [{:label "Add to queue"
- :icon [:i.fa-solid.fa-headphones]
- :on-click #(rf/dispatch [:queue/add-n related-streams true])}
- {:label "Add to playlist"
- :icon [:i.fa-solid.fa-plus]
- :on-click #(rf/dispatch [:modals/open
- [modals/add-to-bookmark
- related-streams]])}]])]
+ [:div.hidden.lg:block
+ [metadata-popover playlist]]]
[:div.flex.items-center.justify-between.my-4.gap-x-4
[:div.flex.items-center
[layout/uploader-avatar playlist]
@@ -40,4 +47,5 @@
:title uploader-name}
uploader-name]]
[:span.text-sm.whitespace-nowrap (str stream-count " streams")]]]
- [items/related-streams related-streams next-page-url]]))))
+ [items/layout-switcher !layout]
+ [items/related-streams related-streams next-page-url !layout]]))))
diff --git a/src/frontend/tubo/search/views.cljs b/src/frontend/tubo/search/views.cljs
index e97a627..5896d01 100644
--- a/src/frontend/tubo/search/views.cljs
+++ b/src/frontend/tubo/search/views.cljs
@@ -1,16 +1,21 @@
(ns tubo.search.views
(:require
[re-frame.core :as rf]
+ [reagent.core :as r]
[tubo.items.views :as items]
[tubo.layout.views :as layout]))
(defn search
- [{{:keys [q serviceId]} :query-params}]
- (let [{:keys [items next-page]} @(rf/subscribe [:search/results])
- next-page-url (:url next-page)
- service-id (or @(rf/subscribe [:service-id]) serviceId)
- scrolled-to-bottom? @(rf/subscribe [:scrolled-to-bottom])]
- (when scrolled-to-bottom?
- (rf/dispatch [:search/fetch-paginated q service-id next-page-url]))
- [layout/content-container
- [items/related-streams items next-page-url]]))
+ [_]
+ (let [!layout (r/atom :list)]
+ (fn [{{:keys [q serviceId]} :query-params}]
+ (let [{:keys [items next-page]} @(rf/subscribe [:search/results])
+ next-page-url (:url next-page)
+ service-id (or @(rf/subscribe [:service-id])
+ serviceId)
+ scrolled-to-bottom? @(rf/subscribe [:scrolled-to-bottom])]
+ (when scrolled-to-bottom?
+ (rf/dispatch [:search/fetch-paginated q service-id next-page-url]))
+ [layout/content-container
+ [items/layout-switcher !layout]
+ [items/related-streams items next-page-url !layout]]))))