mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-07 22:05:44 +03:00
refactor to remove duplication between mobile and desktop
This commit is contained in:
parent
cd45fcd5a9
commit
02c5b9a4f5
@ -32,6 +32,7 @@ import {
|
|||||||
FaChevronRight,
|
FaChevronRight,
|
||||||
} from "react-icons/fa";
|
} from "react-icons/fa";
|
||||||
import { TrackingDetails } from "./TrackingDetails";
|
import { TrackingDetails } from "./TrackingDetails";
|
||||||
|
import { LuSettings } from "react-icons/lu";
|
||||||
import { DetailStreamProvider } from "@/context/detail-stream-context";
|
import { DetailStreamProvider } from "@/context/detail-stream-context";
|
||||||
import {
|
import {
|
||||||
MobilePage,
|
MobilePage,
|
||||||
@ -77,6 +78,234 @@ import { DialogPortal } from "@radix-ui/react-dialog";
|
|||||||
const SEARCH_TABS = ["snapshot", "tracking_details"] as const;
|
const SEARCH_TABS = ["snapshot", "tracking_details"] as const;
|
||||||
export type SearchTab = (typeof SEARCH_TABS)[number];
|
export type SearchTab = (typeof SEARCH_TABS)[number];
|
||||||
|
|
||||||
|
type TabsWithActionsProps = {
|
||||||
|
search: SearchResult;
|
||||||
|
searchTabs: SearchTab[];
|
||||||
|
pageToggle: SearchTab;
|
||||||
|
setPageToggle: (v: SearchTab) => void;
|
||||||
|
config?: FrigateConfig;
|
||||||
|
setSearch: (s: SearchResult | undefined) => void;
|
||||||
|
setSimilarity?: () => void;
|
||||||
|
showControls?: boolean;
|
||||||
|
setShowControls?: (v: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function TabsWithActions({
|
||||||
|
search,
|
||||||
|
searchTabs,
|
||||||
|
pageToggle,
|
||||||
|
setPageToggle,
|
||||||
|
config,
|
||||||
|
setSearch,
|
||||||
|
setSimilarity,
|
||||||
|
showControls,
|
||||||
|
setShowControls,
|
||||||
|
}: TabsWithActionsProps) {
|
||||||
|
const { t } = useTranslation(["views/explore", "views/faceLibrary"]);
|
||||||
|
|
||||||
|
if (!search) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<ScrollArea className="flex-1 whitespace-nowrap">
|
||||||
|
<div className="mb-2 flex flex-row md:mb-0">
|
||||||
|
<ToggleGroup
|
||||||
|
className="*:rounded-md *:px-3 *:py-4"
|
||||||
|
type="single"
|
||||||
|
size="sm"
|
||||||
|
value={pageToggle}
|
||||||
|
onValueChange={(value: SearchTab) => {
|
||||||
|
if (value) {
|
||||||
|
setPageToggle(value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Object.values(searchTabs).map((item) => (
|
||||||
|
<ToggleGroupItem
|
||||||
|
key={item}
|
||||||
|
className={`flex scroll-mx-10 items-center justify-between gap-2 ${pageToggle == item ? "" : "*:text-muted-foreground"}`}
|
||||||
|
value={item}
|
||||||
|
data-nav-item={item}
|
||||||
|
aria-label={`Select ${item}`}
|
||||||
|
>
|
||||||
|
<div className="smart-capitalize">
|
||||||
|
{item === "snapshot"
|
||||||
|
? search?.has_snapshot
|
||||||
|
? t("type.snapshot")
|
||||||
|
: t("type.thumbnail")
|
||||||
|
: t(`type.${item}`)}
|
||||||
|
</div>
|
||||||
|
</ToggleGroupItem>
|
||||||
|
))}
|
||||||
|
</ToggleGroup>
|
||||||
|
<ScrollBar orientation="horizontal" className="h-0" />
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
<DetailActionsMenu
|
||||||
|
search={search}
|
||||||
|
config={config}
|
||||||
|
setSearch={setSearch}
|
||||||
|
setSimilarity={setSimilarity}
|
||||||
|
/>
|
||||||
|
<div className="ml-2">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant={showControls ? "select" : "default"}
|
||||||
|
className="size-7 p-1.5"
|
||||||
|
aria-label={t("trackingDetails.adjustAnnotationSettings")}
|
||||||
|
onClick={() => setShowControls?.(!showControls)}
|
||||||
|
>
|
||||||
|
<LuSettings className="size-5" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPortal>
|
||||||
|
<TooltipContent>
|
||||||
|
{t("trackingDetails.adjustAnnotationSettings")}
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPortal>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type DialogContentComponentProps = {
|
||||||
|
page: SearchTab;
|
||||||
|
search: SearchResult;
|
||||||
|
isDesktop: boolean;
|
||||||
|
apiHost: string;
|
||||||
|
config?: FrigateConfig;
|
||||||
|
searchTabs: SearchTab[];
|
||||||
|
pageToggle: SearchTab;
|
||||||
|
setPageToggle: (v: SearchTab) => void;
|
||||||
|
setSearch: (s: SearchResult | undefined) => void;
|
||||||
|
setInputFocused: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
setSimilarity?: () => void;
|
||||||
|
showControls?: boolean;
|
||||||
|
setShowControls?: (v: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function DialogContentComponent({
|
||||||
|
page,
|
||||||
|
search,
|
||||||
|
isDesktop,
|
||||||
|
apiHost,
|
||||||
|
config,
|
||||||
|
searchTabs,
|
||||||
|
pageToggle,
|
||||||
|
setPageToggle,
|
||||||
|
setSearch,
|
||||||
|
setInputFocused,
|
||||||
|
setSimilarity,
|
||||||
|
showControls,
|
||||||
|
setShowControls,
|
||||||
|
}: DialogContentComponentProps) {
|
||||||
|
if (page === "tracking_details") {
|
||||||
|
return (
|
||||||
|
<TrackingDetails
|
||||||
|
className={cn("size-full", !isDesktop && "flex flex-col gap-4")}
|
||||||
|
event={search as unknown as Event}
|
||||||
|
tabs={
|
||||||
|
isDesktop ? (
|
||||||
|
<TabsWithActions
|
||||||
|
search={search}
|
||||||
|
searchTabs={searchTabs}
|
||||||
|
pageToggle={pageToggle}
|
||||||
|
setPageToggle={setPageToggle}
|
||||||
|
config={config}
|
||||||
|
setSearch={setSearch}
|
||||||
|
setSimilarity={setSimilarity}
|
||||||
|
showControls={showControls}
|
||||||
|
setShowControls={setShowControls}
|
||||||
|
/>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
|
showControls={showControls}
|
||||||
|
setShowControls={setShowControls}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snapshot page content
|
||||||
|
const snapshotElement = search.has_snapshot ? (
|
||||||
|
<ObjectSnapshotTab
|
||||||
|
className={isDesktop ? undefined : "mb-4"}
|
||||||
|
search={
|
||||||
|
{
|
||||||
|
...search,
|
||||||
|
plus_id: config?.plus?.enabled ? search.plus_id : "not_enabled",
|
||||||
|
} as unknown as Event
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className={cn(!isDesktop ? "mb-4 w-full" : "size-full")}>
|
||||||
|
<img
|
||||||
|
className="w-full select-none rounded-lg object-contain transition-opacity"
|
||||||
|
style={
|
||||||
|
isIOS
|
||||||
|
? {
|
||||||
|
WebkitUserSelect: "none",
|
||||||
|
WebkitTouchCallout: "none",
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
draggable={false}
|
||||||
|
src={`${apiHost}api/events/${search.id}/thumbnail.webp`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isDesktop) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-full gap-4 overflow-hidden">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"scrollbar-container flex-[3] overflow-y-hidden",
|
||||||
|
!search.has_snapshot && "flex-[2]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{snapshotElement}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-[2] flex-col gap-4 overflow-hidden">
|
||||||
|
<TabsWithActions
|
||||||
|
search={search}
|
||||||
|
searchTabs={searchTabs}
|
||||||
|
pageToggle={pageToggle}
|
||||||
|
setPageToggle={setPageToggle}
|
||||||
|
config={config}
|
||||||
|
setSearch={setSearch}
|
||||||
|
setSimilarity={setSimilarity}
|
||||||
|
showControls={showControls}
|
||||||
|
setShowControls={setShowControls}
|
||||||
|
/>
|
||||||
|
<div className="scrollbar-container flex-1 overflow-y-auto">
|
||||||
|
<ObjectDetailsTab
|
||||||
|
search={search}
|
||||||
|
config={config}
|
||||||
|
setSearch={setSearch}
|
||||||
|
setInputFocused={setInputFocused}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// mobile
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{snapshotElement}
|
||||||
|
<ObjectDetailsTab
|
||||||
|
search={search}
|
||||||
|
config={config}
|
||||||
|
setSearch={setSearch}
|
||||||
|
setInputFocused={setInputFocused}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
type SearchDetailDialogProps = {
|
type SearchDetailDialogProps = {
|
||||||
search?: SearchResult;
|
search?: SearchResult;
|
||||||
page: SearchTab;
|
page: SearchTab;
|
||||||
@ -87,6 +316,7 @@ type SearchDetailDialogProps = {
|
|||||||
onPrevious?: () => void;
|
onPrevious?: () => void;
|
||||||
onNext?: () => void;
|
onNext?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function SearchDetailDialog({
|
export default function SearchDetailDialog({
|
||||||
search,
|
search,
|
||||||
page,
|
page,
|
||||||
@ -135,6 +365,10 @@ export default function SearchDetailDialog({
|
|||||||
}
|
}
|
||||||
}, [search]);
|
}, [search]);
|
||||||
|
|
||||||
|
// show/hide annotation settings
|
||||||
|
|
||||||
|
const [showControls, setShowControls] = useState(false);
|
||||||
|
|
||||||
const searchTabs = useMemo(() => {
|
const searchTabs = useMemo(() => {
|
||||||
if (!config || !search) {
|
if (!config || !search) {
|
||||||
return [];
|
return [];
|
||||||
@ -160,44 +394,6 @@ export default function SearchDetailDialog({
|
|||||||
}
|
}
|
||||||
}, [pageToggle, searchTabs, setSearchPage]);
|
}, [pageToggle, searchTabs, setSearchPage]);
|
||||||
|
|
||||||
// Tabs component for reuse
|
|
||||||
const tabsComponent = (
|
|
||||||
<ScrollArea className="w-full whitespace-nowrap">
|
|
||||||
<div className="flex flex-row">
|
|
||||||
<ToggleGroup
|
|
||||||
className="*:rounded-md *:px-3 *:py-4"
|
|
||||||
type="single"
|
|
||||||
size="sm"
|
|
||||||
value={pageToggle}
|
|
||||||
onValueChange={(value: SearchTab) => {
|
|
||||||
if (value) {
|
|
||||||
setPageToggle(value);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Object.values(searchTabs).map((item) => (
|
|
||||||
<ToggleGroupItem
|
|
||||||
key={item}
|
|
||||||
className={`flex scroll-mx-10 items-center justify-between gap-2 ${pageToggle == item ? "" : "*:text-muted-foreground"}`}
|
|
||||||
value={item}
|
|
||||||
data-nav-item={item}
|
|
||||||
aria-label={`Select ${item}`}
|
|
||||||
>
|
|
||||||
<div className="smart-capitalize">
|
|
||||||
{item === "snapshot"
|
|
||||||
? search?.has_snapshot
|
|
||||||
? t("type.snapshot")
|
|
||||||
: t("type.thumbnail")
|
|
||||||
: t(`type.${item}`)}
|
|
||||||
</div>
|
|
||||||
</ToggleGroupItem>
|
|
||||||
))}
|
|
||||||
</ToggleGroup>
|
|
||||||
<ScrollBar orientation="horizontal" className="h-0" />
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!search) {
|
if (!search) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -293,152 +489,38 @@ export default function SearchDetailDialog({
|
|||||||
<span className="sr-only" tabIndex={0} />
|
<span className="sr-only" tabIndex={0} />
|
||||||
</Description>
|
</Description>
|
||||||
</Header>
|
</Header>
|
||||||
{isDesktop ? (
|
|
||||||
page === "tracking_details" ? (
|
{!isDesktop && (
|
||||||
<TrackingDetails
|
<div className="flex w-full flex-col justify-center gap-4">
|
||||||
className="size-full"
|
<TabsWithActions
|
||||||
event={search as unknown as Event}
|
search={search}
|
||||||
tabs={tabsComponent}
|
searchTabs={searchTabs}
|
||||||
actions={
|
pageToggle={pageToggle}
|
||||||
<DetailActionsMenu
|
setPageToggle={setPageToggle}
|
||||||
search={search}
|
config={config}
|
||||||
config={config}
|
setSearch={setSearch}
|
||||||
setSearch={setSearch}
|
setSimilarity={setSimilarity}
|
||||||
setSimilarity={setSimilarity}
|
showControls={showControls}
|
||||||
/>
|
setShowControls={setShowControls}
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
</div>
|
||||||
<div className="flex h-full gap-4 overflow-hidden">
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"scrollbar-container flex-[3] overflow-y-hidden",
|
|
||||||
page === "snapshot" && !search.has_snapshot && "flex-[2]",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{page === "snapshot" && search.has_snapshot && (
|
|
||||||
<ObjectSnapshotTab
|
|
||||||
search={
|
|
||||||
{
|
|
||||||
...search,
|
|
||||||
plus_id: config?.plus?.enabled
|
|
||||||
? search.plus_id
|
|
||||||
: "not_enabled",
|
|
||||||
} as unknown as Event
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{page === "snapshot" && !search.has_snapshot && (
|
|
||||||
<div className="size-full">
|
|
||||||
<img
|
|
||||||
className="w-full select-none rounded-lg object-contain transition-opacity"
|
|
||||||
style={
|
|
||||||
isIOS
|
|
||||||
? {
|
|
||||||
WebkitUserSelect: "none",
|
|
||||||
WebkitTouchCallout: "none",
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
draggable={false}
|
|
||||||
src={`${apiHost}api/events/${search.id}/thumbnail.webp`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-[2] flex-col gap-4 overflow-hidden">
|
|
||||||
<div className="scrollbar-container flex-1 overflow-y-auto">
|
|
||||||
{page == "snapshot" && (
|
|
||||||
<ObjectDetailsTab
|
|
||||||
search={search}
|
|
||||||
config={config}
|
|
||||||
setSearch={setSearch}
|
|
||||||
setSimilarity={setSimilarity}
|
|
||||||
setInputFocused={setInputFocused}
|
|
||||||
tabs={tabsComponent}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<ScrollArea
|
|
||||||
className={cn("w-full whitespace-nowrap", isMobile && "my-2")}
|
|
||||||
>
|
|
||||||
<div className="flex flex-row">
|
|
||||||
<ToggleGroup
|
|
||||||
className="*:rounded-md *:px-3 *:py-4"
|
|
||||||
type="single"
|
|
||||||
size="sm"
|
|
||||||
value={pageToggle}
|
|
||||||
onValueChange={(value: SearchTab) => {
|
|
||||||
if (value) {
|
|
||||||
setPageToggle(value);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Object.values(searchTabs).map((item) => (
|
|
||||||
<ToggleGroupItem
|
|
||||||
key={item}
|
|
||||||
className={`flex scroll-mx-10 items-center justify-between gap-2 ${pageToggle == item ? "" : "*:text-muted-foreground"}`}
|
|
||||||
value={item}
|
|
||||||
data-nav-item={item}
|
|
||||||
aria-label={`Select ${item}`}
|
|
||||||
>
|
|
||||||
<div className="smart-capitalize">
|
|
||||||
{t(`type.${item}`)}
|
|
||||||
</div>
|
|
||||||
</ToggleGroupItem>
|
|
||||||
))}
|
|
||||||
</ToggleGroup>
|
|
||||||
<ScrollBar orientation="horizontal" className="h-0" />
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
{page == "snapshot" && (
|
|
||||||
<>
|
|
||||||
{search.has_snapshot && (
|
|
||||||
<ObjectSnapshotTab
|
|
||||||
search={
|
|
||||||
{
|
|
||||||
...search,
|
|
||||||
plus_id: config?.plus?.enabled
|
|
||||||
? search.plus_id
|
|
||||||
: "not_enabled",
|
|
||||||
} as unknown as Event
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{page == "snapshot" && !search.has_snapshot && (
|
|
||||||
<img
|
|
||||||
className="w-full select-none rounded-lg object-contain transition-opacity"
|
|
||||||
style={
|
|
||||||
isIOS
|
|
||||||
? {
|
|
||||||
WebkitUserSelect: "none",
|
|
||||||
WebkitTouchCallout: "none",
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
draggable={false}
|
|
||||||
src={`${apiHost}api/events/${search.id}/thumbnail.webp`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<ObjectDetailsTab
|
|
||||||
search={search}
|
|
||||||
config={config}
|
|
||||||
setSearch={setSearch}
|
|
||||||
setSimilarity={setSimilarity}
|
|
||||||
setInputFocused={setInputFocused}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{page == "tracking_details" && (
|
|
||||||
<TrackingDetails event={search as unknown as Event} />
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<DialogContentComponent
|
||||||
|
page={page}
|
||||||
|
search={search}
|
||||||
|
isDesktop={isDesktop}
|
||||||
|
apiHost={apiHost}
|
||||||
|
config={config}
|
||||||
|
searchTabs={searchTabs}
|
||||||
|
pageToggle={pageToggle}
|
||||||
|
setPageToggle={setPageToggle}
|
||||||
|
setSearch={setSearch}
|
||||||
|
setInputFocused={setInputFocused}
|
||||||
|
setSimilarity={setSimilarity}
|
||||||
|
showControls={showControls}
|
||||||
|
setShowControls={setShowControls}
|
||||||
|
/>
|
||||||
</Content>
|
</Content>
|
||||||
</Overlay>
|
</Overlay>
|
||||||
</DetailStreamProvider>
|
</DetailStreamProvider>
|
||||||
@ -449,17 +531,13 @@ type ObjectDetailsTabProps = {
|
|||||||
search: SearchResult;
|
search: SearchResult;
|
||||||
config?: FrigateConfig;
|
config?: FrigateConfig;
|
||||||
setSearch: (search: SearchResult | undefined) => void;
|
setSearch: (search: SearchResult | undefined) => void;
|
||||||
setSimilarity?: () => void;
|
|
||||||
setInputFocused: React.Dispatch<React.SetStateAction<boolean>>;
|
setInputFocused: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
tabs?: React.ReactNode;
|
|
||||||
};
|
};
|
||||||
function ObjectDetailsTab({
|
function ObjectDetailsTab({
|
||||||
search,
|
search,
|
||||||
config,
|
config,
|
||||||
setSearch,
|
setSearch,
|
||||||
setSimilarity,
|
|
||||||
setInputFocused,
|
setInputFocused,
|
||||||
tabs,
|
|
||||||
}: ObjectDetailsTabProps) {
|
}: ObjectDetailsTabProps) {
|
||||||
const { t, i18n } = useTranslation([
|
const { t, i18n } = useTranslation([
|
||||||
"views/explore",
|
"views/explore",
|
||||||
@ -895,20 +973,6 @@ function ObjectDetailsTab({
|
|||||||
const popoverContainerRef = useRef<HTMLDivElement | null>(null);
|
const popoverContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
return (
|
return (
|
||||||
<div ref={popoverContainerRef} className="flex flex-col gap-5">
|
<div ref={popoverContainerRef} className="flex flex-col gap-5">
|
||||||
{tabs && (
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex-1">{tabs}</div>
|
|
||||||
<div className="ml-2">
|
|
||||||
<DetailActionsMenu
|
|
||||||
search={search}
|
|
||||||
config={config}
|
|
||||||
setSearch={setSearch}
|
|
||||||
setSimilarity={setSimilarity}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex w-full flex-row">
|
<div className="flex w-full flex-row">
|
||||||
<div className="flex w-full flex-col gap-3">
|
<div className="flex w-full flex-col gap-3">
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@ -1301,12 +1365,17 @@ function ObjectDetailsTab({
|
|||||||
|
|
||||||
type ObjectSnapshotTabProps = {
|
type ObjectSnapshotTabProps = {
|
||||||
search: Event;
|
search: Event;
|
||||||
|
className?: string;
|
||||||
|
onEventUploaded?: () => void;
|
||||||
};
|
};
|
||||||
export function ObjectSnapshotTab({ search }: ObjectSnapshotTabProps) {
|
export function ObjectSnapshotTab({
|
||||||
|
search,
|
||||||
|
className,
|
||||||
|
}: ObjectSnapshotTabProps) {
|
||||||
const [imgRef, imgLoaded, onImgLoad] = useImageLoaded();
|
const [imgRef, imgLoaded, onImgLoad] = useImageLoaded();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full">
|
<div className={cn("relative w-full", className)}>
|
||||||
<ImageLoadingIndicator
|
<ImageLoadingIndicator
|
||||||
className="absolute inset-0 aspect-video min-h-[60dvh] w-full"
|
className="absolute inset-0 aspect-video min-h-[60dvh] w-full"
|
||||||
imgLoaded={imgLoaded}
|
imgLoaded={imgLoaded}
|
||||||
|
|||||||
@ -2,20 +2,14 @@ import useSWR from "swr";
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { Event } from "@/types/event";
|
import { Event } from "@/types/event";
|
||||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { TrackingDetailsSequence } from "@/types/timeline";
|
import { TrackingDetailsSequence } from "@/types/timeline";
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
|
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
|
||||||
import { getIconForLabel } from "@/utils/iconUtil";
|
import { getIconForLabel } from "@/utils/iconUtil";
|
||||||
import { LuCircle, LuFolderX, LuSettings } from "react-icons/lu";
|
import { LuCircle, LuFolderX } from "react-icons/lu";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { AnnotationSettingsPane } from "./AnnotationSettingsPane";
|
import { AnnotationSettingsPane } from "./AnnotationSettingsPane";
|
||||||
import { TooltipPortal } from "@radix-ui/react-tooltip";
|
|
||||||
import HlsVideoPlayer from "@/components/player/HlsVideoPlayer";
|
import HlsVideoPlayer from "@/components/player/HlsVideoPlayer";
|
||||||
import { baseUrl } from "@/api/baseUrl";
|
import { baseUrl } from "@/api/baseUrl";
|
||||||
import { REVIEW_PADDING } from "@/types/review";
|
import { REVIEW_PADDING } from "@/types/review";
|
||||||
@ -46,14 +40,15 @@ type TrackingDetailsProps = {
|
|||||||
event: Event;
|
event: Event;
|
||||||
fullscreen?: boolean;
|
fullscreen?: boolean;
|
||||||
tabs?: React.ReactNode;
|
tabs?: React.ReactNode;
|
||||||
actions?: React.ReactNode;
|
showControls?: boolean;
|
||||||
|
setShowControls?: (v: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function TrackingDetails({
|
export function TrackingDetails({
|
||||||
className,
|
className,
|
||||||
event,
|
event,
|
||||||
tabs,
|
tabs,
|
||||||
actions,
|
showControls = false,
|
||||||
}: TrackingDetailsProps) {
|
}: TrackingDetailsProps) {
|
||||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||||
const { t } = useTranslation(["views/explore"]);
|
const { t } = useTranslation(["views/explore"]);
|
||||||
@ -95,7 +90,6 @@ export function TrackingDetails({
|
|||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const [_selectedZone, setSelectedZone] = useState("");
|
const [_selectedZone, setSelectedZone] = useState("");
|
||||||
const [_lifecycleZones, setLifecycleZones] = useState<string[]>([]);
|
const [_lifecycleZones, setLifecycleZones] = useState<string[]>([]);
|
||||||
const [showControls, setShowControls] = useState(false);
|
|
||||||
const [showZones, setShowZones] = useState(true);
|
const [showZones, setShowZones] = useState(true);
|
||||||
const [seekToTimestamp, setSeekToTimestamp] = useState<number | null>(null);
|
const [seekToTimestamp, setSeekToTimestamp] = useState<number | null>(null);
|
||||||
|
|
||||||
@ -454,10 +448,9 @@ export function TrackingDetails({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={cn(isDesktop && "flex-[2] overflow-hidden")}>
|
<div className={cn(isDesktop && "flex-[2] overflow-hidden")}>
|
||||||
{isDesktop && (tabs || actions) && (
|
{isDesktop && tabs && (
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<div className="mb-4 flex items-center justify-between">
|
||||||
<div className="flex-1">{tabs}</div>
|
<div className="flex-1">{tabs}</div>
|
||||||
<div className="ml-2">{actions}</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
@ -465,30 +458,6 @@ export function TrackingDetails({
|
|||||||
isDesktop && "scrollbar-container h-full overflow-y-auto",
|
isDesktop && "scrollbar-container h-full overflow-y-auto",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center justify-between">
|
|
||||||
<div className="flex flex-row gap-2">
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant={showControls ? "select" : "default"}
|
|
||||||
className="size-7 p-1.5"
|
|
||||||
aria-label={t("trackingDetails.adjustAnnotationSettings")}
|
|
||||||
>
|
|
||||||
<LuSettings
|
|
||||||
className="size-5"
|
|
||||||
onClick={() => setShowControls(!showControls)}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPortal>
|
|
||||||
<TooltipContent>
|
|
||||||
{t("trackingDetails.adjustAnnotationSettings")}
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPortal>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{config?.cameras[event.camera]?.onvif.autotracking
|
{config?.cameras[event.camera]?.onvif.autotracking
|
||||||
.enabled_in_config && (
|
.enabled_in_config && (
|
||||||
<div className="-mt-2 mb-2 text-sm text-danger">
|
<div className="-mt-2 mb-2 text-sm text-danger">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user