mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
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:
parent
e5e18a5026
commit
fc0f6d6095
@ -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"),
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user