mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-06-13 01:16:53 +02: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.end_time,
|
||||||
Event.has_clip,
|
Event.has_clip,
|
||||||
Event.has_snapshot,
|
Event.has_snapshot,
|
||||||
|
Event.top_score,
|
||||||
Event.data,
|
Event.data,
|
||||||
Event.plus_id,
|
Event.plus_id,
|
||||||
ReviewSegment.thumb_path,
|
ReviewSegment.thumb_path,
|
||||||
|
@ -1,38 +1,10 @@
|
|||||||
import { useCallback, useState } from "react";
|
|
||||||
import TimeAgo from "../dynamic/TimeAgo";
|
import TimeAgo from "../dynamic/TimeAgo";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
import { useFormattedTimestamp } from "@/hooks/use-date-utils";
|
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 { SearchResult } from "@/types/search";
|
||||||
import {
|
import ActivityIndicator from "../indicators/activity-indicator";
|
||||||
DropdownMenu,
|
import SearchResultActions from "../menu/SearchResultActions";
|
||||||
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 { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type SearchThumbnailProps = {
|
type SearchThumbnailProps = {
|
||||||
@ -52,31 +24,7 @@ export default function SearchThumbnailFooter({
|
|||||||
}: SearchThumbnailProps) {
|
}: SearchThumbnailProps) {
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
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
|
// date
|
||||||
|
|
||||||
const formattedDate = useFormattedTimestamp(
|
const formattedDate = useFormattedTimestamp(
|
||||||
searchResult.start_time,
|
searchResult.start_time,
|
||||||
config?.ui.time_format == "24hour" ? "%b %-d, %H:%M" : "%b %-d, %I:%M %p",
|
config?.ui.time_format == "24hour" ? "%b %-d, %H:%M" : "%b %-d, %I:%M %p",
|
||||||
@ -84,39 +32,6 @@ export default function SearchThumbnailFooter({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
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
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full flex-row items-center justify-between",
|
"flex w-full flex-row items-center justify-between",
|
||||||
@ -135,95 +50,13 @@ export default function SearchThumbnailFooter({
|
|||||||
{formattedDate}
|
{formattedDate}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row items-center justify-end gap-6 md:gap-4">
|
<div className="flex flex-row items-center justify-end gap-6 md:gap-4">
|
||||||
{!isMobileOnly &&
|
<SearchResultActions
|
||||||
config?.plus?.enabled &&
|
searchResult={searchResult}
|
||||||
searchResult.has_snapshot &&
|
findSimilar={findSimilar}
|
||||||
searchResult.end_time &&
|
refreshResults={refreshResults}
|
||||||
!searchResult.plus_id && (
|
showObjectLifecycle={showObjectLifecycle}
|
||||||
<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>
|
</div>
|
||||||
</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 { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { FrigateStats } from "@/types/stats";
|
import { FrigateStats } from "@/types/stats";
|
||||||
import { useFrigateStats } from "@/api/ws";
|
import { useEmbeddingsReindexProgress, useFrigateStats } from "@/api/ws";
|
||||||
import { useContext, useEffect, useMemo } from "react";
|
import { useContext, useEffect, useMemo } from "react";
|
||||||
import useStats from "@/hooks/use-stats";
|
import useStats from "@/hooks/use-stats";
|
||||||
import GeneralSettings from "../menu/GeneralSettings";
|
import GeneralSettings from "../menu/GeneralSettings";
|
||||||
@ -74,6 +74,23 @@ function StatusAlertNav({ className }: StatusAlertNavProps) {
|
|||||||
});
|
});
|
||||||
}, [potentialProblems, addMessage, clearMessages]);
|
}, [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) {
|
if (!messages || Object.keys(messages).length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -18,15 +18,22 @@ import ActivityIndicator from "@/components/indicators/activity-indicator";
|
|||||||
import { useEventUpdate } from "@/api/ws";
|
import { useEventUpdate } from "@/api/ws";
|
||||||
import { isEqual } from "lodash";
|
import { isEqual } from "lodash";
|
||||||
import TimeAgo from "@/components/dynamic/TimeAgo";
|
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 = {
|
type ExploreViewProps = {
|
||||||
searchDetail: SearchResult | undefined;
|
searchDetail: SearchResult | undefined;
|
||||||
setSearchDetail: (search: SearchResult | undefined) => void;
|
setSearchDetail: (search: SearchResult | undefined) => void;
|
||||||
|
setSimilaritySearch: (search: SearchResult) => void;
|
||||||
|
onSelectSearch: (item: SearchResult, index: number, page?: SearchTab) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ExploreView({
|
export default function ExploreView({
|
||||||
searchDetail,
|
searchDetail,
|
||||||
setSearchDetail,
|
setSearchDetail,
|
||||||
|
setSimilaritySearch,
|
||||||
|
onSelectSearch,
|
||||||
}: ExploreViewProps) {
|
}: ExploreViewProps) {
|
||||||
// title
|
// title
|
||||||
|
|
||||||
@ -102,6 +109,9 @@ export default function ExploreView({
|
|||||||
isValidating={isValidating}
|
isValidating={isValidating}
|
||||||
objectType={label}
|
objectType={label}
|
||||||
setSearchDetail={setSearchDetail}
|
setSearchDetail={setSearchDetail}
|
||||||
|
mutate={mutate}
|
||||||
|
setSimilaritySearch={setSimilaritySearch}
|
||||||
|
onSelectSearch={onSelectSearch}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -113,6 +123,9 @@ type ThumbnailRowType = {
|
|||||||
searchResults?: SearchResult[];
|
searchResults?: SearchResult[];
|
||||||
isValidating: boolean;
|
isValidating: boolean;
|
||||||
setSearchDetail: (search: SearchResult | undefined) => void;
|
setSearchDetail: (search: SearchResult | undefined) => void;
|
||||||
|
mutate: () => void;
|
||||||
|
setSimilaritySearch: (search: SearchResult) => void;
|
||||||
|
onSelectSearch: (item: SearchResult, index: number, page?: SearchTab) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
function ThumbnailRow({
|
function ThumbnailRow({
|
||||||
@ -120,6 +133,9 @@ function ThumbnailRow({
|
|||||||
searchResults,
|
searchResults,
|
||||||
isValidating,
|
isValidating,
|
||||||
setSearchDetail,
|
setSearchDetail,
|
||||||
|
mutate,
|
||||||
|
setSimilaritySearch,
|
||||||
|
onSelectSearch,
|
||||||
}: ThumbnailRowType) {
|
}: ThumbnailRowType) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@ -155,6 +171,9 @@ function ThumbnailRow({
|
|||||||
<ExploreThumbnailImage
|
<ExploreThumbnailImage
|
||||||
event={event}
|
event={event}
|
||||||
setSearchDetail={setSearchDetail}
|
setSearchDetail={setSearchDetail}
|
||||||
|
mutate={mutate}
|
||||||
|
setSimilaritySearch={setSimilaritySearch}
|
||||||
|
onSelectSearch={onSelectSearch}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@ -184,25 +203,49 @@ function ThumbnailRow({
|
|||||||
type ExploreThumbnailImageProps = {
|
type ExploreThumbnailImageProps = {
|
||||||
event: SearchResult;
|
event: SearchResult;
|
||||||
setSearchDetail: (search: SearchResult | undefined) => void;
|
setSearchDetail: (search: SearchResult | undefined) => void;
|
||||||
|
mutate: () => void;
|
||||||
|
setSimilaritySearch: (search: SearchResult) => void;
|
||||||
|
onSelectSearch: (item: SearchResult, index: number, page?: SearchTab) => void;
|
||||||
};
|
};
|
||||||
function ExploreThumbnailImage({
|
function ExploreThumbnailImage({
|
||||||
event,
|
event,
|
||||||
setSearchDetail,
|
setSearchDetail,
|
||||||
|
mutate,
|
||||||
|
setSimilaritySearch,
|
||||||
|
onSelectSearch,
|
||||||
}: ExploreThumbnailImageProps) {
|
}: ExploreThumbnailImageProps) {
|
||||||
const apiHost = useApiHost();
|
const apiHost = useApiHost();
|
||||||
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
const [imgRef, imgLoaded, onImgLoad] = useImageLoaded();
|
const [imgRef, imgLoaded, onImgLoad] = useImageLoaded();
|
||||||
|
|
||||||
|
const handleFindSimilar = () => {
|
||||||
|
if (config?.semantic_search.enabled) {
|
||||||
|
setSimilaritySearch(event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleShowObjectLifecycle = () => {
|
||||||
|
onSelectSearch(event, 0, "object lifecycle");
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<SearchResultActions
|
||||||
|
searchResult={event}
|
||||||
|
findSimilar={handleFindSimilar}
|
||||||
|
refreshResults={mutate}
|
||||||
|
showObjectLifecycle={handleShowObjectLifecycle}
|
||||||
|
isContextMenu={true}
|
||||||
|
>
|
||||||
|
<div className="relative size-full">
|
||||||
<ImageLoadingIndicator
|
<ImageLoadingIndicator
|
||||||
className="absolute inset-0"
|
className="absolute inset-0"
|
||||||
imgLoaded={imgLoaded}
|
imgLoaded={imgLoaded}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<img
|
<img
|
||||||
ref={imgRef}
|
ref={imgRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute h-full w-full cursor-pointer rounded-lg object-cover transition-all duration-300 ease-in-out lg:rounded-2xl",
|
"absolute size-full cursor-pointer rounded-lg object-cover transition-all duration-300 ease-in-out lg:rounded-2xl",
|
||||||
|
!imgLoaded && "invisible",
|
||||||
)}
|
)}
|
||||||
style={
|
style={
|
||||||
isIOS
|
isIOS
|
||||||
@ -216,9 +259,8 @@ function ExploreThumbnailImage({
|
|||||||
draggable={false}
|
draggable={false}
|
||||||
src={`${apiHost}api/events/${event.id}/thumbnail.jpg`}
|
src={`${apiHost}api/events/${event.id}/thumbnail.jpg`}
|
||||||
onClick={() => setSearchDetail(event)}
|
onClick={() => setSearchDetail(event)}
|
||||||
onLoad={() => {
|
onLoad={onImgLoad}
|
||||||
onImgLoad();
|
alt={`${event.label} thumbnail`}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
{isDesktop && (
|
{isDesktop && (
|
||||||
<div className="absolute bottom-1 right-1 z-10 rounded-lg bg-black/50 px-2 py-1 text-xs text-white">
|
<div className="absolute bottom-1 right-1 z-10 rounded-lg bg-black/50 px-2 py-1 text-xs text-white">
|
||||||
@ -231,7 +273,8 @@ function ExploreThumbnailImage({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</div>
|
||||||
|
</SearchResultActions>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -489,6 +489,8 @@ export default function SearchView({
|
|||||||
<ExploreView
|
<ExploreView
|
||||||
searchDetail={searchDetail}
|
searchDetail={searchDetail}
|
||||||
setSearchDetail={setSearchDetail}
|
setSearchDetail={setSearchDetail}
|
||||||
|
setSimilaritySearch={setSimilaritySearch}
|
||||||
|
onSelectSearch={onSelectSearch}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
Loading…
Reference in New Issue
Block a user