diff --git a/web/src/components/overlay/detail/ReviewDetailDialog.tsx b/web/src/components/overlay/detail/ReviewDetailDialog.tsx new file mode 100644 index 000000000..db7c372b0 --- /dev/null +++ b/web/src/components/overlay/detail/ReviewDetailDialog.tsx @@ -0,0 +1,122 @@ +import { isDesktop, isIOS } from "react-device-detect"; +import { Sheet, SheetContent } from "../../ui/sheet"; +import { Drawer, DrawerContent } from "../../ui/drawer"; +import useSWR from "swr"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { useFormattedTimestamp } from "@/hooks/use-date-utils"; +import { getIconForLabel } from "@/utils/iconUtil"; +import { useApiHost } from "@/api"; +import { ReviewSegment } from "@/types/review"; + +type ReviewDetailDialogProps = { + review?: ReviewSegment; + setReview: (review: ReviewSegment | undefined) => void; +}; +export default function ReviewDetailDialog({ + review, + setReview, +}: ReviewDetailDialogProps) { + const { data: config } = useSWR("config", { + revalidateOnFocus: false, + }); + + const apiHost = useApiHost(); + + // data + + const formattedDate = useFormattedTimestamp( + review?.start_time ?? 0, + config?.ui.time_format == "24hour" + ? "%b %-d %Y, %H:%M" + : "%b %-d %Y, %I:%M %p", + ); + + // content + + const Overlay = isDesktop ? Sheet : Drawer; + const Content = isDesktop ? SheetContent : DrawerContent; + + return ( + { + if (!open) { + setReview(undefined); + } + }} + > + + {review && ( +
+
+
+
+
Labels
+
+ {[ + ...new Set([ + ...(review.data.objects || []), + ...(review.data.sub_labels || []), + ...(review.data.audio || []), + ]), + ] + .filter( + (item) => + item !== undefined && !item.includes("-verified"), + ) + .sort() + .map((obj) => { + return ( +
+ {getIconForLabel(obj, "size-3 text-white")} + {obj} +
+ ); + })} +
+
+
+
Camera
+
+ {review.camera.replaceAll("_", " ")} +
+
+
+
Timestamp
+
{formattedDate}
+
+
+
+ {review.data.detections.map((eventId) => { + return ( + + ); + })} +
+
+
+ )} +
+
+ ); +} diff --git a/web/src/components/overlay/SearchDetailDialog.tsx b/web/src/components/overlay/detail/SearchDetailDialog.tsx similarity index 96% rename from web/src/components/overlay/SearchDetailDialog.tsx rename to web/src/components/overlay/detail/SearchDetailDialog.tsx index 1e9dd8c2a..4a726a246 100644 --- a/web/src/components/overlay/SearchDetailDialog.tsx +++ b/web/src/components/overlay/detail/SearchDetailDialog.tsx @@ -1,17 +1,17 @@ import { isDesktop, isIOS } from "react-device-detect"; -import { Sheet, SheetContent } from "../ui/sheet"; -import { Drawer, DrawerContent } from "../ui/drawer"; +import { Sheet, SheetContent } from "../../ui/sheet"; +import { Drawer, DrawerContent } from "../../ui/drawer"; import { SearchResult } from "@/types/search"; import useSWR from "swr"; import { FrigateConfig } from "@/types/frigateConfig"; import { useFormattedTimestamp } from "@/hooks/use-date-utils"; import { getIconForLabel } from "@/utils/iconUtil"; import { useApiHost } from "@/api"; -import { Button } from "../ui/button"; +import { Button } from "../../ui/button"; import { useCallback, useEffect, useState } from "react"; import axios from "axios"; import { toast } from "sonner"; -import { Textarea } from "../ui/textarea"; +import { Textarea } from "../../ui/textarea"; type SearchDetailDialogProps = { search?: SearchResult; diff --git a/web/src/components/player/PreviewThumbnailPlayer.tsx b/web/src/components/player/PreviewThumbnailPlayer.tsx index 99f2e9413..2d91c5e3d 100644 --- a/web/src/components/player/PreviewThumbnailPlayer.tsx +++ b/web/src/components/player/PreviewThumbnailPlayer.tsx @@ -28,7 +28,7 @@ type PreviewPlayerProps = { timeRange: TimeRange; onTimeUpdate?: (time: number | undefined) => void; setReviewed: (review: ReviewSegment) => void; - onClick: (review: ReviewSegment, ctrl: boolean) => void; + onClick: (review: ReviewSegment, ctrl: boolean, detail: boolean) => void; }; export default function PreviewThumbnailPlayer({ @@ -50,7 +50,7 @@ export default function PreviewThumbnailPlayer({ const handleOnClick = useCallback( (e: React.MouseEvent) => { if (!ignoreClick) { - onClick(review, e.metaKey); + onClick(review, e.metaKey, false); } }, [ignoreClick, review, onClick], @@ -73,7 +73,7 @@ export default function PreviewThumbnailPlayer({ }); useContextMenu(imgRef, () => { - onClick(review, true); + onClick(review, true, false); }); // playback @@ -237,6 +237,7 @@ export default function PreviewThumbnailPlayer({ <> onClick(review, false, true)} > {review.data.objects.sort().map((object) => { return getIconForLabel(object, "size-3 text-white"); diff --git a/web/src/components/player/SearchThumbnailPlayer.tsx b/web/src/components/player/SearchThumbnailPlayer.tsx index 12e7cfd4f..84050e792 100644 --- a/web/src/components/player/SearchThumbnailPlayer.tsx +++ b/web/src/components/player/SearchThumbnailPlayer.tsx @@ -212,6 +212,7 @@ export default function SearchThumbnailPlayer({ <> onClick(searchResult, true)} > {getIconForLabel( searchResult.label, @@ -231,7 +232,8 @@ export default function SearchThumbnailPlayer({ .map((text) => capitalizeFirstLetter(text)) .sort() .join(", ") - .replaceAll("-verified", "")} + .replaceAll("-verified", "")}{" "} + Click To View Detection Details @@ -257,9 +259,7 @@ export default function SearchThumbnailPlayer({ - - View Detection Details - + {!playingBack && ( diff --git a/web/src/views/events/EventView.tsx b/web/src/views/events/EventView.tsx index a9df87ac5..36913aa60 100644 --- a/web/src/views/events/EventView.tsx +++ b/web/src/views/events/EventView.tsx @@ -52,6 +52,7 @@ import { cn } from "@/lib/utils"; import { FilterList, LAST_24_HOURS_KEY } from "@/types/filter"; import { GiSoundWaves } from "react-icons/gi"; import useKeyboardListener from "@/hooks/use-keyboard-listener"; +import ReviewDetailDialog from "@/components/overlay/detail/ReviewDetailDialog"; type EventViewProps = { reviewItems?: SegmentedReviewData; @@ -461,6 +462,10 @@ function DetectionReview({ const segmentDuration = 60; + // detail + + const [reviewDetail, setReviewDetail] = useState(); + // preview const [previewTime, setPreviewTime] = useState(); @@ -615,6 +620,8 @@ function DetectionReview({ return ( <> + +
{ + if (detail) { + setReviewDetail(review); + } else { + onSelectReview(review, ctrl); + } + }} />