import { useCallback, useMemo, 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 axios from "axios"; import { getHourlyTimelineData } from "@/utils/historyUtil"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import HistoryFilterPopover from "@/components/filter/HistoryFilterPopover"; import useApiFilter from "@/hooks/use-api-filter"; import HistoryCardView from "@/views/history/HistoryCardView"; import { Button } from "@/components/ui/button"; import { IoMdArrowBack } from "react-icons/io"; import useOverlayState from "@/hooks/use-overlay-state"; import { useNavigate } from "react-router-dom"; import { Dialog, DialogContent } from "@/components/ui/dialog"; import MobileTimelineView from "@/views/history/MobileTimelineView"; import DesktopTimelineView from "@/views/history/DesktopTimelineView"; const API_LIMIT = 200; function History() { const { data: config } = useSWR("config"); const timezone = useMemo( () => config?.ui?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone, [config] ); const [historyFilter, setHistoryFilter, historySearchParams] = useApiFilter(); 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 = historySearchParams == undefined ? { before: lastDate, timezone, limit: API_LIMIT } : { ...historySearchParams, before: lastDate, timezone, limit: API_LIMIT, }; return ["timeline/hourly", pagedParams]; } const params = historySearchParams == undefined ? { timezone, limit: API_LIMIT } : { ...historySearchParams, timezone, limit: API_LIMIT }; return ["timeline/hourly", params]; }, [historySearchParams] ); const { data: timelinePages, mutate: updateHistory, size, setSize, isValidating, } = useSWRInfinite(getKey, timelineFetcher); const previewTimes = useMemo(() => { if (!timelinePages) { return undefined; } const startDate = new Date(); startDate.setMinutes(0, 0, 0); const endDate = new Date(timelinePages.at(-1)!!.end); endDate.setHours(0, 0, 0, 0); return { start: startDate.getTime() / 1000, end: endDate.getTime() / 1000, }; }, [timelinePages]); const { data: allPreviews } = useSWR( previewTimes ? `preview/all/start/${previewTimes.start}/end/${previewTimes.end}` : null, { revalidateOnFocus: false } ); const navigate = useNavigate(); const [playback, setPlayback] = useState(); const [viewingPlayback, setViewingPlayback] = useOverlayState("timeline"); const setPlaybackState = useCallback( (playback: TimelinePlayback | undefined) => { if (playback == undefined) { setPlayback(undefined); navigate(-1); } else { setPlayback(playback); setViewingPlayback(true); } }, [navigate] ); const isMobile = useMemo(() => { return window.innerWidth < 768; }, [playback]); const timelineCards: CardsData = useMemo(() => { if (!timelinePages) { return {}; } return getHourlyTimelineData( timelinePages, historyFilter?.detailLevel ?? "normal" ); }, [historyFilter, timelinePages]); const isDone = (timelinePages?.[timelinePages.length - 1]?.count ?? 0) < API_LIMIT; 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) { return ; } return ( <>
{viewingPlayback && ( )} History
{!playback && ( setHistoryFilter(filter)} /> )}
setItemsToDelete(null)} > {`Delete ${itemsToDelete?.length} events?`} This will delete all events associated with these objects. setItemsToDelete(null)}> Cancel onDeleteMulti()} > Delete { setSize(size + 1); }} onDelete={onDelete} onItemSelected={(item) => setPlaybackState(item)} /> setPlaybackState(undefined)} /> ); } type TimelineViewerProps = { timelineData: CardsData | undefined; allPreviews: Preview[]; playback: TimelinePlayback | undefined; isMobile: boolean; onClose: () => void; }; function TimelineViewer({ timelineData, allPreviews, playback, isMobile, onClose, }: TimelineViewerProps) { if (isMobile) { return playback != undefined ? (
{timelineData && }
) : null; } return ( onClose()}> {timelineData && playback && ( )} ); } export default History;