blakeblackshear.frigate/web/src/pages/History.tsx
Nicolas Mowen f8d114cd33 Webui cleanups (#8991)
* Fix mobile event timeago

* Reduce preview playback rate for safari browser

* Fix dashboard buttons

* Update recent events correctly

* Fix opening page on icon toggle

* Fix video player remote playback check

* fix history image

* Add sticky headers to history page

* Fix iOS empty frame

* reduce duplicate items and improve time format

* Organize data more effictively and ensure data is not overwritten

* Use icon to indicate preview
2024-01-31 12:56:11 +00:00

188 lines
6.5 KiB
TypeScript

import { useCallback, useMemo, useRef, useState } from "react";
import useSWR from "swr";
import useSWRInfinite from "swr/infinite";
import { FrigateConfig } from "@/types/frigateConfig";
import Heading from "@/components/ui/heading";
import ActivityIndicator from "@/components/ui/activity-indicator";
import HistoryCard from "@/components/card/HistoryCard";
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
import axios from "axios";
import TimelinePlayerCard from "@/components/card/TimelinePlayerCard";
import { getHourlyTimelineData } from "@/utils/historyUtil";
const API_LIMIT = 200;
function History() {
const { data: config } = useSWR<FrigateConfig>("config");
const timezone = useMemo(
() =>
config?.ui?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
[config]
);
const timelineFetcher = useCallback((key: any) => {
const [path, params] = Array.isArray(key) ? key : [key, undefined];
return axios.get(path, { params }).then((res) => res.data);
}, []);
const getKey = useCallback((index: number, prevData: HourlyTimeline) => {
if (index > 0) {
const lastDate = prevData.end;
const pagedParams = { before: lastDate, timezone, limit: API_LIMIT };
return ["timeline/hourly", pagedParams];
}
return ["timeline/hourly", { timezone, limit: API_LIMIT }];
}, []);
const {
data: timelinePages,
size,
setSize,
isValidating,
} = useSWRInfinite<HourlyTimeline>(getKey, timelineFetcher);
const { data: allPreviews } = useSWR<Preview[]>(
timelinePages
? `preview/all/start/${timelinePages?.at(0)
?.start}/end/${timelinePages?.at(-1)?.end}`
: null,
{ revalidateOnFocus: false }
);
const [detailLevel, _] = useState<"normal" | "extra" | "full">("normal");
const [playback, setPlayback] = useState<Card | undefined>();
const shouldAutoPlay = useMemo(() => {
return playback == undefined && window.innerWidth < 480;
}, [playback]);
const timelineCards: CardsData | never[] = useMemo(() => {
if (!timelinePages) {
return [];
}
return getHourlyTimelineData(timelinePages, detailLevel);
}, [detailLevel, timelinePages]);
const isDone =
(timelinePages?.[timelinePages.length - 1]?.count ?? 0) < API_LIMIT;
// hooks for infinite scroll
const observer = useRef<IntersectionObserver | null>();
const lastTimelineRef = useCallback(
(node: HTMLElement | null) => {
if (isValidating) return;
if (observer.current) observer.current.disconnect();
try {
observer.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && !isDone) {
setSize(size + 1);
}
});
if (node) observer.current.observe(node);
} catch (e) {
// no op
}
},
[size, setSize, isValidating, isDone]
);
if (!config || !timelineCards || timelineCards.length == 0) {
return <ActivityIndicator />;
}
return (
<>
<Heading as="h2">Review</Heading>
<TimelinePlayerCard
timeline={playback}
onDismiss={() => setPlayback(undefined)}
/>
<div>
{Object.entries(timelineCards)
.reverse()
.map(([day, timelineDay], dayIdx) => {
return (
<div key={day}>
<Heading
className="sticky py-2 -top-4 left-0 bg-background w-full z-10"
as="h3"
>
{formatUnixTimestampToDateTime(parseInt(day), {
strftime_fmt: "%A %b %d",
time_style: "medium",
date_style: "medium",
})}
</Heading>
{Object.entries(timelineDay).map(
([hour, timelineHour], hourIdx) => {
if (Object.values(timelineHour).length == 0) {
return <div key={hour}></div>;
}
const lastRow =
dayIdx == Object.values(timelineCards).length - 1 &&
hourIdx == Object.values(timelineDay).length - 1;
const previewMap: { [key: string]: Preview | undefined } =
{};
return (
<div key={hour} ref={lastRow ? lastTimelineRef : null}>
<Heading as="h4">
{formatUnixTimestampToDateTime(parseInt(hour), {
strftime_fmt:
config.ui.time_format == "24hour"
? "%H:00"
: "%I:00 %p",
time_style: "medium",
date_style: "medium",
})}
</Heading>
<div className="flex flex-wrap">
{Object.entries(timelineHour)
.reverse()
.map(([key, timeline]) => {
const startTs = Object.values(timeline.entries)[0]
.timestamp;
let relevantPreview = previewMap[timeline.camera];
if (relevantPreview == undefined) {
relevantPreview = previewMap[timeline.camera] =
Object.values(allPreviews || []).find(
(preview) =>
preview.camera == timeline.camera &&
preview.start < startTs &&
preview.end > startTs
);
}
return (
<HistoryCard
key={key}
timeline={timeline}
shouldAutoPlay={shouldAutoPlay}
relevantPreview={relevantPreview}
onClick={() => {
setPlayback(timeline);
}}
/>
);
})}
</div>
{lastRow && <ActivityIndicator />}
</div>
);
}
)}
</div>
);
})}
</div>
</>
);
}
export default History;