import React, { useCallback, useEffect, useMemo, useState } from "react"; import { useApiHost } from "@/api"; import { isCurrentHour } from "@/utils/dateUtil"; import { ReviewSegment } from "@/types/review"; import { getIconForLabel } from "@/utils/iconUtil"; import TimeAgo from "../dynamic/TimeAgo"; import useSWR from "swr"; import { FrigateConfig } from "@/types/frigateConfig"; import { isIOS, isMobile, isSafari } from "react-device-detect"; import Chip from "@/components/indicators/Chip"; import { useFormattedTimestamp } from "@/hooks/use-date-utils"; import useImageLoaded from "@/hooks/use-image-loaded"; import { useSwipeable } from "react-swipeable"; import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; import ImageLoadingIndicator from "../indicators/ImageLoadingIndicator"; import useContextMenu from "@/hooks/use-contextmenu"; import ActivityIndicator from "../indicators/activity-indicator"; import { TimeRange } from "@/types/timeline"; import { capitalizeFirstLetter } from "@/utils/stringUtil"; import { cn } from "@/lib/utils"; import { InProgressPreview, VideoPreview } from "../preview/ScrubbablePreview"; import { Preview } from "@/types/preview"; import { baseUrl } from "@/api/baseUrl"; import { useTranslation } from "react-i18next"; type PreviewPlayerProps = { review: ReviewSegment; allPreviews?: Preview[]; scrollLock?: boolean; timeRange: TimeRange; onTimeUpdate?: (time: number | undefined) => void; setReviewed: (review: ReviewSegment) => void; onClick: (review: ReviewSegment, ctrl: boolean, detail: boolean) => void; }; export default function PreviewThumbnailPlayer({ review, allPreviews, scrollLock = false, timeRange, setReviewed, onClick, onTimeUpdate, }: PreviewPlayerProps) { const { t } = useTranslation(["components/player"]); const apiHost = useApiHost(); const { data: config } = useSWR("config"); const [imgRef, imgLoaded, onImgLoad] = useImageLoaded(); // interaction const [ignoreClick, setIgnoreClick] = useState(false); const handleOnClick = useCallback( (e: React.MouseEvent) => { if (!ignoreClick) { onClick(review, e.metaKey, false); } }, [ignoreClick, review, onClick], ); const handleSetReviewed = useCallback(() => { if (review.end_time && !review.has_been_reviewed) { review.has_been_reviewed = true; setReviewed(review); } }, [review, setReviewed]); const swipeHandlers = useSwipeable({ onSwipedLeft: () => { setPlayback(false); handleSetReviewed(); }, onSwipedRight: () => setPlayback(true), preventScrollOnSwipe: true, }); useContextMenu(imgRef, () => { onClick(review, true, false); }); // playback const relevantPreview = useMemo(() => { if (!allPreviews) { return undefined; } let multiHour = false; const firstIndex = Object.values(allPreviews).findIndex((preview) => { if (preview.camera != review.camera || preview.end < review.start_time) { return false; } if ((review.end_time ?? timeRange.before) > preview.end) { multiHour = true; } return true; }); if (firstIndex == -1) { return undefined; } if (!multiHour) { return allPreviews[firstIndex]; } const firstPrev = allPreviews[firstIndex]; const firstDuration = firstPrev.end - review.start_time; const secondDuration = (review.end_time ?? timeRange.before) - firstPrev.end; if (firstDuration > secondDuration) { // the first preview is longer than the second, return the first return firstPrev; } else { // the second preview is longer, return the second if it exists if (firstIndex < allPreviews.length - 1) { return allPreviews.find( (preview, idx) => idx > firstIndex && preview.camera == review.camera, ); } return undefined; } }, [allPreviews, review, timeRange]); // Hover Playback const [hoverTimeout, setHoverTimeout] = useState(); const [playback, setPlayback] = useState(false); const [tooltipHovering, setTooltipHovering] = useState(false); const playingBack = useMemo( () => playback && !tooltipHovering, [playback, tooltipHovering], ); const [isHovered, setIsHovered] = useState(false); useEffect(() => { if (isHovered && scrollLock) { return; } if (isHovered && !tooltipHovering) { setHoverTimeout( setTimeout(() => { setPlayback(true); setHoverTimeout(null); }, 500), ); } else { if (hoverTimeout) { clearTimeout(hoverTimeout); } setPlayback(false); if (onTimeUpdate) { onTimeUpdate(undefined); } } // we know that these deps are correct // eslint-disable-next-line react-hooks/exhaustive-deps }, [isHovered, scrollLock, tooltipHovering]); // date const formattedDate = useFormattedTimestamp( review.start_time, config?.ui.time_format == "24hour" ? t("time.formattedTimestampMonthDayHourMinute.24hour", { ns: "common" }) : t("time.formattedTimestampMonthDayHourMinute.12hour", { ns: "common" }), config?.ui?.timezone, ); return (
setIsHovered(true)} onMouseLeave={isMobile ? undefined : () => setIsHovered(false)} onClick={handleOnClick} onAuxClick={(e) => { if (e.button === 1) { window.open(`${baseUrl}review?id=${review.id}`, "_blank")?.focus(); } }} {...swipeHandlers} > {playingBack && (
)}
{ onImgLoad(); }} /> {!playingBack && (
)}
setTooltipHovering(true)} onMouseLeave={() => setTooltipHovering(false)} >
{(review.severity == "alert" || review.severity == "detection") && ( <> onClick(review, false, true)} > {review.data.objects.sort().map((object) => { return getIconForLabel(object, "size-3 text-white"); })} {review.data.audio.map((audio) => { return getIconForLabel(audio, "size-3 text-white"); })} )}
{[ ...new Set([ ...(review.data.objects || []), ...(review.data.sub_labels || []), ...(review.data.audio || []), ]), ] .filter( (item) => item !== undefined && !item.includes("-verified"), ) .map((text) => capitalizeFirstLetter(text)) .sort() .join(", ") .replaceAll("-verified", "")}
{!playingBack && (
{review.end_time ? ( ) : (
)} {formattedDate}
)}
); } type PreviewContentProps = { review: ReviewSegment; relevantPreview: Preview | undefined; timeRange: TimeRange; setReviewed: () => void; setIgnoreClick: (ignore: boolean) => void; isPlayingBack: (ended: boolean) => void; onTimeUpdate?: (time: number | undefined) => void; }; function PreviewContent({ review, relevantPreview, timeRange, setReviewed, setIgnoreClick, isPlayingBack, onTimeUpdate, }: PreviewContentProps) { // preview if (relevantPreview) { return ( ); } else if (isCurrentHour(review.start_time)) { return ( ); } }