mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	UI tweaks (#14505)
* Add reindex progress to mobile bottom bar status alert * move menu to new component * actions component in search footer thumbnail * context menu for explore summary thumbnail images * readd top_score to search query for old events
This commit is contained in:
		
							parent
							
								
									40c6fda19d
								
							
						
					
					
						commit
						828fdbfd2d
					
				| @ -394,6 +394,7 @@ def events_search(request: Request, params: EventsSearchQueryParams = Depends()) | ||||
|         Event.end_time, | ||||
|         Event.has_clip, | ||||
|         Event.has_snapshot, | ||||
|         Event.top_score, | ||||
|         Event.data, | ||||
|         Event.plus_id, | ||||
|         ReviewSegment.thumb_path, | ||||
|  | ||||
| @ -1,38 +1,10 @@ | ||||
| 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, | ||||
|   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"; | ||||
| import { isMobileOnly } from "react-device-detect"; | ||||
| import { buttonVariants } from "../ui/button"; | ||||
| import ActivityIndicator from "../indicators/activity-indicator"; | ||||
| import SearchResultActions from "../menu/SearchResultActions"; | ||||
| import { cn } from "@/lib/utils"; | ||||
| 
 | ||||
| type SearchThumbnailProps = { | ||||
| @ -52,31 +24,7 @@ export default function SearchThumbnailFooter({ | ||||
| }: 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", | ||||
| @ -84,146 +32,31 @@ export default function SearchThumbnailFooter({ | ||||
|   ); | ||||
| 
 | ||||
|   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={buttonVariants({ variant: "destructive" })} | ||||
|               onClick={handleDelete} | ||||
|             > | ||||
|               Delete | ||||
|             </AlertDialogAction> | ||||
|           </AlertDialogFooter> | ||||
|         </AlertDialogContent> | ||||
|       </AlertDialog> | ||||
|       <FrigatePlusDialog | ||||
|         upload={ | ||||
|           showFrigatePlus ? (searchResult as unknown as Event) : undefined | ||||
|         } | ||||
|         onClose={() => setShowFrigatePlus(false)} | ||||
|         onEventUploaded={() => { | ||||
|           searchResult.plus_id = "submitted"; | ||||
|         }} | ||||
|       /> | ||||
| 
 | ||||
|       <div | ||||
|         className={cn( | ||||
|           "flex w-full flex-row items-center justify-between", | ||||
|           columns > 4 && | ||||
|             "items-start sm:flex-col sm:gap-2 lg:flex-row lg:items-center lg:gap-1", | ||||
|     <div | ||||
|       className={cn( | ||||
|         "flex w-full flex-row items-center justify-between", | ||||
|         columns > 4 && | ||||
|           "items-start sm:flex-col sm:gap-2 lg:flex-row lg:items-center lg:gap-1", | ||||
|       )} | ||||
|     > | ||||
|       <div className="flex flex-col items-start text-xs text-primary-variant"> | ||||
|         {searchResult.end_time ? ( | ||||
|           <TimeAgo time={searchResult.start_time * 1000} dense /> | ||||
|         ) : ( | ||||
|           <div> | ||||
|             <ActivityIndicator size={14} /> | ||||
|           </div> | ||||
|         )} | ||||
|       > | ||||
|         <div className="flex flex-col items-start text-xs text-primary-variant"> | ||||
|           {searchResult.end_time ? ( | ||||
|             <TimeAgo time={searchResult.start_time * 1000} dense /> | ||||
|           ) : ( | ||||
|             <div> | ||||
|               <ActivityIndicator size={14} /> | ||||
|             </div> | ||||
|           )} | ||||
|           {formattedDate} | ||||
|         </div> | ||||
|         <div className="flex flex-row items-center justify-end gap-6 md:gap-4"> | ||||
|           {!isMobileOnly && | ||||
|             config?.plus?.enabled && | ||||
|             searchResult.has_snapshot && | ||||
|             searchResult.end_time && | ||||
|             !searchResult.plus_id && ( | ||||
|               <Tooltip> | ||||
|                 <TooltipTrigger> | ||||
|                   <FrigatePlusIcon | ||||
|                     className="size-5 cursor-pointer text-primary-variant hover: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-variant hover:text-primary" | ||||
|                   onClick={findSimilar} | ||||
|                 /> | ||||
|               </TooltipTrigger> | ||||
|               <TooltipContent>Find similar</TooltipContent> | ||||
|             </Tooltip> | ||||
|           )} | ||||
| 
 | ||||
|           <DropdownMenu> | ||||
|             <DropdownMenuTrigger> | ||||
|               <LuMoreVertical className="size-5 cursor-pointer text-primary-variant hover:text-primary" /> | ||||
|             </DropdownMenuTrigger> | ||||
|             <DropdownMenuContent align={"end"}> | ||||
|               {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 | ||||
|                 className="cursor-pointer" | ||||
|                 onClick={showObjectLifecycle} | ||||
|               > | ||||
|                 <FaArrowsRotate className="mr-2 size-4" /> | ||||
|                 <span>View object lifecycle</span> | ||||
|               </DropdownMenuItem> | ||||
| 
 | ||||
|               {isMobileOnly && | ||||
|                 config?.plus?.enabled && | ||||
|                 searchResult.has_snapshot && | ||||
|                 searchResult.end_time && | ||||
|                 !searchResult.plus_id && ( | ||||
|                   <DropdownMenuItem | ||||
|                     className="cursor-pointer" | ||||
|                     onClick={() => setShowFrigatePlus(true)} | ||||
|                   > | ||||
|                     <FrigatePlusIcon className="mr-2 size-4 cursor-pointer text-primary" /> | ||||
|                     <span>Submit to Frigate+</span> | ||||
|                   </DropdownMenuItem> | ||||
|                 )} | ||||
|               <DropdownMenuItem | ||||
|                 className="cursor-pointer" | ||||
|                 onClick={() => setDeleteDialogOpen(true)} | ||||
|               > | ||||
|                 <LuTrash2 className="mr-2 size-4" /> | ||||
|                 <span>Delete</span> | ||||
|               </DropdownMenuItem> | ||||
|             </DropdownMenuContent> | ||||
|           </DropdownMenu> | ||||
|         </div> | ||||
|         {formattedDate} | ||||
|       </div> | ||||
|     </> | ||||
|       <div className="flex flex-row items-center justify-end gap-6 md:gap-4"> | ||||
|         <SearchResultActions | ||||
|           searchResult={searchResult} | ||||
|           findSimilar={findSimilar} | ||||
|           refreshResults={refreshResults} | ||||
|           showObjectLifecycle={showObjectLifecycle} | ||||
|         /> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
|  | ||||
							
								
								
									
										218
									
								
								web/src/components/menu/SearchResultActions.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										218
									
								
								web/src/components/menu/SearchResultActions.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,218 @@ | ||||
| import { useState, ReactNode } from "react"; | ||||
| import { SearchResult } from "@/types/search"; | ||||
| import { FrigateConfig } from "@/types/frigateConfig"; | ||||
| import { baseUrl } from "@/api/baseUrl"; | ||||
| import { toast } from "sonner"; | ||||
| import axios from "axios"; | ||||
| import { LuCamera, LuDownload, LuMoreVertical, LuTrash2 } from "react-icons/lu"; | ||||
| import { FaArrowsRotate } from "react-icons/fa6"; | ||||
| import { MdImageSearch } from "react-icons/md"; | ||||
| import FrigatePlusIcon from "@/components/icons/FrigatePlusIcon"; | ||||
| import { isMobileOnly } from "react-device-detect"; | ||||
| import { buttonVariants } from "@/components/ui/button"; | ||||
| import { | ||||
|   ContextMenu, | ||||
|   ContextMenuContent, | ||||
|   ContextMenuItem, | ||||
|   ContextMenuTrigger, | ||||
| } from "@/components/ui/context-menu"; | ||||
| import { | ||||
|   DropdownMenu, | ||||
|   DropdownMenuContent, | ||||
|   DropdownMenuItem, | ||||
|   DropdownMenuTrigger, | ||||
| } from "@/components/ui/dropdown-menu"; | ||||
| import { | ||||
|   AlertDialog, | ||||
|   AlertDialogAction, | ||||
|   AlertDialogCancel, | ||||
|   AlertDialogContent, | ||||
|   AlertDialogDescription, | ||||
|   AlertDialogFooter, | ||||
|   AlertDialogHeader, | ||||
|   AlertDialogTitle, | ||||
| } from "@/components/ui/alert-dialog"; | ||||
| import { | ||||
|   Tooltip, | ||||
|   TooltipContent, | ||||
|   TooltipTrigger, | ||||
| } from "@/components/ui/tooltip"; | ||||
| import { FrigatePlusDialog } from "@/components/overlay/dialog/FrigatePlusDialog"; | ||||
| import useSWR from "swr"; | ||||
| import { Event } from "@/types/event"; | ||||
| 
 | ||||
| type SearchResultActionsProps = { | ||||
|   searchResult: SearchResult; | ||||
|   findSimilar: () => void; | ||||
|   refreshResults: () => void; | ||||
|   showObjectLifecycle: () => void; | ||||
|   isContextMenu?: boolean; | ||||
|   children?: ReactNode; | ||||
| }; | ||||
| 
 | ||||
| export default function SearchResultActions({ | ||||
|   searchResult, | ||||
|   findSimilar, | ||||
|   refreshResults, | ||||
|   showObjectLifecycle, | ||||
|   isContextMenu = false, | ||||
|   children, | ||||
| }: SearchResultActionsProps) { | ||||
|   const { data: config } = useSWR<FrigateConfig>("config"); | ||||
| 
 | ||||
|   const [showFrigatePlus, setShowFrigatePlus] = useState(false); | ||||
|   const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); | ||||
| 
 | ||||
|   const handleDelete = () => { | ||||
|     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", | ||||
|         }); | ||||
|       }); | ||||
|   }; | ||||
| 
 | ||||
|   const MenuItem = isContextMenu ? ContextMenuItem : DropdownMenuItem; | ||||
| 
 | ||||
|   const menuItems = ( | ||||
|     <> | ||||
|       {searchResult.has_clip && ( | ||||
|         <MenuItem> | ||||
|           <a | ||||
|             className="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> | ||||
|         </MenuItem> | ||||
|       )} | ||||
|       {searchResult.has_snapshot && ( | ||||
|         <MenuItem> | ||||
|           <a | ||||
|             className="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> | ||||
|         </MenuItem> | ||||
|       )} | ||||
|       <MenuItem onClick={showObjectLifecycle}> | ||||
|         <FaArrowsRotate className="mr-2 size-4" /> | ||||
|         <span>View object lifecycle</span> | ||||
|       </MenuItem> | ||||
|       {config?.semantic_search?.enabled && isContextMenu && ( | ||||
|         <MenuItem onClick={findSimilar}> | ||||
|           <MdImageSearch className="mr-2 size-4" /> | ||||
|           <span>Find similar</span> | ||||
|         </MenuItem> | ||||
|       )} | ||||
|       {isMobileOnly && | ||||
|         config?.plus?.enabled && | ||||
|         searchResult.has_snapshot && | ||||
|         searchResult.end_time && | ||||
|         !searchResult.plus_id && ( | ||||
|           <MenuItem onClick={() => setShowFrigatePlus(true)}> | ||||
|             <FrigatePlusIcon className="mr-2 size-4 cursor-pointer text-primary" /> | ||||
|             <span>Submit to Frigate+</span> | ||||
|           </MenuItem> | ||||
|         )} | ||||
|       <MenuItem onClick={() => setDeleteDialogOpen(true)}> | ||||
|         <LuTrash2 className="mr-2 size-4" /> | ||||
|         <span>Delete</span> | ||||
|       </MenuItem> | ||||
|     </> | ||||
|   ); | ||||
| 
 | ||||
|   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={buttonVariants({ variant: "destructive" })} | ||||
|               onClick={handleDelete} | ||||
|             > | ||||
|               Delete | ||||
|             </AlertDialogAction> | ||||
|           </AlertDialogFooter> | ||||
|         </AlertDialogContent> | ||||
|       </AlertDialog> | ||||
|       <FrigatePlusDialog | ||||
|         upload={ | ||||
|           showFrigatePlus ? (searchResult as unknown as Event) : undefined | ||||
|         } | ||||
|         onClose={() => setShowFrigatePlus(false)} | ||||
|         onEventUploaded={() => { | ||||
|           searchResult.plus_id = "submitted"; | ||||
|         }} | ||||
|       /> | ||||
| 
 | ||||
|       {isContextMenu ? ( | ||||
|         <ContextMenu> | ||||
|           <ContextMenuTrigger>{children}</ContextMenuTrigger> | ||||
|           <ContextMenuContent>{menuItems}</ContextMenuContent> | ||||
|         </ContextMenu> | ||||
|       ) : ( | ||||
|         <> | ||||
|           {config?.semantic_search?.enabled && ( | ||||
|             <Tooltip> | ||||
|               <TooltipTrigger> | ||||
|                 <MdImageSearch | ||||
|                   className="size-5 cursor-pointer text-primary-variant hover:text-primary" | ||||
|                   onClick={findSimilar} | ||||
|                 /> | ||||
|               </TooltipTrigger> | ||||
|               <TooltipContent>Find similar</TooltipContent> | ||||
|             </Tooltip> | ||||
|           )} | ||||
| 
 | ||||
|           {!isMobileOnly && | ||||
|             config?.plus?.enabled && | ||||
|             searchResult.has_snapshot && | ||||
|             searchResult.end_time && | ||||
|             !searchResult.plus_id && ( | ||||
|               <Tooltip> | ||||
|                 <TooltipTrigger> | ||||
|                   <FrigatePlusIcon | ||||
|                     className="size-5 cursor-pointer text-primary-variant hover:text-primary" | ||||
|                     onClick={() => setShowFrigatePlus(true)} | ||||
|                   /> | ||||
|                 </TooltipTrigger> | ||||
|                 <TooltipContent>Submit to Frigate+</TooltipContent> | ||||
|               </Tooltip> | ||||
|             )} | ||||
| 
 | ||||
|           <DropdownMenu> | ||||
|             <DropdownMenuTrigger> | ||||
|               <LuMoreVertical className="size-5 cursor-pointer text-primary-variant hover:text-primary" /> | ||||
|             </DropdownMenuTrigger> | ||||
|             <DropdownMenuContent align="end">{menuItems}</DropdownMenuContent> | ||||
|           </DropdownMenu> | ||||
|         </> | ||||
|       )} | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
| @ -3,7 +3,7 @@ import { IoIosWarning } from "react-icons/io"; | ||||
| import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer"; | ||||
| import useSWR from "swr"; | ||||
| import { FrigateStats } from "@/types/stats"; | ||||
| import { useFrigateStats } from "@/api/ws"; | ||||
| import { useEmbeddingsReindexProgress, useFrigateStats } from "@/api/ws"; | ||||
| import { useContext, useEffect, useMemo } from "react"; | ||||
| import useStats from "@/hooks/use-stats"; | ||||
| import GeneralSettings from "../menu/GeneralSettings"; | ||||
| @ -74,6 +74,23 @@ function StatusAlertNav({ className }: StatusAlertNavProps) { | ||||
|     }); | ||||
|   }, [potentialProblems, addMessage, clearMessages]); | ||||
| 
 | ||||
