Implement infinite scrolling for frigate+ view (#11273)

* Implement infinite scrolling for frigate+ view

* Also fixes safari preview glitch

* Show sub label name in hover
This commit is contained in:
Nicolas Mowen 2024-05-06 17:00:21 -06:00 committed by GitHub
parent e5e18a5026
commit fc0f6d6095
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 127 additions and 18 deletions

View File

@ -184,7 +184,13 @@ export default function LivePlayer({
</TooltipTrigger> </TooltipTrigger>
</div> </div>
<TooltipContent className="capitalize"> <TooltipContent className="capitalize">
{[...new Set([...(objects || []).map(({ label }) => label)])] {[
...new Set([
...(objects || []).map(({ label, sub_label }) =>
label.endsWith("verified") ? sub_label : label,
),
]),
]
.filter( .filter(
(label) => (label) =>
label !== undefined && !label.includes("-verified"), label !== undefined && !label.includes("-verified"),

View File

@ -431,6 +431,11 @@ export function VideoPreview({
setReviewed(); setReviewed();
if (loop && playerRef.current) { if (loop && playerRef.current) {
if (manualPlayback) {
setManualPlayback(false);
setTimeout(() => setManualPlayback(true), 100);
}
playerRef.current.currentTime = playerStartTime; playerRef.current.currentTime = playerStartTime;
return; return;
} }

View File

@ -4,6 +4,7 @@ import {
GeneralFilterContent, GeneralFilterContent,
} from "@/components/filter/ReviewFilterGroup"; } from "@/components/filter/ReviewFilterGroup";
import Chip from "@/components/indicators/Chip"; import Chip from "@/components/indicators/Chip";
import ActivityIndicator from "@/components/indicators/activity-indicator";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Dialog, Dialog,
@ -34,7 +35,7 @@ import { ATTRIBUTE_LABELS, FrigateConfig } from "@/types/frigateConfig";
import { getIconForLabel } from "@/utils/iconUtil"; import { getIconForLabel } from "@/utils/iconUtil";
import { capitalizeFirstLetter } from "@/utils/stringUtil"; import { capitalizeFirstLetter } from "@/utils/stringUtil";
import axios from "axios"; 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 { isMobile } from "react-device-detect";
import { import {
FaList, FaList,
@ -44,6 +45,9 @@ import {
} from "react-icons/fa"; } from "react-icons/fa";
import { PiSlidersHorizontalFill } from "react-icons/pi"; import { PiSlidersHorizontalFill } from "react-icons/pi";
import useSWR from "swr"; import useSWR from "swr";
import useSWRInfinite from "swr/infinite";
const API_LIMIT = 100;
export default function SubmitPlus() { export default function SubmitPlus() {
const { data: config } = useSWR<FrigateConfig>("config"); const { data: config } = useSWR<FrigateConfig>("config");
@ -64,21 +68,93 @@ export default function SubmitPlus() {
// data // data
const { data: events, mutate: refresh } = useSWR<Event[]>([ const eventFetcher = useCallback((key: string) => {
"events", const [path, params] = Array.isArray(key) ? key : [key, undefined];
{ return axios.get(path, { params }).then((res) => res.data);
limit: 100, }, []);
in_progress: 0,
is_submitted: 0, const getKey = useCallback(
cameras: selectedCameras ? selectedCameras.join(",") : null, (index: number, prevData: Event[]) => {
labels: selectedLabels ? selectedLabels.join(",") : null, if (index > 0) {
min_score: scoreRange ? scoreRange[0] : null, const lastDate = prevData[prevData.length - 1].start_time;
max_score: scoreRange ? scoreRange[1] : null, return [
sort: sort ? sort : null, "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<Event[]>(getKey, eventFetcher, {
revalidateOnFocus: false,
});
const events = useMemo(
() => (eventPages ? eventPages.flat() : []),
[eventPages],
);
const [upload, setUpload] = useState<Event>(); const [upload, setUpload] = useState<Event>();
// paging
const isDone = useMemo(
() => (eventPages?.at(-1)?.length ?? 0) < API_LIMIT,
[eventPages],
);
const pagingObserver = useRef<IntersectionObserver | null>();
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(() => { const grow = useMemo(() => {
if (!config || !upload) { if (!config || !upload) {
return ""; return "";
@ -110,18 +186,35 @@ export default function SubmitPlus() {
}); });
refresh( refresh(
(data: Event[] | undefined) => { (data: Event[][] | undefined) => {
if (!data) { if (!data) {
return 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) { if (index == -1) {
return data; 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 }, { revalidate: false, populateCache: true },
); );
@ -182,14 +275,17 @@ export default function SubmitPlus() {
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{events?.map((event) => { {events?.map((event, eIdx) => {
if (event.data.type != "object") { if (event.data.type != "object") {
return; return;
} }
const lastRow = eIdx == events.length - 1;
return ( return (
<div <div
key={event.id} key={event.id}
ref={lastRow ? lastEventRef : null}
className="w-full relative rounded-lg md:rounded-2xl aspect-video flex justify-center items-center bg-black cursor-pointer" className="w-full relative rounded-lg md:rounded-2xl aspect-video flex justify-center items-center bg-black cursor-pointer"
onClick={() => setUpload(event)} onClick={() => setUpload(event)}
> >
@ -228,6 +324,8 @@ export default function SubmitPlus() {
</div> </div>
); );
})} })}
{isValidating && <ActivityIndicator />}
</div> </div>
</div> </div>
</div> </div>