Skip to content

Commit

Permalink
Explore layout changes (#14348)
Browse files Browse the repository at this point in the history
* Reset selected index on new searches

* Remove right click for similarity search

* Fix sub label icon

* add card footer

* Add Frigate+ dialog

* Move buttons and menu to thumbnail footer

* Add similarity search

* Show object score

* Implement download buttons

* remove confidence score

* conditionally show submenu items

* Implement delete

* fix icon color

* Add object lifecycle button

* fix score

* delete confirmation

* small tweaks

* consistent icons

---------

Co-authored-by: Nicolas Mowen <[email protected]>
  • Loading branch information
hawkeye217 and NickM-27 authored Oct 15, 2024
1 parent 0eccb6a commit 644069f
Show file tree
Hide file tree
Showing 7 changed files with 283 additions and 96 deletions.
57 changes: 28 additions & 29 deletions web/src/components/card/SearchThumbnail.tsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,56 @@
import { useCallback } from "react";
import { useCallback, useMemo } from "react";
import { useApiHost } from "@/api";
import { getIconForLabel } from "@/utils/iconUtil";
import TimeAgo from "../dynamic/TimeAgo";
import useSWR from "swr";
import { FrigateConfig } from "@/types/frigateConfig";
import { isIOS, 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 { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
import ImageLoadingIndicator from "../indicators/ImageLoadingIndicator";
import ActivityIndicator from "../indicators/activity-indicator";
import { capitalizeFirstLetter } from "@/utils/stringUtil";
import { SearchResult } from "@/types/search";
import useContextMenu from "@/hooks/use-contextmenu";
import { cn } from "@/lib/utils";
import { TooltipPortal } from "@radix-ui/react-tooltip";

type SearchThumbnailProps = {
searchResult: SearchResult;
findSimilar: () => void;
onClick: (searchResult: SearchResult) => void;
};

export default function SearchThumbnail({
searchResult,
findSimilar,
onClick,
}: SearchThumbnailProps) {
const apiHost = useApiHost();
const { data: config } = useSWR<FrigateConfig>("config");
const [imgRef, imgLoaded, onImgLoad] = useImageLoaded();

useContextMenu(imgRef, findSimilar);
// interactions

const handleOnClick = useCallback(() => {
onClick(searchResult);
}, [searchResult, onClick]);

// date
const objectLabel = useMemo(() => {
if (
!config ||
!searchResult.sub_label ||
!config.model.attributes_map[searchResult.label]
) {
return searchResult.label;
}

const formattedDate = useFormattedTimestamp(
searchResult.start_time,
config?.ui.time_format == "24hour" ? "%b %-d, %H:%M" : "%b %-d, %I:%M %p",
config?.ui.timezone,
);
if (
config.model.attributes_map[searchResult.label].includes(
searchResult.sub_label,
)
) {
return searchResult.sub_label;
}

return `${searchResult.label}-verified`;
}, [config, searchResult]);

return (
<div className="relative size-full cursor-pointer" onClick={handleOnClick}>
Expand Down Expand Up @@ -80,17 +86,21 @@ export default function SearchThumbnail({
<TooltipTrigger asChild>
<div className="mx-3 pb-1 text-sm text-white">
<Chip
className={`z-0 flex items-start justify-between space-x-1 bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500`}
className={`z-0 flex items-center justify-between gap-1 space-x-1 bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500 text-xs`}
onClick={() => onClick(searchResult)}
>
{getIconForLabel(searchResult.label, "size-3 text-white")}
{getIconForLabel(objectLabel, "size-3 text-white")}
{Math.floor(
searchResult.score ?? searchResult.data.top_score * 100,
)}
%
</Chip>
</div>
</TooltipTrigger>
</div>
<TooltipPortal>
<TooltipContent className="capitalize">
{[...new Set([searchResult.label])]
{[objectLabel]
.filter(
(item) => item !== undefined && !item.includes("-verified"),
)
Expand All @@ -103,18 +113,7 @@ export default function SearchThumbnail({
</Tooltip>
</div>
<div className="rounded-t-l pointer-events-none absolute inset-x-0 top-0 z-10 h-[30%] w-full bg-gradient-to-b from-black/60 to-transparent"></div>
<div className="rounded-b-l pointer-events-none absolute inset-x-0 bottom-0 z-10 h-[20%] w-full bg-gradient-to-t from-black/60 to-transparent">
<div className="mx-3 flex h-full items-end justify-between pb-1 text-sm text-white">
{searchResult.end_time ? (
<TimeAgo time={searchResult.start_time * 1000} dense />
) : (
<div>
<ActivityIndicator size={24} />
</div>
)}
{formattedDate}
</div>
</div>
<div className="rounded-b-l pointer-events-none absolute inset-x-0 bottom-0 z-10 flex h-[20%] items-end bg-gradient-to-t from-black/60 to-transparent"></div>
</div>
</div>
);
Expand Down
198 changes: 198 additions & 0 deletions web/src/components/card/SearchThumbnailFooter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import { useCallback, useState } from "react";
import TimeAgo from "../dynamic/TimeAgo";
import useSWR from "swr";
import { FrigateConfig } from "@/types/frigateConfig";
import { useFormattedTimestamp } from "@/hooks/use-date-utils";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
import ActivityIndicator from "../indicators/activity-indicator";
import { SearchResult } from "@/types/search";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "../ui/alert-dialog";
import { LuCamera, LuDownload, LuMoreVertical, LuTrash2 } from "react-icons/lu";
import FrigatePlusIcon from "@/components/icons/FrigatePlusIcon";
import { FrigatePlusDialog } from "../overlay/dialog/FrigatePlusDialog";
import { Event } from "@/types/event";
import { FaArrowsRotate } from "react-icons/fa6";
import { baseUrl } from "@/api/baseUrl";
import axios from "axios";
import { toast } from "sonner";
import { MdImageSearch } from "react-icons/md";

type SearchThumbnailProps = {
searchResult: SearchResult;
findSimilar: () => void;
refreshResults: () => void;
showObjectLifecycle: () => void;
};

export default function SearchThumbnailFooter({
searchResult,
findSimilar,
refreshResults,
showObjectLifecycle,
}: SearchThumbnailProps) {
const { data: config } = useSWR<FrigateConfig>("config");

// interactions

const [showFrigatePlus, setShowFrigatePlus] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);

const handleDelete = useCallback(() => {
axios
.delete(`events/${searchResult.id}`)
.then((resp) => {
if (resp.status == 200) {
toast.success("Tracked object deleted successfully.", {
position: "top-center",
});
refreshResults();
}
})
.catch(() => {
toast.error("Failed to delete tracked object.", {
position: "top-center",
});
});
}, [searchResult, refreshResults]);

// date

const formattedDate = useFormattedTimestamp(
searchResult.start_time,
config?.ui.time_format == "24hour" ? "%b %-d, %H:%M" : "%b %-d, %I:%M %p",
config?.ui.timezone,
);

return (
<>
<AlertDialog
open={deleteDialogOpen}
onOpenChange={() => setDeleteDialogOpen(!deleteDialogOpen)}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirm Delete</AlertDialogTitle>
</AlertDialogHeader>
<AlertDialogDescription>
Are you sure you want to delete this tracked object?
</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
className="bg-destructive"
onClick={handleDelete}
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<FrigatePlusDialog
upload={
showFrigatePlus ? (searchResult as unknown as Event) : undefined
}
onClose={() => setShowFrigatePlus(false)}
onEventUploaded={() => {}}
/>

<div className="flex flex-col items-start">
{searchResult.end_time ? (
<TimeAgo time={searchResult.start_time * 1000} dense />
) : (
<div>
<ActivityIndicator size={24} />
</div>
)}
{formattedDate}
</div>
<div className="flex flex-row items-center justify-end gap-8 md:gap-4">
{config?.plus?.enabled &&
searchResult.has_snapshot &&
searchResult.end_time && (
<Tooltip>
<TooltipTrigger>
<FrigatePlusIcon
className="size-5 cursor-pointer text-primary"
onClick={() => setShowFrigatePlus(true)}
/>
</TooltipTrigger>
<TooltipContent>Submit to Frigate+</TooltipContent>
</Tooltip>
)}

{config?.semantic_search?.enabled && (
<Tooltip>
<TooltipTrigger>
<MdImageSearch
className="size-5 cursor-pointer text-primary"
onClick={findSimilar}
/>
</TooltipTrigger>
<TooltipContent>Find similar</TooltipContent>
</Tooltip>
)}

<DropdownMenu>
<DropdownMenuTrigger>
<LuMoreVertical className="size-5 cursor-pointer text-primary" />
</DropdownMenuTrigger>
<DropdownMenuContent align={"end"}>
<DropdownMenuLabel className="mt-0.5">
Tracked Object Actions
</DropdownMenuLabel>
<DropdownMenuSeparator className="mt-1" />
{searchResult.has_clip && (
<DropdownMenuItem>
<a
className="justify_start flex items-center"
href={`${baseUrl}api/events/${searchResult.id}/clip.mp4`}
download={`${searchResult.camera}_${searchResult.label}.mp4`}
>
<LuDownload className="mr-2 size-4" />
<span>Download video</span>
</a>
</DropdownMenuItem>
)}
{searchResult.has_snapshot && (
<DropdownMenuItem>
<a
className="justify_start flex items-center"
href={`${baseUrl}api/events/${searchResult.id}/snapshot.jpg`}
download={`${searchResult.camera}_${searchResult.label}.jpg`}
>
<LuCamera className="mr-2 size-4" />
<span>Download snapshot</span>
</a>
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={showObjectLifecycle}>
<FaArrowsRotate className="mr-2 size-4" />
<span>View object lifecycle</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setDeleteDialogOpen(true)}>
<LuTrash2 className="mr-2 size-4" />
<span>Delete</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</>
);
}
6 changes: 3 additions & 3 deletions web/src/components/input/InputWithTags.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React, { useState, useRef, useEffect, useCallback } from "react";
import {
LuX,
LuFilter,
LuImage,
LuChevronDown,
LuChevronUp,
LuTrash2,
Expand Down Expand Up @@ -44,6 +43,7 @@ import {
import { toast } from "sonner";
import useSWR from "swr";
import { FrigateConfig } from "@/types/frigateConfig";
import { MdImageSearch } from "react-icons/md";

type InputWithTagsProps = {
inputFocused: boolean;
Expand Down Expand Up @@ -514,7 +514,7 @@ export default function InputWithTags({
onFocus={handleInputFocus}
onBlur={handleInputBlur}
onKeyDown={handleInputKeyDown}
className="text-md h-9 pr-24"
className="text-md h-9 pr-32"
placeholder="Search..."
/>
<div className="absolute right-3 top-0 flex h-full flex-row items-center justify-center gap-5">
Expand Down Expand Up @@ -549,7 +549,7 @@ export default function InputWithTags({
{isSimilaritySearch && (
<Tooltip>
<TooltipTrigger className="cursor-default">
<LuImage
<MdImageSearch
aria-label="Similarity search active"
className="size-4 text-selected"
/>
Expand Down
17 changes: 12 additions & 5 deletions web/src/components/overlay/detail/SearchDetailDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,16 +69,20 @@ const SEARCH_TABS = [
"video",
"object lifecycle",
] as const;
type SearchTab = (typeof SEARCH_TABS)[number];
export type SearchTab = (typeof SEARCH_TABS)[number];

type SearchDetailDialogProps = {
search?: SearchResult;
page: SearchTab;
setSearch: (search: SearchResult | undefined) => void;
setSearchPage: (page: SearchTab) => void;
setSimilarity?: () => void;
};
export default function SearchDetailDialog({
search,
page,
setSearch,
setSearchPage,
setSimilarity,
}: SearchDetailDialogProps) {
const { data: config } = useSWR<FrigateConfig>("config", {
Expand All @@ -87,8 +91,11 @@ export default function SearchDetailDialog({

// tabs

const [page, setPage] = useState<SearchTab>("details");
const [pageToggle, setPageToggle] = useOptimisticState(page, setPage, 100);
const [pageToggle, setPageToggle] = useOptimisticState(
page,
setSearchPage,
100,
);

// dialog and mobile page

Expand Down Expand Up @@ -130,9 +137,9 @@ export default function SearchDetailDialog({
}

if (!searchTabs.includes(pageToggle)) {
setPage("details");
setSearchPage("details");
}
}, [pageToggle, searchTabs]);
}, [pageToggle, searchTabs, setSearchPage]);

if (!search) {
return;
Expand Down
Loading

0 comments on commit 644069f

Please sign in to comment.