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("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(getKey, timelineFetcher); const { data: allPreviews } = useSWR( 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(); 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(); 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 ; } return ( <> Review setPlayback(undefined)} />
{Object.entries(timelineCards) .reverse() .map(([day, timelineDay], dayIdx) => { return (
{formatUnixTimestampToDateTime(parseInt(day), { strftime_fmt: "%A %b %d", time_style: "medium", date_style: "medium", })} {Object.entries(timelineDay).map( ([hour, timelineHour], hourIdx) => { if (Object.values(timelineHour).length == 0) { return
; } const lastRow = dayIdx == Object.values(timelineCards).length - 1 && hourIdx == Object.values(timelineDay).length - 1; const previewMap: { [key: string]: Preview | undefined } = {}; return (
{formatUnixTimestampToDateTime(parseInt(hour), { strftime_fmt: config.ui.time_format == "24hour" ? "%H:00" : "%I:00 %p", time_style: "medium", date_style: "medium", })}
{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 ( { setPlayback(timeline); }} /> ); })}
{lastRow && }
); } )}
); })}
); } export default History;