Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Explore layout changes #14348

Merged
merged 23 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
165d48e
Reset selected index on new searches
hawkeye217 Oct 14, 2024
70be0f2
Remove right click for similarity search
hawkeye217 Oct 14, 2024
53afc58
Fix sub label icon
NickM-27 Oct 14, 2024
be47400
add card footer
hawkeye217 Oct 14, 2024
b5f99c8
Merge branch 'fix-selected-outline' of https://github.com/blakeblacks…
hawkeye217 Oct 14, 2024
9756829
Add Frigate+ dialog
NickM-27 Oct 14, 2024
778fd7b
Move buttons and menu to thumbnail footer
hawkeye217 Oct 14, 2024
5684e41
Add similarity search
hawkeye217 Oct 15, 2024
216e54a
Show object score
NickM-27 Oct 15, 2024
87c8aaf
Implement download buttons
NickM-27 Oct 15, 2024
6e6fa0d
remove confidence score
hawkeye217 Oct 15, 2024
74aad2a
Merge branch 'fix-selected-outline' of https://github.com/blakeblacks…
hawkeye217 Oct 15, 2024
6ecd3ca
conditionally show submenu items
hawkeye217 Oct 15, 2024
2442fb8
Implement delete
NickM-27 Oct 15, 2024
c1fcfca
Merge branch 'fix-selected-outline' of github.com:blakeblackshear/fri…
NickM-27 Oct 15, 2024
9d498ae
fix icon color
hawkeye217 Oct 15, 2024
143d8f1
Add object lifecycle button
NickM-27 Oct 15, 2024
5a978e1
Merge branch 'fix-selected-outline' of github.com:blakeblackshear/fri…
NickM-27 Oct 15, 2024
ae679ac
fix score
hawkeye217 Oct 15, 2024
d7d2b75
Merge branch 'fix-selected-outline' of https://github.com/blakeblacks…
hawkeye217 Oct 15, 2024
90c6000
delete confirmation
hawkeye217 Oct 15, 2024
934a0e4
small tweaks
hawkeye217 Oct 15, 2024
37c7985
consistent icons
hawkeye217 Oct 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading