From d0977b0c514d5d2a9abadf712261d4d2c1a6cf87 Mon Sep 17 00:00:00 2001 From: Miguel Ángel Moreno Date: Sun, 8 Dec 2024 21:54:26 +0100 Subject: feat: replace scrolled to bottom sub with IntersectionObserver API --- src/frontend/tubo/items/views.cljs | 279 ++++++++++++++++++++----------------- 1 file changed, 150 insertions(+), 129 deletions(-) diff --git a/src/frontend/tubo/items/views.cljs b/src/frontend/tubo/items/views.cljs index 959285c..8bfc7a1 100644 --- a/src/frontend/tubo/items/views.cljs +++ b/src/frontend/tubo/items/views.cljs @@ -62,143 +62,164 @@ [{:keys [url name uploader-url uploader-name subscriber-count view-count stream-count verified? thumbnails duration type] :as item} route bookmarks] - [:div.w-full - [:div.flex.flex-col.max-w-full.min-h-full.max-h-full - [layout/thumbnail - (-> thumbnails - last - :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.text-sm])]) - [:div.flex.justify-between + [:div.flex.flex-col.max-w-full.min-h-full.max-h-full + [layout/thumbnail + (-> thumbnails + last + :url) route name duration + :classes [:py-2 :h-44 "xs:h-28"] :rounded? true] + [:div + (when name [: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.text-xs])] - [item-popover item bookmarks]] - (when (and (= type "channel") 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)]])]]]]) + [: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.text-sm])]) + [: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.text-xs])] + [item-popover item bookmarks]] + (when (and (= type "channel") 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 list-item-content [{:keys [url name uploader-url uploader-name subscriber-count view-count stream-count verified? thumbnails duration upload-date type] :as item} route bookmarks] - [:div.w-full - [:div.flex.gap-x-3.xs:gap-x-5 - [layout/thumbnail - (-> thumbnails - last - :url) route name duration - :classes - [:py-2 :h-28 "sm:h-36" "min-w-[130px]" "max-w-[130px]" "sm:min-w-[250px]" - "sm:max-w-[250px]"] :rounded? - true] - [:div.flex.flex-col.flex-auto.xs:mr-2.gap-y-2 - (when name - [:div.flex.items-center.justify-between.mt-2 - [:a {:href route :title name} - [:h1.line-clamp-1.xs: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.items-center.justify-between.gap-y-2 - [:div.flex.flex-col.justify-center.gap-y-2.text-neutral-600.dark:text-neutral-400 - (when (or uploader-url uploader-name) - [:div.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.font-semibold.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-xs])]) - (when (or view-count upload-date) - [:div.flex.text-xs.xs: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 "•"}}]]) - [:span (utils/format-date-ago upload-date)]]) - (when (or subscriber-count stream-count) - [:div.flex.text-xs.xs:text-sm.flex-col.xs:flex-row - (when (and (= type "channel") subscriber-count) - [:<> - [:div.flex.items-center.h-full - [:p - (str (utils/format-quantity subscriber-count) " subscribers")]] - (when stream-count - [:span.px-2.hidden.xs:inline-block - {:dangerouslySetInnerHTML {:__html "•"}}])]) - (when stream-count - [:span - (str (utils/format-quantity stream-count) " streams")])])]]]]]) + [:div.flex.gap-x-3.xs:gap-x-5 + [layout/thumbnail + (-> thumbnails + last + :url) route name duration + :classes + [:py-2 :h-28 "sm:h-36" "min-w-[130px]" "max-w-[130px]" "sm:min-w-[250px]" + "sm:max-w-[250px]"] :rounded? + true] + [:div.flex.flex-col.flex-auto.xs:mr-2.gap-y-2 + (when name + [:div.flex.items-center.justify-between.mt-2 + [:a {:href route :title name} + [:h1.line-clamp-1.xs: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.items-center.justify-between.gap-y-2 + [:div.flex.flex-col.justify-center.gap-y-2.text-neutral-600.dark:text-neutral-400 + (when (or uploader-url uploader-name) + [:div.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.font-semibold.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-xs])]) + (when (or view-count upload-date) + [:div.flex.text-xs.xs: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 "•"}}]]) + [:span (utils/format-date-ago upload-date)]]) + (when (or subscriber-count stream-count) + [:div.flex.text-xs.xs:text-sm.flex-col.xs:flex-row + (when (and (= type "channel") subscriber-count) + [:<> + [:div.flex.items-center.h-full + [:p + (str (utils/format-quantity subscriber-count) " subscribers")]] + (when stream-count + [:span.px-2.hidden.xs:inline-block + {:dangerouslySetInnerHTML {:__html "•"}}])]) + (when stream-count + [:span + (str (utils/format-quantity stream-count) " streams")])])]]]]) (defn related-streams - [related-streams next-page-url !layout] - (let [service-color @(rf/subscribe [:service-color]) - pagination-loading? @(rf/subscribe [:show-pagination-loading]) - bookmarks @(rf/subscribe [:bookmarks]) - item-url #(case (:type %) - "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.flex.flex-col.flex-auto.my-2.md:my-8 - (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"]] - (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])])) + [] + (let [!observer (atom nil)] + (fn [related-streams next-page-url !layout pagination-fn] + (let [service-color @(rf/subscribe [:service-color]) + pagination-loading? @(rf/subscribe [:show-pagination-loading]) + bookmarks @(rf/subscribe [:bookmarks]) + item-url #(case (:type %) + "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 %)) + last-item-ref #(when-not pagination-loading? + (when @!observer (.disconnect @!observer)) + (when % + (.observe + (reset! !observer + (js/IntersectionObserver. + (fn [entries] + (when (.-isIntersecting (first + entries)) + (pagination-fn))))) + %)))] + [:div.flex.flex-col.flex-auto.my-2.md:my-8 + (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"]] + (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} + [:div.w-full + {:ref (when (and (seq next-page-url) + (= (+ i 1) (count related-streams))) + last-item-ref)} + [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} + [:div.w-full + {:ref (when (and (seq next-page-url) + (= (+ i 1) (count related-streams))) + last-item-ref)} + [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] -- cgit v1.2.3