mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-11 17:47:37 +03:00
fix object selection and time updating
This commit is contained in:
parent
24ef3f213c
commit
7843398a1b
@ -31,10 +31,9 @@ import {
|
|||||||
FaDownload,
|
FaDownload,
|
||||||
FaHistory,
|
FaHistory,
|
||||||
FaImage,
|
FaImage,
|
||||||
FaRegListAlt,
|
|
||||||
FaVideo,
|
|
||||||
} from "react-icons/fa";
|
} from "react-icons/fa";
|
||||||
import TrackingDetails from "./TrackingDetails";
|
import { TrackingDetails } from "./TrackingDetails";
|
||||||
|
import { DetailStreamProvider } from "@/context/detail-stream-context";
|
||||||
import {
|
import {
|
||||||
MobilePage,
|
MobilePage,
|
||||||
MobilePageContent,
|
MobilePageContent,
|
||||||
@ -80,13 +79,9 @@ import { getTranslatedLabel } from "@/utils/i18n";
|
|||||||
import { CgTranscript } from "react-icons/cg";
|
import { CgTranscript } from "react-icons/cg";
|
||||||
import { CameraNameLabel } from "@/components/camera/CameraNameLabel";
|
import { CameraNameLabel } from "@/components/camera/CameraNameLabel";
|
||||||
import { PiPath } from "react-icons/pi";
|
import { PiPath } from "react-icons/pi";
|
||||||
|
import Heading from "@/components/ui/heading";
|
||||||
|
|
||||||
const SEARCH_TABS = [
|
const SEARCH_TABS = ["snapshot", "tracking_details"] as const;
|
||||||
"details",
|
|
||||||
"snapshot",
|
|
||||||
"video",
|
|
||||||
"tracking_details",
|
|
||||||
] as const;
|
|
||||||
export type SearchTab = (typeof SEARCH_TABS)[number];
|
export type SearchTab = (typeof SEARCH_TABS)[number];
|
||||||
|
|
||||||
type SearchDetailDialogProps = {
|
type SearchDetailDialogProps = {
|
||||||
@ -150,16 +145,6 @@ export default function SearchDetailDialog({
|
|||||||
|
|
||||||
const views = [...SEARCH_TABS];
|
const views = [...SEARCH_TABS];
|
||||||
|
|
||||||
if (!search.has_snapshot) {
|
|
||||||
const index = views.indexOf("snapshot");
|
|
||||||
views.splice(index, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!search.has_clip) {
|
|
||||||
const index = views.indexOf("video");
|
|
||||||
views.splice(index, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (search.data.type != "object" || !search.has_clip) {
|
if (search.data.type != "object" || !search.has_clip) {
|
||||||
const index = views.indexOf("tracking_details");
|
const index = views.indexOf("tracking_details");
|
||||||
views.splice(index, 1);
|
views.splice(index, 1);
|
||||||
@ -174,7 +159,7 @@ export default function SearchDetailDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!searchTabs.includes(pageToggle)) {
|
if (!searchTabs.includes(pageToggle)) {
|
||||||
setSearchPage("details");
|
setSearchPage("snapshot");
|
||||||
}
|
}
|
||||||
}, [pageToggle, searchTabs, setSearchPage]);
|
}, [pageToggle, searchTabs, setSearchPage]);
|
||||||
|
|
||||||
@ -196,16 +181,20 @@ export default function SearchDetailDialog({
|
|||||||
{Object.values(searchTabs).map((item) => (
|
{Object.values(searchTabs).map((item) => (
|
||||||
<ToggleGroupItem
|
<ToggleGroupItem
|
||||||
key={item}
|
key={item}
|
||||||
className={`flex scroll-mx-10 items-center justify-between gap-2 ${page == "details" ? "last:mr-20" : ""} ${pageToggle == item ? "" : "*:text-muted-foreground"}`}
|
className={`flex scroll-mx-10 items-center justify-between gap-2 ${pageToggle == item ? "" : "*:text-muted-foreground"}`}
|
||||||
value={item}
|
value={item}
|
||||||
data-nav-item={item}
|
data-nav-item={item}
|
||||||
aria-label={`Select ${item}`}
|
aria-label={`Select ${item}`}
|
||||||
>
|
>
|
||||||
{item == "details" && <FaRegListAlt className="size-4" />}
|
|
||||||
{item == "snapshot" && <FaImage className="size-4" />}
|
{item == "snapshot" && <FaImage className="size-4" />}
|
||||||
{item == "video" && <FaVideo className="size-4" />}
|
|
||||||
{item == "tracking_details" && <PiPath className="size-4" />}
|
{item == "tracking_details" && <PiPath className="size-4" />}
|
||||||
<div className="smart-capitalize">{t(`type.${item}`)}</div>
|
<div className="smart-capitalize">
|
||||||
|
{item === "snapshot"
|
||||||
|
? search?.has_snapshot
|
||||||
|
? t("type.snapshot")
|
||||||
|
: t("type.thumbnail")
|
||||||
|
: t(`type.${item}`)}
|
||||||
|
</div>
|
||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
))}
|
))}
|
||||||
</ToggleGroup>
|
</ToggleGroup>
|
||||||
@ -227,186 +216,191 @@ export default function SearchDetailDialog({
|
|||||||
const Description = isDesktop ? DialogDescription : MobilePageDescription;
|
const Description = isDesktop ? DialogDescription : MobilePageDescription;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Overlay
|
<DetailStreamProvider
|
||||||
open={isOpen}
|
isDetailMode={true}
|
||||||
onOpenChange={handleOpenChange}
|
currentTime={(search as unknown as Event)?.start_time ?? 0}
|
||||||
enableHistoryBack={true}
|
camera={(search as unknown as Event)?.camera ?? ""}
|
||||||
|
initialSelectedObjectIds={[(search as unknown as Event).id as string]}
|
||||||
>
|
>
|
||||||
<Content
|
<Overlay
|
||||||
className={cn(
|
open={isOpen}
|
||||||
"scrollbar-container overflow-y-auto",
|
onOpenChange={handleOpenChange}
|
||||||
isDesktop &&
|
enableHistoryBack={true}
|
||||||
"max-h-[95dvh] sm:max-w-xl md:max-w-4xl lg:max-w-4xl xl:max-w-7xl",
|
|
||||||
isDesktop &&
|
|
||||||
page == "tracking_details" &&
|
|
||||||
"lg:max-w-[75%] xl:max-w-[80%]",
|
|
||||||
isMobile && "px-4",
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<Header>
|
<Content
|
||||||
<Title>{t("trackedObjectDetails")}</Title>
|
className={cn(
|
||||||
<Description className="sr-only">
|
"scrollbar-container overflow-y-auto",
|
||||||
{t("trackedObjectDetails")}
|
isDesktop &&
|
||||||
</Description>
|
"max-h-[95dvh] sm:max-w-xl md:max-w-4xl lg:max-w-4xl xl:max-w-7xl",
|
||||||
</Header>
|
isDesktop &&
|
||||||
{isDesktop ? (
|
page == "tracking_details" &&
|
||||||
page === "tracking_details" ? (
|
"lg:max-w-[75%] xl:max-w-[80%]",
|
||||||
<TrackingDetails
|
isMobile && "px-4",
|
||||||
className="size-full"
|
)}
|
||||||
event={search as unknown as Event}
|
>
|
||||||
tabs={tabsComponent}
|
<Header>
|
||||||
/>
|
<Title>{t("trackedObjectDetails")}</Title>
|
||||||
) : (
|
<Description className="sr-only">
|
||||||
<div className="flex h-full gap-4 overflow-hidden">
|
{t("trackedObjectDetails")}
|
||||||
<div className="scrollbar-container flex-[3] overflow-y-hidden">
|
</Description>
|
||||||
{page === "snapshot" && search.has_snapshot && (
|
</Header>
|
||||||
<ObjectSnapshotTab
|
{isDesktop ? (
|
||||||
search={
|
page === "tracking_details" ? (
|
||||||
{
|
<TrackingDetails
|
||||||
...search,
|
className="size-full"
|
||||||
plus_id: config?.plus?.enabled
|
event={search as unknown as Event}
|
||||||
? search.plus_id
|
tabs={tabsComponent}
|
||||||
: "not_enabled",
|
/>
|
||||||
} as unknown as Event
|
) : (
|
||||||
}
|
<div className="flex h-full gap-4 overflow-hidden">
|
||||||
onEventUploaded={() => {
|
<div
|
||||||
search.plus_id = "new_upload";
|
className={cn(
|
||||||
}}
|
"scrollbar-container flex-[3] overflow-y-hidden",
|
||||||
/>
|
page === "snapshot" && !search.has_snapshot && "flex-[2]",
|
||||||
)}
|
)}
|
||||||
{page === "video" && search.has_clip && (
|
>
|
||||||
<VideoTab search={search} />
|
{page === "snapshot" && search.has_snapshot && (
|
||||||
)}
|
<ObjectSnapshotTab
|
||||||
{(page === "details" ||
|
search={
|
||||||
(!search.has_snapshot && page === "snapshot") ||
|
{
|
||||||
(!search.has_clip && page === "video")) && (
|
...search,
|
||||||
<img
|
plus_id: config?.plus?.enabled
|
||||||
className="aspect-video select-none rounded-lg object-contain transition-opacity"
|
? search.plus_id
|
||||||
style={
|
: "not_enabled",
|
||||||
isIOS
|
} as unknown as Event
|
||||||
? {
|
}
|
||||||
WebkitUserSelect: "none",
|
onEventUploaded={() => {
|
||||||
WebkitTouchCallout: "none",
|
search.plus_id = "new_upload";
|
||||||
}
|
}}
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
draggable={false}
|
|
||||||
src={`${apiHost}api/events/${search.id}/thumbnail.webp`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-[2] flex-col gap-4 overflow-hidden">
|
|
||||||
{tabsComponent}
|
|
||||||
<div className="scrollbar-container flex-1 overflow-y-auto">
|
|
||||||
{page == "details" && (
|
|
||||||
<ObjectDetailsTab
|
|
||||||
search={search}
|
|
||||||
config={config}
|
|
||||||
setSearch={setSearch}
|
|
||||||
setSimilarity={setSimilarity}
|
|
||||||
setInputFocused={setInputFocused}
|
|
||||||
showThumbnail={false}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{page == "snapshot" && (
|
{page === "snapshot" && !search.has_snapshot && (
|
||||||
<ObjectDetailsTab
|
<img
|
||||||
search={search}
|
className="size-full select-none rounded-lg object-contain transition-opacity"
|
||||||
config={config}
|
style={
|
||||||
setSearch={setSearch}
|
isIOS
|
||||||
setSimilarity={setSimilarity}
|
? {
|
||||||
setInputFocused={setInputFocused}
|
WebkitUserSelect: "none",
|
||||||
showThumbnail={false}
|
WebkitTouchCallout: "none",
|
||||||
/>
|
}
|
||||||
)}
|
: undefined
|
||||||
{page == "video" && (
|
}
|
||||||
<ObjectDetailsTab
|
draggable={false}
|
||||||
search={search}
|
src={`${apiHost}api/events/${search.id}/thumbnail.webp`}
|
||||||
config={config}
|
|
||||||
setSearch={setSearch}
|
|
||||||
setSimilarity={setSimilarity}
|
|
||||||
setInputFocused={setInputFocused}
|
|
||||||
showThumbnail={false}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex flex-[2] flex-col gap-4 overflow-hidden">
|
||||||
|
{tabsComponent}
|
||||||
|
<div className="scrollbar-container flex-1 overflow-y-auto">
|
||||||
|
{page == "snapshot" && (
|
||||||
|
<ObjectDetailsTab
|
||||||
|
search={search}
|
||||||
|
config={config}
|
||||||
|
setSearch={setSearch}
|
||||||
|
setSimilarity={setSimilarity}
|
||||||
|
setInputFocused={setInputFocused}
|
||||||
|
showThumbnail={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
)
|
) : (
|
||||||
) : (
|
<>
|
||||||
<>
|
<ScrollArea
|
||||||
<ScrollArea
|
className={cn("w-full whitespace-nowrap", isMobile && "my-2")}
|
||||||
className={cn("w-full whitespace-nowrap", isMobile && "my-2")}
|
>
|
||||||
>
|
<div className="flex flex-row">
|
||||||
<div className="flex flex-row">
|
<ToggleGroup
|
||||||
<ToggleGroup
|
className="*:rounded-md *:px-3 *:py-4"
|
||||||
className="*:rounded-md *:px-3 *:py-4"
|
type="single"
|
||||||
type="single"
|
size="sm"
|
||||||
size="sm"
|
value={pageToggle}
|
||||||
value={pageToggle}
|
onValueChange={(value: SearchTab) => {
|
||||||
onValueChange={(value: SearchTab) => {
|
if (value) {
|
||||||
if (value) {
|
setPageToggle(value);
|
||||||
setPageToggle(value);
|
}
|
||||||
}
|
}}
|
||||||
}}
|
>
|
||||||
>
|
{Object.values(searchTabs).map((item) => (
|
||||||
{Object.values(searchTabs).map((item) => (
|
<ToggleGroupItem
|
||||||
<ToggleGroupItem
|
key={item}
|
||||||
key={item}
|
className={`flex scroll-mx-10 items-center justify-between gap-2 ${pageToggle == item ? "" : "*:text-muted-foreground"}`}
|
||||||
className={`flex scroll-mx-10 items-center justify-between gap-2 ${page == "details" ? "last:mr-20" : ""} ${pageToggle == item ? "" : "*:text-muted-foreground"}`}
|
value={item}
|
||||||
value={item}
|
data-nav-item={item}
|
||||||
data-nav-item={item}
|
aria-label={`Select ${item}`}
|
||||||
aria-label={`Select ${item}`}
|
>
|
||||||
>
|
{item == "snapshot" && <FaImage className="size-4" />}
|
||||||
{item == "details" && <FaRegListAlt className="size-4" />}
|
{item == "tracking_details" && (
|
||||||
{item == "snapshot" && <FaImage className="size-4" />}
|
<PiPath className="size-4" />
|
||||||
{item == "video" && <FaVideo className="size-4" />}
|
)}
|
||||||
{item == "tracking_details" && (
|
<div className="smart-capitalize">
|
||||||
<PiPath className="size-4" />
|
{t(`type.${item}`)}
|
||||||
)}
|
</div>
|
||||||
<div className="smart-capitalize">
|
</ToggleGroupItem>
|
||||||
{t(`type.${item}`)}
|
))}
|
||||||
</div>
|
</ToggleGroup>
|
||||||
</ToggleGroupItem>
|
<ScrollBar orientation="horizontal" className="h-0" />
|
||||||
))}
|
</div>
|
||||||
</ToggleGroup>
|
</ScrollArea>
|
||||||
<ScrollBar orientation="horizontal" className="h-0" />
|
{page == "snapshot" && (
|
||||||
</div>
|
<>
|
||||||
</ScrollArea>
|
{search.has_snapshot && (
|
||||||
{page == "details" && (
|
<ObjectSnapshotTab
|
||||||
<ObjectDetailsTab
|
search={
|
||||||
search={search}
|
{
|
||||||
config={config}
|
...search,
|
||||||
setSearch={setSearch}
|
plus_id: config?.plus?.enabled
|
||||||
setSimilarity={setSimilarity}
|
? search.plus_id
|
||||||
setInputFocused={setInputFocused}
|
: "not_enabled",
|
||||||
/>
|
} as unknown as Event
|
||||||
)}
|
}
|
||||||
{page == "snapshot" && (
|
onEventUploaded={() => {
|
||||||
<ObjectSnapshotTab
|
search.plus_id = "new_upload";
|
||||||
search={
|
}}
|
||||||
{
|
/>
|
||||||
...search,
|
)}
|
||||||
plus_id: config?.plus?.enabled
|
{page == "snapshot" && !search.has_snapshot && (
|
||||||
? search.plus_id
|
<img
|
||||||
: "not_enabled",
|
className="w-full select-none rounded-lg object-contain transition-opacity"
|
||||||
} as unknown as Event
|
style={
|
||||||
}
|
isIOS
|
||||||
onEventUploaded={() => {
|
? {
|
||||||
search.plus_id = "new_upload";
|
WebkitUserSelect: "none",
|
||||||
}}
|
WebkitTouchCallout: "none",
|
||||||
/>
|
}
|
||||||
)}
|
: undefined
|
||||||
{page == "video" && <VideoTab search={search} />}
|
}
|
||||||
{page == "tracking_details" && (
|
draggable={false}
|
||||||
<TrackingDetails
|
src={`${apiHost}api/events/${search.id}/thumbnail.webp`}
|
||||||
className="w-full overflow-x-hidden"
|
/>
|
||||||
event={search as unknown as Event}
|
)}
|
||||||
/>
|
<Heading as="h3" className="mt-2 smart-capitalize">
|
||||||
)}
|
{t("type.details")}
|
||||||
</>
|
</Heading>
|
||||||
)}
|
<ObjectDetailsTab
|
||||||
</Content>
|
search={search}
|
||||||
</Overlay>
|
config={config}
|
||||||
|
setSearch={setSearch}
|
||||||
|
setSimilarity={setSimilarity}
|
||||||
|
setInputFocused={setInputFocused}
|
||||||
|
showThumbnail={false}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{page == "tracking_details" && (
|
||||||
|
<TrackingDetails
|
||||||
|
className="w-full overflow-x-hidden"
|
||||||
|
event={search as unknown as Event}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Content>
|
||||||
|
</Overlay>
|
||||||
|
</DetailStreamProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1305,7 +1299,7 @@ export function ObjectSnapshotTab({
|
|||||||
search.label != "on_demand" && (
|
search.label != "on_demand" && (
|
||||||
<Card className="p-1 text-sm md:p-2">
|
<Card className="p-1 text-sm md:p-2">
|
||||||
<CardContent className="flex flex-col items-center justify-between gap-3 p-2 md:flex-row">
|
<CardContent className="flex flex-col items-center justify-between gap-3 p-2 md:flex-row">
|
||||||
<div className={cn("flex flex-col space-y-3")}>
|
<div className={cn("flex max-w-sm flex-col space-y-3")}>
|
||||||
<div className={"text-lg leading-none"}>
|
<div className={"text-lg leading-none"}>
|
||||||
{t("explore.plus.submitToPlus.label")}
|
{t("explore.plus.submitToPlus.label")}
|
||||||
</div>
|
</div>
|
||||||
@ -1314,7 +1308,7 @@ export function ObjectSnapshotTab({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex w-full flex-1 flex-col justify-center gap-2 md:ml-8 md:w-auto md:justify-end">
|
<div className="flex w-full flex-1 flex-col justify-center gap-2 md:ml-8 md:flex-1 md:justify-end">
|
||||||
{state == "reviewing" && (
|
{state == "reviewing" && (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -8,20 +8,7 @@ import Heading from "@/components/ui/heading";
|
|||||||
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 {
|
import { LuCircle, LuSettings } from "react-icons/lu";
|
||||||
LuCircle,
|
|
||||||
LuCircleDot,
|
|
||||||
LuEar,
|
|
||||||
LuPlay,
|
|
||||||
LuSettings,
|
|
||||||
LuTruck,
|
|
||||||
} from "react-icons/lu";
|
|
||||||
import { IoMdExit } from "react-icons/io";
|
|
||||||
import {
|
|
||||||
MdFaceUnlock,
|
|
||||||
MdOutlineLocationOn,
|
|
||||||
MdOutlinePictureInPictureAlt,
|
|
||||||
} from "react-icons/md";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@ -43,17 +30,13 @@ import {
|
|||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { getLifecycleItemDescription } from "@/utils/lifecycleUtil";
|
import { getLifecycleItemDescription } from "@/utils/lifecycleUtil";
|
||||||
import { IoPlayCircleOutline } from "react-icons/io5";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { getTranslatedLabel } from "@/utils/i18n";
|
import { getTranslatedLabel } from "@/utils/i18n";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { HiDotsHorizontal } from "react-icons/hi";
|
import { HiDotsHorizontal } from "react-icons/hi";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import {
|
import { useDetailStream } from "@/context/detail-stream-context";
|
||||||
DetailStreamProvider,
|
|
||||||
useDetailStream,
|
|
||||||
} from "@/context/detail-stream-context";
|
|
||||||
import { isDesktop } from "react-device-detect";
|
import { isDesktop } from "react-device-detect";
|
||||||
|
|
||||||
type TrackingDetailsProps = {
|
type TrackingDetailsProps = {
|
||||||
@ -63,36 +46,16 @@ type TrackingDetailsProps = {
|
|||||||
tabs?: React.ReactNode;
|
tabs?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Wrapper component that provides DetailStreamContext
|
export function TrackingDetails({
|
||||||
export default function TrackingDetails(props: TrackingDetailsProps) {
|
|
||||||
const [currentTime, setCurrentTime] = useState(props.event.start_time ?? 0);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DetailStreamProvider
|
|
||||||
isDetailMode={true}
|
|
||||||
currentTime={currentTime}
|
|
||||||
camera={props.event.camera}
|
|
||||||
>
|
|
||||||
<TrackingDetailsInner {...props} onTimeUpdate={setCurrentTime} />
|
|
||||||
</DetailStreamProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inner component with access to DetailStreamContext
|
|
||||||
function TrackingDetailsInner({
|
|
||||||
className,
|
className,
|
||||||
event,
|
event,
|
||||||
tabs,
|
tabs,
|
||||||
onTimeUpdate,
|
}: TrackingDetailsProps) {
|
||||||
}: TrackingDetailsProps & { onTimeUpdate: (time: number) => void }) {
|
|
||||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||||
const { t } = useTranslation(["views/explore"]);
|
const { t } = useTranslation(["views/explore"]);
|
||||||
const {
|
const { setSelectedObjectIds, annotationOffset, setAnnotationOffset } =
|
||||||
setSelectedObjectIds,
|
useDetailStream();
|
||||||
annotationOffset,
|
const [currentTime, setCurrentTime] = useState(event.start_time ?? 0);
|
||||||
setAnnotationOffset,
|
|
||||||
currentTime,
|
|
||||||
} = useDetailStream();
|
|
||||||
|
|
||||||
const { data: eventSequence } = useSWR<TrackingDetailsSequence[]>([
|
const { data: eventSequence } = useSWR<TrackingDetailsSequence[]>([
|
||||||
"timeline",
|
"timeline",
|
||||||
@ -110,7 +73,7 @@ function TrackingDetailsInner({
|
|||||||
);
|
);
|
||||||
|
|
||||||
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 [showControls, setShowControls] = useState(false);
|
||||||
const [showZones, setShowZones] = useState(true);
|
const [showZones, setShowZones] = useState(true);
|
||||||
@ -151,7 +114,7 @@ function TrackingDetailsInner({
|
|||||||
const handleLifecycleClick = useCallback((item: TrackingDetailsSequence) => {
|
const handleLifecycleClick = useCallback((item: TrackingDetailsSequence) => {
|
||||||
const timestamp = item.timestamp ?? 0;
|
const timestamp = item.timestamp ?? 0;
|
||||||
setLifecycleZones(item.data.zones);
|
setLifecycleZones(item.data.zones);
|
||||||
_setSelectedZone("");
|
setSelectedZone("");
|
||||||
|
|
||||||
// Set the target timestamp to seek to
|
// Set the target timestamp to seek to
|
||||||
setSeekToTimestamp(timestamp);
|
setSeekToTimestamp(timestamp);
|
||||||
@ -287,8 +250,6 @@ function TrackingDetailsInner({
|
|||||||
}
|
}
|
||||||
}, [aspectRatio]);
|
}, [aspectRatio]);
|
||||||
|
|
||||||
// Container layout classes - no longer needed, handled in return JSX
|
|
||||||
|
|
||||||
const handleSeekToTime = useCallback((timestamp: number, _play?: boolean) => {
|
const handleSeekToTime = useCallback((timestamp: number, _play?: boolean) => {
|
||||||
// Set the target timestamp to seek to
|
// Set the target timestamp to seek to
|
||||||
setSeekToTimestamp(timestamp);
|
setSeekToTimestamp(timestamp);
|
||||||
@ -296,16 +257,16 @@ function TrackingDetailsInner({
|
|||||||
|
|
||||||
const handleTimeUpdate = useCallback(
|
const handleTimeUpdate = useCallback(
|
||||||
(time: number) => {
|
(time: number) => {
|
||||||
// Convert video time to absolute timestamp
|
|
||||||
const absoluteTime = time - REVIEW_PADDING + event.start_time;
|
const absoluteTime = time - REVIEW_PADDING + event.start_time;
|
||||||
onTimeUpdate(absoluteTime);
|
setCurrentTime(absoluteTime);
|
||||||
},
|
},
|
||||||
[event.start_time, onTimeUpdate],
|
[event.start_time],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return <ActivityIndicator />;
|
return <ActivityIndicator />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -337,7 +298,7 @@ function TrackingDetailsInner({
|
|||||||
hotKeys={false}
|
hotKeys={false}
|
||||||
supportsFullscreen={false}
|
supportsFullscreen={false}
|
||||||
fullscreen={false}
|
fullscreen={false}
|
||||||
frigateControls={false}
|
frigateControls={true}
|
||||||
onTimeUpdate={handleTimeUpdate}
|
onTimeUpdate={handleTimeUpdate}
|
||||||
onSeekToTime={handleSeekToTime}
|
onSeekToTime={handleSeekToTime}
|
||||||
isDetailMode={true}
|
isDetailMode={true}
|
||||||
@ -534,8 +495,10 @@ function TrackingDetailsInner({
|
|||||||
areaPx={areaPx}
|
areaPx={areaPx}
|
||||||
areaPct={areaPct}
|
areaPct={areaPct}
|
||||||
onClick={() => handleLifecycleClick(item)}
|
onClick={() => handleLifecycleClick(item)}
|
||||||
setSelectedZone={_setSelectedZone}
|
setSelectedZone={setSelectedZone}
|
||||||
getZoneColor={getZoneColor}
|
getZoneColor={getZoneColor}
|
||||||
|
effectiveTime={effectiveTime}
|
||||||
|
isTimelineActive={isWithinEventRange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -551,43 +514,6 @@ function TrackingDetailsInner({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetTimelineIconParams = {
|
|
||||||
lifecycleItem: TrackingDetailsSequence;
|
|
||||||
className?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function LifecycleIcon({
|
|
||||||
lifecycleItem,
|
|
||||||
className,
|
|
||||||
}: GetTimelineIconParams) {
|
|
||||||
switch (lifecycleItem.class_type) {
|
|
||||||
case "visible":
|
|
||||||
return <LuPlay className={cn("size-5", className)} />;
|
|
||||||
case "gone":
|
|
||||||
return <IoMdExit className={cn("size-5", className)} />;
|
|
||||||
case "active":
|
|
||||||
return <LuCircleDot className={cn("size-5", className)} />;
|
|
||||||
case "stationary":
|
|
||||||
return <LuCircle className={cn("size-5", className)} />;
|
|
||||||
case "entered_zone":
|
|
||||||
return <MdOutlineLocationOn className={cn("size-5", className)} />;
|
|
||||||
case "attribute":
|
|
||||||
return lifecycleItem.data.attribute === "face" ? (
|
|
||||||
<MdFaceUnlock className={cn("size-5", className)} />
|
|
||||||
) : lifecycleItem.data.attribute === "license_plate" ? (
|
|
||||||
<MdOutlinePictureInPictureAlt className={cn("size-5", className)} />
|
|
||||||
) : (
|
|
||||||
<LuTruck className={cn("size-5", className)} />
|
|
||||||
);
|
|
||||||
case "heard":
|
|
||||||
return <LuEar className={cn("size-5", className)} />;
|
|
||||||
case "external":
|
|
||||||
return <IoPlayCircleOutline className={cn("size-5", className)} />;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type LifecycleIconRowProps = {
|
type LifecycleIconRowProps = {
|
||||||
item: TrackingDetailsSequence;
|
item: TrackingDetailsSequence;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
@ -598,6 +524,8 @@ type LifecycleIconRowProps = {
|
|||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
setSelectedZone: (z: string) => void;
|
setSelectedZone: (z: string) => void;
|
||||||
getZoneColor: (zoneName: string) => number[] | undefined;
|
getZoneColor: (zoneName: string) => number[] | undefined;
|
||||||
|
effectiveTime?: number;
|
||||||
|
isTimelineActive?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function LifecycleIconRow({
|
function LifecycleIconRow({
|
||||||
@ -610,6 +538,8 @@ function LifecycleIconRow({
|
|||||||
onClick,
|
onClick,
|
||||||
setSelectedZone,
|
setSelectedZone,
|
||||||
getZoneColor,
|
getZoneColor,
|
||||||
|
effectiveTime,
|
||||||
|
isTimelineActive,
|
||||||
}: LifecycleIconRowProps) {
|
}: LifecycleIconRowProps) {
|
||||||
const { t } = useTranslation(["views/explore", "components/player"]);
|
const { t } = useTranslation(["views/explore", "components/player"]);
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
@ -632,7 +562,9 @@ function LifecycleIconRow({
|
|||||||
<LuCircle
|
<LuCircle
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative z-10 ml-[1px] size-2.5 fill-secondary-foreground stroke-none",
|
"relative z-10 ml-[1px] size-2.5 fill-secondary-foreground stroke-none",
|
||||||
isActive && "fill-selected duration-300",
|
(isActive || (effectiveTime ?? 0) >= (item?.timestamp ?? 0)) &&
|
||||||
|
isTimelineActive &&
|
||||||
|
"fill-selected duration-300",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -22,6 +22,7 @@ interface DetailStreamProviderProps {
|
|||||||
isDetailMode: boolean;
|
isDetailMode: boolean;
|
||||||
currentTime: number;
|
currentTime: number;
|
||||||
camera: string;
|
camera: string;
|
||||||
|
initialSelectedObjectIds?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DetailStreamProvider({
|
export function DetailStreamProvider({
|
||||||
@ -29,8 +30,11 @@ export function DetailStreamProvider({
|
|||||||
isDetailMode,
|
isDetailMode,
|
||||||
currentTime,
|
currentTime,
|
||||||
camera,
|
camera,
|
||||||
|
initialSelectedObjectIds,
|
||||||
}: DetailStreamProviderProps) {
|
}: DetailStreamProviderProps) {
|
||||||
const [selectedObjectIds, setSelectedObjectIds] = useState<string[]>([]);
|
const [selectedObjectIds, setSelectedObjectIds] = useState<string[]>(
|
||||||
|
() => initialSelectedObjectIds ?? [],
|
||||||
|
);
|
||||||
|
|
||||||
const toggleObjectSelection = (id: string | undefined) => {
|
const toggleObjectSelection = (id: string | undefined) => {
|
||||||
if (id === undefined) {
|
if (id === undefined) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user