|   const { payload: reindexState } = useEmbeddingsReindexProgress(); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if (reindexState) { | ||||
|       if (reindexState.status == "indexing") { | ||||
|         clearMessages("embeddings-reindex"); | ||||
|         addMessage( | ||||
|           "embeddings-reindex", | ||||
|           `Reindexing embeddings (${Math.floor((reindexState.processed_objects / reindexState.total_objects) * 100)}% complete)`, | ||||
|         ); | ||||
|       } | ||||
|       if (reindexState.status === "completed") { | ||||
|         clearMessages("embeddings-reindex"); | ||||
|       } | ||||
|     } | ||||
|   }, [reindexState, addMessage, clearMessages]); | ||||
| 
 | ||||
|   if (!messages || Object.keys(messages).length === 0) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
| @ -18,15 +18,22 @@ import ActivityIndicator from "@/components/indicators/activity-indicator"; | ||||
| import { useEventUpdate } from "@/api/ws"; | ||||
| import { isEqual } from "lodash"; | ||||
| import TimeAgo from "@/components/dynamic/TimeAgo"; | ||||
| import SearchResultActions from "@/components/menu/SearchResultActions"; | ||||
| import { SearchTab } from "@/components/overlay/detail/SearchDetailDialog"; | ||||
| import { FrigateConfig } from "@/types/frigateConfig"; | ||||
| 
 | ||||
