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"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; 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, mutate: updateHistory, 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] ); const [itemsToDelete, setItemsToDelete] = useState(null); const onDelete = useCallback( async (timeline: Card) => { if (timeline.entries.length > 1) { const uniqueEvents = new Set( timeline.entries.map((entry) => entry.source_id) ); setItemsToDelete(new Array(...uniqueEvents)); } else { const response = await axios.delete( `events/${timeline.entries[0].source_id}` ); if (response.status === 200) { updateHistory(); } } }, [updateHistory] ); const onDeleteMulti = useCallback(async () => { if (!itemsToDelete) { return; } const responses = itemsToDelete.map(async (id) => { return axios.delete(`events/${id}`); }); if ((await responses[0]).status == 200) { updateHistory(); setItemsToDelete(null); } }, [itemsToDelete, updateHistory]); if (!config || !timelineCards || timelineCards.length == 0) { return ; } return ( <> Review setItemsToDelete(null)} > {`Delete ${itemsToDelete?.length} events?`} This will delete all events associated with these objects. setItemsToDelete(null)}> Cancel onDeleteMulti()} > Delete 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); }} onDelete={() => onDelete(timeline)} /> ); })}
{lastRow && }
); } )}
); })}
); } export default History;