Skip to content

Commit

Permalink
Add support for review information side panel (#13063)
Browse files Browse the repository at this point in the history
  • Loading branch information
NickM-27 committed Aug 14, 2024
1 parent 99fb3e7 commit bdb50ba
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 38 deletions.
20 changes: 20 additions & 0 deletions frigate/api/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,26 @@ def events():
return jsonify(list(events))


@EventBp.route("/event_ids")
def event_ids():
idString = request.args.get("ids")
ids = idString.split(",")

if not ids:
return make_response(
jsonify({"success": False, "message": "Valid list of ids must be sent"}),
400,
)

try:
events = Event.select().where(Event.id << ids).dicts().iterator()
return jsonify(list(events))
except Exception:
return make_response(
jsonify({"success": False, "message": "Events not found"}), 400
)


@EventBp.route("/events/search")
def events_search():
query = request.args.get("query", type=str)
Expand Down
154 changes: 154 additions & 0 deletions web/src/components/overlay/detail/ReviewDetailDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
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";
import { Event } from "@/types/event";
import { useMemo } from "react";

type ReviewDetailDialogProps = {
review?: ReviewSegment;
setReview: (review: ReviewSegment | undefined) => void;
};
export default function ReviewDetailDialog({
review,
setReview,
}: ReviewDetailDialogProps) {
const { data: config } = useSWR<FrigateConfig>("config", {
revalidateOnFocus: false,
});

const apiHost = useApiHost();

// data

const { data: events } = useSWR<Event[]>(
review ? ["event_ids", { ids: review.data.detections.join(",") }] : null,
);

const hasMismatch = useMemo(() => {
if (!review || !events) {
return false;
}

return events.length != review?.data.detections.length;
}, [review, events]);

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 (
<Overlay
open={review != undefined}
onOpenChange={(open) => {
if (!open) {
setReview(undefined);
}
}}
>
<Content
className={
isDesktop ? "sm:max-w-xl" : "max-h-[75dvh] overflow-hidden p-2 pb-4"
}
>
{review && (
<div className="mt-3 flex size-full flex-col gap-5 md:mt-0">
<div className="flex w-full flex-row">
<div className="flex w-full flex-col gap-3">
<div className="flex flex-col gap-1.5">
<div className="text-sm text-primary/40">Camera</div>
<div className="text-sm capitalize">
{review.camera.replaceAll("_", " ")}
</div>
</div>
<div className="flex flex-col gap-1.5">
<div className="text-sm text-primary/40">Timestamp</div>
<div className="text-sm">{formattedDate}</div>
</div>
</div>
<div className="flex w-full flex-col gap-2 px-6">
<div className="flex flex-col gap-1.5">
<div className="text-sm text-primary/40">Objects</div>
<div className="flex flex-col items-start gap-2 text-sm capitalize">
{events?.map((event) => {
return (
<div
key={event.id}
className="flex flex-row items-center gap-2 text-sm capitalize"
>
{getIconForLabel(event.label, "size-3 text-white")}
{event.sub_label ?? event.label} (
{Math.round(event.data.top_score * 100)}%)
</div>
);
})}
</div>
</div>
{review.data.zones.length > 0 && (
<div className="flex flex-col gap-1.5">
<div className="text-sm text-primary/40">Zones</div>
<div className="flex flex-col items-start gap-2 text-sm capitalize">
{review.data.zones.map((zone) => {
return (
<div
key={zone}
className="flex flex-row items-center gap-2 text-sm capitalize"
>
{zone.replaceAll("_", " ")}
</div>
);
})}
</div>
</div>
)}
</div>
</div>
{hasMismatch && (
<div className="p-4 text-center text-sm">
Some objects that were detected are not included in this list
because the object does not have a snapshot
</div>
)}
<div className="scrollbar-container flex w-full flex-col gap-2 overflow-y-auto px-6">
{events?.map((event) => {
return (
<img
key={event.id}
className="aspect-video select-none rounded-lg object-contain transition-opacity"
style={
isIOS
? {
WebkitUserSelect: "none",
WebkitTouchCallout: "none",
}
: undefined
}
draggable={false}
src={
event.has_snapshot
? `${apiHost}api/events/${event.id}/snapshot.jpg`
: `${apiHost}api/events/${event.id}/thumbnail.jpg`
}
/>
);
})}
</div>
</div>
)}
</Content>
</Overlay>
);
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
8 changes: 5 additions & 3 deletions web/src/components/player/PreviewThumbnailPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -50,7 +50,7 @@ export default function PreviewThumbnailPlayer({
const handleOnClick = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
if (!ignoreClick) {
onClick(review, e.metaKey);
onClick(review, e.metaKey, false);
}
},
[ignoreClick, review, onClick],
Expand All @@ -73,7 +73,7 @@ export default function PreviewThumbnailPlayer({
});

useContextMenu(imgRef, () => {
onClick(review, true);
onClick(review, true, false);
});

// playback
Expand Down Expand Up @@ -237,6 +237,7 @@ export default function PreviewThumbnailPlayer({
<>
<Chip
className={`flex items-start justify-between space-x-1 ${playingBack ? "hidden" : ""} bg-gradient-to-br ${review.has_been_reviewed ? "bg-green-600 from-green-600 to-green-700" : "bg-gray-500 from-gray-400 to-gray-500"} z-0`}
onClick={() => onClick(review, false, true)}
>
{review.data.objects.sort().map((object) => {
return getIconForLabel(object, "size-3 text-white");
Expand Down Expand Up @@ -265,6 +266,7 @@ export default function PreviewThumbnailPlayer({
.sort()
.join(", ")
.replaceAll("-verified", "")}
{` Click To View Detection Details`}
</TooltipContent>
</Tooltip>
</div>
Expand Down
32 changes: 3 additions & 29 deletions web/src/components/player/SearchThumbnailPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import { capitalizeFirstLetter } from "@/utils/stringUtil";
import { VideoPreview } from "../preview/ScrubbablePreview";
import { Preview } from "@/types/preview";
import { SearchResult } from "@/types/search";
import { LuInfo } from "react-icons/lu";
import useContextMenu from "@/hooks/use-contextmenu";
import { cn } from "@/lib/utils";

Expand Down Expand Up @@ -212,6 +211,7 @@ export default function SearchThumbnailPlayer({
<>
<Chip
className={`flex items-start justify-between space-x-1 ${playingBack ? "hidden" : ""} "bg-gray-500 z-0 bg-gradient-to-br from-gray-400 to-gray-500`}
onClick={() => onClick(searchResult, true)}
>
{getIconForLabel(
searchResult.label,
Expand All @@ -231,34 +231,8 @@ export default function SearchThumbnailPlayer({
.map((text) => capitalizeFirstLetter(text))
.sort()
.join(", ")
.replaceAll("-verified", "")}
</TooltipContent>
</Tooltip>
</div>
<div className="absolute right-0 top-2 z-40">
<Tooltip>
<div
className="flex"
onMouseEnter={() => setTooltipHovering(true)}
onMouseLeave={() => setTooltipHovering(false)}
>
<TooltipTrigger asChild>
<div className="mx-3 pb-1 text-sm text-white">
{
<>
<Chip
className={`flex items-start justify-between space-x-1 ${playingBack ? "hidden" : ""} "bg-gray-500 z-0 bg-gradient-to-br from-gray-400 to-gray-500`}
onClick={() => onClick(searchResult, true)}
>
<LuInfo className="size-3" />
</Chip>
</>
}
</div>
</TooltipTrigger>
</div>
<TooltipContent className="capitalize">
View Detection Details
.replaceAll("-verified", "")}{" "}
{` Click To View Detection Details`}
</TooltipContent>
</Tooltip>
</div>
Expand Down
19 changes: 18 additions & 1 deletion web/src/views/events/EventView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -464,6 +465,10 @@ function DetectionReview({

const segmentDuration = 60;

// detail

const [reviewDetail, setReviewDetail] = useState<ReviewSegment>();

// preview

const [previewTime, setPreviewTime] = useState<number>();
Expand Down Expand Up @@ -618,6 +623,8 @@ function DetectionReview({

return (
<>
<ReviewDetailDialog review={reviewDetail} setReview={setReviewDetail} />

<div
ref={contentRef}
className="no-scrollbar flex flex-1 flex-wrap content-start gap-2 overflow-y-auto md:gap-4"
Expand Down Expand Up @@ -672,7 +679,17 @@ function DetectionReview({
setReviewed={markItemAsReviewed}
scrollLock={scrollLock}
onTimeUpdate={onPreviewTimeUpdate}
onClick={onSelectReview}
onClick={(
review: ReviewSegment,
ctrl: boolean,
detail: boolean,
) => {
if (detail) {
setReviewDetail(review);
} else {
onSelectReview(review, ctrl);
}
}}
/>
</div>
<div
Expand Down
2 changes: 1 addition & 1 deletion web/src/views/search/SearchView.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import SearchFilterGroup from "@/components/filter/SearchFilterGroup";
import ActivityIndicator from "@/components/indicators/activity-indicator";
import SearchDetailDialog from "@/components/overlay/SearchDetailDialog";
import SearchDetailDialog from "@/components/overlay/detail/SearchDetailDialog";
import SearchThumbnailPlayer from "@/components/player/SearchThumbnailPlayer";
import { Input } from "@/components/ui/input";
import { Toaster } from "@/components/ui/sonner";
Expand Down

0 comments on commit bdb50ba

Please sign in to comment.