| type ExploreViewProps = { | ||||
|   searchDetail: SearchResult | undefined; | ||||
|   setSearchDetail: (search: SearchResult | undefined) => void; | ||||
|   setSimilaritySearch: (search: SearchResult) => void; | ||||
|   onSelectSearch: (item: SearchResult, index: number, page?: SearchTab) => void; | ||||
| }; | ||||
| 
 | ||||
| export default function ExploreView({ | ||||
|   searchDetail, | ||||
|   setSearchDetail, | ||||
|   setSimilaritySearch, | ||||
|   onSelectSearch, | ||||
| }: ExploreViewProps) { | ||||
|   // title
 | ||||
| 
 | ||||
| @ -102,6 +109,9 @@ export default function ExploreView({ | ||||
|           isValidating={isValidating} | ||||
|           objectType={label} | ||||
|           setSearchDetail={setSearchDetail} | ||||
|           mutate={mutate} | ||||
|           setSimilaritySearch={setSimilaritySearch} | ||||
|           onSelectSearch={onSelectSearch} | ||||
|         /> | ||||
|       ))} | ||||
|     </div> | ||||
| @ -113,6 +123,9 @@ type ThumbnailRowType = { | ||||
|   searchResults?: SearchResult[]; | ||||
|   isValidating: boolean; | ||||
|   setSearchDetail: (search: SearchResult | undefined) => void; | ||||
|   mutate: () => void; | ||||
|   setSimilaritySearch: (search: SearchResult) => void; | ||||
|   onSelectSearch: (item: SearchResult, index: number, page?: SearchTab) => void; | ||||
| }; | ||||
| 
 | ||||
