mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-10-13 11:16:29 +02:00
* 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
188 lines
6.5 KiB
TypeScript
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;
|