From cbf24817194d3085da8f1be0c532c8f42019a585 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Mon, 6 May 2024 15:54:43 -0600 Subject: [PATCH] Implement infinite scrolling for frigate+ view --- web/src/pages/SubmitPlus.tsx | 132 ++++++++++++++++++++++++++++++----- 1 file changed, 115 insertions(+), 17 deletions(-) diff --git a/web/src/pages/SubmitPlus.tsx b/web/src/pages/SubmitPlus.tsx index e2863760a..0799597aa 100644 --- a/web/src/pages/SubmitPlus.tsx +++ b/web/src/pages/SubmitPlus.tsx @@ -4,6 +4,7 @@ import { GeneralFilterContent, } from "@/components/filter/ReviewFilterGroup"; import Chip from "@/components/indicators/Chip"; +import ActivityIndicator from "@/components/indicators/activity-indicator"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -34,7 +35,7 @@ import { ATTRIBUTE_LABELS, FrigateConfig } from "@/types/frigateConfig"; import { getIconForLabel } from "@/utils/iconUtil"; import { capitalizeFirstLetter } from "@/utils/stringUtil"; import axios from "axios"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { isMobile } from "react-device-detect"; import { FaList, @@ -44,6 +45,9 @@ import { } from "react-icons/fa"; import { PiSlidersHorizontalFill } from "react-icons/pi"; import useSWR from "swr"; +import useSWRInfinite from "swr/infinite"; + +const API_LIMIT = 100; export default function SubmitPlus() { const { data: config } = useSWR("config"); @@ -64,21 +68,93 @@ export default function SubmitPlus() { // data - const { data: events, mutate: refresh } = useSWR([ - "events", - { - limit: 100, - in_progress: 0, - is_submitted: 0, - cameras: selectedCameras ? selectedCameras.join(",") : null, - labels: selectedLabels ? selectedLabels.join(",") : null, - min_score: scoreRange ? scoreRange[0] : null, - max_score: scoreRange ? scoreRange[1] : null, - sort: sort ? sort : null, + const eventFetcher = useCallback((key: string) => { + const [path, params] = Array.isArray(key) ? key : [key, undefined]; + return axios.get(path, { params }).then((res) => res.data); + }, []); + + const getKey = useCallback( + (index: number, prevData: Event[]) => { + if (index > 0) { + const lastDate = prevData[prevData.length - 1].start_time; + return [ + "events", + { + limit: API_LIMIT, + in_progress: 0, + is_submitted: 0, + cameras: selectedCameras ? selectedCameras.join(",") : null, + labels: selectedLabels ? selectedLabels.join(",") : null, + min_score: scoreRange ? scoreRange[0] : null, + max_score: scoreRange ? scoreRange[1] : null, + sort: sort ? sort : null, + before: lastDate, + }, + ]; + } + + return [ + "events", + { + limit: 100, + in_progress: 0, + is_submitted: 0, + cameras: selectedCameras ? selectedCameras.join(",") : null, + labels: selectedLabels ? selectedLabels.join(",") : null, + min_score: scoreRange ? scoreRange[0] : null, + max_score: scoreRange ? scoreRange[1] : null, + sort: sort ? sort : null, + }, + ]; }, - ]); + [scoreRange, selectedCameras, selectedLabels, sort], + ); + + const { + data: eventPages, + mutate: refresh, + size, + setSize, + isValidating, + } = useSWRInfinite(getKey, eventFetcher, { + revalidateOnFocus: false, + }); + + const events = useMemo( + () => (eventPages ? eventPages.flat() : []), + [eventPages], + ); + const [upload, setUpload] = useState(); + // paging + + const isDone = useMemo( + () => (eventPages?.at(-1)?.length ?? 0) < API_LIMIT, + [eventPages], + ); + + const pagingObserver = useRef(); + const lastEventRef = useCallback( + (node: HTMLElement | null) => { + if (isValidating) return; + if (pagingObserver.current) pagingObserver.current.disconnect(); + try { + pagingObserver.current = new IntersectionObserver((entries) => { + if (entries[0].isIntersecting && !isDone) { + setSize(size + 1); + } + }); + if (node) pagingObserver.current.observe(node); + } catch (e) { + // no op + } + }, + [isValidating, isDone, size, setSize], + ); + + // layout + const grow = useMemo(() => { if (!config || !upload) { return ""; @@ -110,18 +186,35 @@ export default function SubmitPlus() { }); refresh( - (data: Event[] | undefined) => { + (data: Event[][] | undefined) => { if (!data) { return data; } - const index = data.findIndex((e) => e.id == upload.id); + let pageIndex = -1; + let index = -1; + + data.forEach((page, pIdx) => { + const search = page.findIndex((e) => e.id == upload.id); + + if (search != -1) { + pageIndex = pIdx; + index = search; + } + }); if (index == -1) { return data; } - return [...data.slice(0, index), ...data.slice(index + 1)]; + return [ + ...data.slice(0, pageIndex), + [ + ...data[pageIndex].slice(0, index), + ...data[pageIndex].slice(index + 1), + ], + ...data.slice(pageIndex + 1), + ]; }, { revalidate: false, populateCache: true }, ); @@ -182,14 +275,17 @@ export default function SubmitPlus() { - {events?.map((event) => { + {events?.map((event, eIdx) => { if (event.data.type != "object") { return; } + const lastRow = eIdx == events.length - 1; + return (
setUpload(event)} > @@ -228,6 +324,8 @@ export default function SubmitPlus() {
); })} + + {isValidating && }