| function ThumbnailRow({ | ||||
| @ -120,6 +133,9 @@ function ThumbnailRow({ | ||||
|   searchResults, | ||||
|   isValidating, | ||||
|   setSearchDetail, | ||||
|   mutate, | ||||
|   setSimilaritySearch, | ||||
|   onSelectSearch, | ||||
| }: ThumbnailRowType) { | ||||
|   const navigate = useNavigate(); | ||||
| 
 | ||||
| @ -155,6 +171,9 @@ function ThumbnailRow({ | ||||
|             <ExploreThumbnailImage | ||||
|               event={event} | ||||
|               setSearchDetail={setSearchDetail} | ||||
|               mutate={mutate} | ||||
|               setSimilaritySearch={setSimilaritySearch} | ||||
|               onSelectSearch={onSelectSearch} | ||||
|             /> | ||||
|           </div> | ||||
|         ))} | ||||
| @ -184,54 +203,78 @@ function ThumbnailRow({ | ||||
| type ExploreThumbnailImageProps = { | ||||
|   event: SearchResult; | ||||
|   setSearchDetail: (search: SearchResult | undefined) => void; | ||||
|   mutate: () => void; | ||||
|   setSimilaritySearch: (search: SearchResult) => void; | ||||
|   onSelectSearch: (item: SearchResult, index: number, page?: SearchTab) => void; | ||||
| }; | ||||
| function ExploreThumbnailImage({ | ||||
|   event, | ||||
|   setSearchDetail, | ||||
|   mutate, | ||||
|   setSimilaritySearch, | ||||
|   onSelectSearch, | ||||
| }: ExploreThumbnailImageProps) { | ||||
|   const apiHost = useApiHost(); | ||||
|   const { data: config } = useSWR<FrigateConfig>("config"); | ||||
|   const [imgRef, imgLoaded, onImgLoad] = useImageLoaded(); | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       <ImageLoadingIndicator | ||||
|         className="absolute inset-0" | ||||
|         imgLoaded={imgLoaded} | ||||
|       /> | ||||
|   const handleFindSimilar = () => { | ||||
|     if (config?.semantic_search.enabled) { | ||||
|       setSimilaritySearch(event); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|       <img | ||||
|         ref={imgRef} | ||||
|         className={cn( | ||||
|           "absolute h-full w-full cursor-pointer rounded-lg object-cover transition-all duration-300 ease-in-out lg:rounded-2xl", | ||||
|         )} | ||||
|         style={ | ||||
|           isIOS | ||||
|             ? { | ||||
|                 WebkitUserSelect: "none", | ||||
|                 WebkitTouchCallout: "none", | ||||
|               } | ||||
|             : undefined | ||||
|         } | ||||
|         loading={isSafari ? "eager" : "lazy"} | ||||
|         draggable={false} | ||||
|         src={`${apiHost}api/events/${event.id}/thumbnail.jpg`} | ||||
|         onClick={() => setSearchDetail(event)} | ||||
|         onLoad={() => { | ||||
|           onImgLoad(); | ||||
|         }} | ||||
|       /> | ||||
|       {isDesktop && ( | ||||
|         <div className="absolute bottom-1 right-1 z-10 rounded-lg bg-black/50 px-2 py-1 text-xs text-white"> | ||||
|           {event.end_time ? ( | ||||
|             <TimeAgo time={event.start_time * 1000} dense /> | ||||
|           ) : ( | ||||
|             <div> | ||||
|               <ActivityIndicator size={10} /> | ||||
|             </div> | ||||
|   const handleShowObjectLifecycle = () => { | ||||
|     onSelectSearch(event, 0, "object lifecycle"); | ||||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <SearchResultActions | ||||
|       searchResult={event} | ||||
|       findSimilar={handleFindSimilar} | ||||
|       refreshResults={mutate} | ||||
|       showObjectLifecycle={handleShowObjectLifecycle} | ||||
|       isContextMenu={true} | ||||
|     > | ||||
|       <div className="relative size-full"> | ||||
|         <ImageLoadingIndicator | ||||
|           className="absolute inset-0" | ||||
|           imgLoaded={imgLoaded} | ||||
|         /> | ||||
|         <img | ||||
|           ref={imgRef} | ||||
|           className={cn( | ||||
|             "absolute size-full cursor-pointer rounded-lg object-cover transition-all duration-300 ease-in-out lg:rounded-2xl", | ||||
|             !imgLoaded && "invisible", | ||||
|           )} | ||||
|         </div> | ||||
|       )} | ||||
|     </> | ||||
|           style={ | ||||
|             isIOS | ||||
|               ? { | ||||
|                   WebkitUserSelect: "none", | ||||
|                   WebkitTouchCallout: "none", | ||||
|                 } | ||||
|               : undefined | ||||
|           } | ||||
|           loading={isSafari ? "eager" : "lazy"} | ||||
|           draggable={false} | ||||
|           src={`${apiHost}api/events/${event.id}/thumbnail.jpg`} | ||||
|           onClick={() => setSearchDetail(event)} | ||||
|           onLoad={onImgLoad} | ||||
|           alt={`${event.label} thumbnail`} | ||||
|         /> | ||||
|         {isDesktop && ( | ||||
|           <div className="absolute bottom-1 right-1 z-10 rounded-lg bg-black/50 px-2 py-1 text-xs text-white"> | ||||
|             {event.end_time ? ( | ||||
|               <TimeAgo time={event.start_time * 1000} dense /> | ||||
|             ) : ( | ||||
|               <div> | ||||
|                 <ActivityIndicator size={10} /> | ||||
|               </div> | ||||
|             )} | ||||
|           </div> | ||||
|         )} | ||||
|       </div> | ||||
|     </SearchResultActions> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -489,6 +489,8 @@ export default function SearchView({ | ||||
|             <ExploreView | ||||
|               searchDetail={searchDetail} | ||||
|               setSearchDetail={setSearchDetail} | ||||
|               setSimilaritySearch={setSimilaritySearch} | ||||
|               onSelectSearch={onSelectSearch} | ||||
|             /> | ||||
|           </div> | ||||
|         )} | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user