diff --git a/web/public/locales/en/views/explore.json b/web/public/locales/en/views/explore.json index 8ba170882..0316ed2fe 100644 --- a/web/public/locales/en/views/explore.json +++ b/web/public/locales/en/views/explore.json @@ -36,8 +36,8 @@ "video": "video", "object_lifecycle": "object lifecycle" }, - "objectLifecycle": { - "title": "Object Lifecycle", + "trackingDetails": { + "title": "Tracking Details", "noImageFound": "No image found for this timestamp.", "createObjectMask": "Create Object Mask", "adjustAnnotationSettings": "Adjust annotation settings", @@ -168,9 +168,9 @@ "label": "Download snapshot", "aria": "Download snapshot" }, - "viewObjectLifecycle": { - "label": "View object lifecycle", - "aria": "Show the object lifecycle" + "viewTrackingDetails": { + "label": "View tracking details", + "aria": "Show the tracking details" }, "findSimilar": { "label": "Find similar", @@ -205,7 +205,7 @@ "dialog": { "confirmDelete": { "title": "Confirm Delete", - "desc": "Deleting this tracked object removes the snapshot, any saved embeddings, and any associated object lifecycle entries. Recorded footage of this tracked object in History view will NOT be deleted.

Are you sure you want to proceed?" + "desc": "Deleting this tracked object removes the snapshot, any saved embeddings, and any associated tracking details entries. Recorded footage of this tracked object in History view will NOT be deleted.

Are you sure you want to proceed?" } }, "noTrackedObjects": "No Tracked Objects Found", diff --git a/web/src/components/card/SearchThumbnailFooter.tsx b/web/src/components/card/SearchThumbnailFooter.tsx index 9a6f4e6ee..7cd510629 100644 --- a/web/src/components/card/SearchThumbnailFooter.tsx +++ b/web/src/components/card/SearchThumbnailFooter.tsx @@ -13,7 +13,7 @@ type SearchThumbnailProps = { columns: number; findSimilar: () => void; refreshResults: () => void; - showObjectLifecycle: () => void; + showTrackingDetails: () => void; showSnapshot: () => void; addTrigger: () => void; }; @@ -23,7 +23,7 @@ export default function SearchThumbnailFooter({ columns, findSimilar, refreshResults, - showObjectLifecycle, + showTrackingDetails, showSnapshot, addTrigger, }: SearchThumbnailProps) { @@ -61,7 +61,7 @@ export default function SearchThumbnailFooter({ searchResult={searchResult} findSimilar={findSimilar} refreshResults={refreshResults} - showObjectLifecycle={showObjectLifecycle} + showTrackingDetails={showTrackingDetails} showSnapshot={showSnapshot} addTrigger={addTrigger} /> diff --git a/web/src/components/menu/SearchResultActions.tsx b/web/src/components/menu/SearchResultActions.tsx index 99d9e4881..45e9a55d8 100644 --- a/web/src/components/menu/SearchResultActions.tsx +++ b/web/src/components/menu/SearchResultActions.tsx @@ -47,7 +47,7 @@ type SearchResultActionsProps = { searchResult: SearchResult; findSimilar: () => void; refreshResults: () => void; - showObjectLifecycle: () => void; + showTrackingDetails: () => void; showSnapshot: () => void; addTrigger: () => void; isContextMenu?: boolean; @@ -58,7 +58,7 @@ export default function SearchResultActions({ searchResult, findSimilar, refreshResults, - showObjectLifecycle, + showTrackingDetails, showSnapshot, addTrigger, isContextMenu = false, @@ -125,11 +125,11 @@ export default function SearchResultActions({ )} {searchResult.data.type == "object" && ( - {t("itemMenu.viewObjectLifecycle.label")} + {t("itemMenu.viewTrackingDetails.label")} )} {config?.semantic_search?.enabled && isContextMenu && ( diff --git a/web/src/components/overlay/ObjectTrackOverlay.tsx b/web/src/components/overlay/ObjectTrackOverlay.tsx index 50cf92781..f95cabf7a 100644 --- a/web/src/components/overlay/ObjectTrackOverlay.tsx +++ b/web/src/components/overlay/ObjectTrackOverlay.tsx @@ -1,5 +1,5 @@ import { useMemo, useCallback } from "react"; -import { ObjectLifecycleSequence, LifecycleClassType } from "@/types/timeline"; +import { TrackingDetailsSequence, LifecycleClassType } from "@/types/timeline"; import { FrigateConfig } from "@/types/frigateConfig"; import useSWR from "swr"; import { useDetailStream } from "@/context/detail-stream-context"; @@ -27,7 +27,7 @@ type PathPoint = { x: number; y: number; timestamp: number; - lifecycle_item?: ObjectLifecycleSequence; + lifecycle_item?: TrackingDetailsSequence; objectId: string; }; @@ -63,7 +63,7 @@ export default function ObjectTrackOverlay({ ); // Fetch timeline data for each object ID using fixed number of hooks - const { data: timelineData } = useSWR( + const { data: timelineData } = useSWR( selectedObjectIds.length > 0 ? `timeline?source_id=${selectedObjectIds.join(",")}&limit=1000` : null, @@ -74,7 +74,7 @@ export default function ObjectTrackOverlay({ // Group timeline entries by source_id if (!timelineData) return selectedObjectIds.map(() => []); - const grouped: Record = {}; + const grouped: Record = {}; for (const entry of timelineData) { if (!grouped[entry.source_id]) { grouped[entry.source_id] = []; @@ -152,9 +152,9 @@ export default function ObjectTrackOverlay({ const eventSequencePoints: PathPoint[] = timelineData ?.filter( - (event: ObjectLifecycleSequence) => event.data.box !== undefined, + (event: TrackingDetailsSequence) => event.data.box !== undefined, ) - .map((event: ObjectLifecycleSequence) => { + .map((event: TrackingDetailsSequence) => { const [left, top, width, height] = event.data.box!; return { x: left + width / 2, // Center x @@ -183,22 +183,22 @@ export default function ObjectTrackOverlay({ const currentZones = timelineData ?.filter( - (event: ObjectLifecycleSequence) => + (event: TrackingDetailsSequence) => event.timestamp <= effectiveCurrentTime, ) .sort( - (a: ObjectLifecycleSequence, b: ObjectLifecycleSequence) => + (a: TrackingDetailsSequence, b: TrackingDetailsSequence) => b.timestamp - a.timestamp, )[0]?.data?.zones || []; // Get current bounding box const currentBox = timelineData ?.filter( - (event: ObjectLifecycleSequence) => + (event: TrackingDetailsSequence) => event.timestamp <= effectiveCurrentTime && event.data.box, ) .sort( - (a: ObjectLifecycleSequence, b: ObjectLifecycleSequence) => + (a: TrackingDetailsSequence, b: TrackingDetailsSequence) => b.timestamp - a.timestamp, )[0]?.data?.box; diff --git a/web/src/components/overlay/detail/AnnotationOffsetSlider.tsx b/web/src/components/overlay/detail/AnnotationOffsetSlider.tsx index 03aad4d60..0a2594d26 100644 --- a/web/src/components/overlay/detail/AnnotationOffsetSlider.tsx +++ b/web/src/components/overlay/detail/AnnotationOffsetSlider.tsx @@ -40,7 +40,7 @@ export default function AnnotationOffsetSlider({ className }: Props) { ); toast.success( - t("objectLifecycle.annotationSettings.offset.toast.success", { + t("trackingDetails.annotationSettings.offset.toast.success", { camera, }), { position: "top-center" }, diff --git a/web/src/components/overlay/detail/AnnotationSettingsPane.tsx b/web/src/components/overlay/detail/AnnotationSettingsPane.tsx index 56214b99d..c180502f4 100644 --- a/web/src/components/overlay/detail/AnnotationSettingsPane.tsx +++ b/web/src/components/overlay/detail/AnnotationSettingsPane.tsx @@ -79,7 +79,7 @@ export function AnnotationSettingsPane({ .then((res) => { if (res.status === 200) { toast.success( - t("objectLifecycle.annotationSettings.offset.toast.success", { + t("trackingDetails.annotationSettings.offset.toast.success", { camera: event?.camera, }), { @@ -142,7 +142,7 @@ export function AnnotationSettingsPane({ return (
- {t("objectLifecycle.annotationSettings.title")} + {t("trackingDetails.annotationSettings.title")}
@@ -152,11 +152,11 @@ export function AnnotationSettingsPane({ onCheckedChange={setShowZones} />
- {t("objectLifecycle.annotationSettings.showAllZones.desc")} + {t("trackingDetails.annotationSettings.showAllZones.desc")}
@@ -171,14 +171,14 @@ export function AnnotationSettingsPane({ render={({ field }) => ( - {t("objectLifecycle.annotationSettings.offset.label")} + {t("trackingDetails.annotationSettings.offset.label")}
- objectLifecycle.annotationSettings.offset.desc + trackingDetails.annotationSettings.offset.desc
- objectLifecycle.annotationSettings.offset.millisecondsToOffset + trackingDetails.annotationSettings.offset.millisecondsToOffset
- {t("objectLifecycle.annotationSettings.offset.tips")} + {t("trackingDetails.annotationSettings.offset.tips")}
diff --git a/web/src/components/overlay/detail/ObjectPath.tsx b/web/src/components/overlay/detail/ObjectPath.tsx index 0101a71f1..7f43fb2c7 100644 --- a/web/src/components/overlay/detail/ObjectPath.tsx +++ b/web/src/components/overlay/detail/ObjectPath.tsx @@ -105,7 +105,7 @@ export function ObjectPath({ {pos.lifecycle_item ? getLifecycleItemDescription(pos.lifecycle_item) - : t("objectLifecycle.trackedPoint")} + : t("trackingDetails.trackedPoint")} diff --git a/web/src/components/overlay/detail/ReviewDetailDialog.tsx b/web/src/components/overlay/detail/ReviewDetailDialog.tsx index f796f03f5..16050245c 100644 --- a/web/src/components/overlay/detail/ReviewDetailDialog.tsx +++ b/web/src/components/overlay/detail/ReviewDetailDialog.tsx @@ -20,7 +20,7 @@ import { Event } from "@/types/event"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { cn } from "@/lib/utils"; import { FrigatePlusDialog } from "../dialog/FrigatePlusDialog"; -import ObjectLifecycle from "./ObjectLifecycle"; +import TrackingDetails from "./TrackingDetails"; import Chip from "@/components/indicators/Chip"; import { FaDownload, FaImages, FaShareAlt } from "react-icons/fa"; import FrigatePlusIcon from "@/components/icons/FrigatePlusIcon"; @@ -411,7 +411,7 @@ export default function ReviewDetailDialog({ {pane == "details" && selectedEvent && (
- +
)} @@ -544,7 +544,7 @@ function EventItem({ - {t("itemMenu.viewObjectLifecycle.label")} + {t("itemMenu.viewTrackingDetails.label")} )} diff --git a/web/src/components/overlay/detail/SearchDetailDialog.tsx b/web/src/components/overlay/detail/SearchDetailDialog.tsx index 3a016588a..0c0339793 100644 --- a/web/src/components/overlay/detail/SearchDetailDialog.tsx +++ b/web/src/components/overlay/detail/SearchDetailDialog.tsx @@ -34,8 +34,7 @@ import { FaRegListAlt, FaVideo, } from "react-icons/fa"; -import { FaRotate } from "react-icons/fa6"; -import ObjectLifecycle from "./ObjectLifecycle"; +import TrackingDetails from "./TrackingDetails"; import { MobilePage, MobilePageContent, @@ -80,12 +79,13 @@ import FaceSelectionDialog from "../FaceSelectionDialog"; import { getTranslatedLabel } from "@/utils/i18n"; import { CgTranscript } from "react-icons/cg"; import { CameraNameLabel } from "@/components/camera/CameraNameLabel"; +import { PiPath } from "react-icons/pi"; const SEARCH_TABS = [ "details", "snapshot", "video", - "object_lifecycle", + "tracking_details", ] as const; export type SearchTab = (typeof SEARCH_TABS)[number]; @@ -160,7 +160,7 @@ export default function SearchDetailDialog({ } if (search.data.type != "object" || !search.has_clip) { - const index = views.indexOf("object_lifecycle"); + const index = views.indexOf("tracking_details"); views.splice(index, 1); } @@ -235,9 +235,7 @@ export default function SearchDetailDialog({ {item == "details" && } {item == "snapshot" && } {item == "video" && } - {item == "object_lifecycle" && ( - - )} + {item == "tracking_details" && }
{t(`type.${item}`)}
))} @@ -268,8 +266,8 @@ export default function SearchDetailDialog({ /> )} {page == "video" && } - {page == "object_lifecycle" && ( - >; }; -export default function ObjectLifecycle({ +export default function TrackingDetails({ className, event, fullscreen = false, setPane, -}: ObjectLifecycleProps) { +}: TrackingDetailsProps) { const { t } = useTranslation(["views/explore"]); - const { data: eventSequence } = useSWR([ + const { data: eventSequence } = useSWR([ "timeline", { source_id: event.id, @@ -447,7 +457,7 @@ export default function ObjectLifecycle({
- {t("objectLifecycle.noImageFound")} + {t("trackingDetails.noImageFound")}
)} @@ -558,7 +568,7 @@ export default function ObjectLifecycle({ } >
- {t("objectLifecycle.createObjectMask")} + {t("trackingDetails.createObjectMask")}
@@ -568,7 +578,7 @@ export default function ObjectLifecycle({
- {t("objectLifecycle.title")} + {t("trackingDetails.title")}
@@ -576,7 +586,7 @@ export default function ObjectLifecycle({
- {t("objectLifecycle.scrollViewTips")} + {t("trackingDetails.scrollViewTips")}
- {t("objectLifecycle.count", { + {t("trackingDetails.count", { first: selectedIndex + 1, second: eventSequence?.length ?? 0, })} @@ -605,7 +615,7 @@ export default function ObjectLifecycle({
{config?.cameras[event.camera]?.onvif.autotracking.enabled_in_config && (
- {t("objectLifecycle.autoTrackingTips")} + {t("trackingDetails.autoTrackingTips")}
)} {showControls && ( @@ -756,7 +766,7 @@ export default function ObjectLifecycle({ } type GetTimelineIconParams = { - lifecycleItem: ObjectLifecycleSequence; + lifecycleItem: TrackingDetailsSequence; className?: string; }; @@ -794,7 +804,7 @@ export function LifecycleIcon({ } type LifecycleIconRowProps = { - item: ObjectLifecycleSequence; + item: TrackingDetailsSequence; isActive?: boolean; formattedEventTimestamp: string; ratio: string; @@ -816,7 +826,11 @@ function LifecycleIconRow({ setSelectedZone, getZoneColor, }: LifecycleIconRowProps) { - const { t } = useTranslation(["views/explore"]); + const { t } = useTranslation(["views/explore", "components/player"]); + const { data: config } = useSWR("config"); + const [isOpen, setIsOpen] = useState(false); + + const navigate = useNavigate(); return (
- {t("objectLifecycle.lifecycleItemDesc.header.ratio")} + {t("trackingDetails.lifecycleItemDesc.header.ratio")} {ratio}
- {t("objectLifecycle.lifecycleItemDesc.header.area")} + {t("trackingDetails.lifecycleItemDesc.header.area")} {areaPx !== undefined && areaPct !== undefined ? ( @@ -903,7 +917,69 @@ function LifecycleIconRow({
-
{formattedEventTimestamp}
+
+
{formattedEventTimestamp}
+ {(config?.plus?.enabled || item.data.box) && ( + + +
+ +
+
+ + + {config?.plus?.enabled && ( + { + const resp = await axios.post( + `/${item.camera}/plus/${item.timestamp}`, + ); + + if (resp && resp.status == 200) { + toast.success( + t("toast.success.submittedFrigatePlus", { + ns: "components/player", + }), + { + position: "top-center", + }, + ); + } else { + toast.success( + t("toast.error.submitFrigatePlusFailed", { + ns: "components/player", + }), + { + position: "top-center", + }, + ); + } + }} + > + {t("itemMenu.submitToPlus.label")} + + )} + {item.data.box && ( + { + setIsOpen(false); + setTimeout(() => { + navigate( + `/settings?page=masksAndZones&camera=${item.camera}&object_mask=${item.data.box}`, + ); + }, 0); + }} + > + {t("trackingDetails.createObjectMask")} + + )} + + +
+ )} +
diff --git a/web/src/components/player/dynamic/DynamicVideoController.ts b/web/src/components/player/dynamic/DynamicVideoController.ts index d4d7d4a2b..1afb30efa 100644 --- a/web/src/components/player/dynamic/DynamicVideoController.ts +++ b/web/src/components/player/dynamic/DynamicVideoController.ts @@ -1,7 +1,7 @@ import { Recording } from "@/types/record"; import { DynamicPlayback } from "@/types/playback"; import { PreviewController } from "../PreviewPlayer"; -import { TimeRange, ObjectLifecycleSequence } from "@/types/timeline"; +import { TimeRange, TrackingDetailsSequence } from "@/types/timeline"; import { calculateInpointOffset } from "@/utils/videoUtil"; type PlayerMode = "playback" | "scrubbing"; @@ -12,7 +12,7 @@ export class DynamicVideoController { private playerController: HTMLVideoElement; private previewController: PreviewController; private setNoRecording: (noRecs: boolean) => void; - private setFocusedItem: (timeline: ObjectLifecycleSequence) => void; + private setFocusedItem: (timeline: TrackingDetailsSequence) => void; private playerMode: PlayerMode = "playback"; // playback @@ -29,7 +29,7 @@ export class DynamicVideoController { annotationOffset: number, defaultMode: PlayerMode, setNoRecording: (noRecs: boolean) => void, - setFocusedItem: (timeline: ObjectLifecycleSequence) => void, + setFocusedItem: (timeline: TrackingDetailsSequence) => void, ) { this.camera = camera; this.playerController = playerController; @@ -132,7 +132,7 @@ export class DynamicVideoController { }); } - seekToTimelineItem(timeline: ObjectLifecycleSequence) { + seekToTimelineItem(timeline: TrackingDetailsSequence) { this.playerController.pause(); this.seekToTimestamp(timeline.timestamp + this.annotationOffset); this.setFocusedItem(timeline); diff --git a/web/src/components/timeline/DetailStream.tsx b/web/src/components/timeline/DetailStream.tsx index 727f1bbed..b0749ae13 100644 --- a/web/src/components/timeline/DetailStream.tsx +++ b/web/src/components/timeline/DetailStream.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useRef, useState } from "react"; -import { ObjectLifecycleSequence } from "@/types/timeline"; +import { TrackingDetailsSequence } from "@/types/timeline"; import { getLifecycleItemDescription } from "@/utils/lifecycleUtil"; import { useDetailStream } from "@/context/detail-stream-context"; import scrollIntoView from "scroll-into-view-if-needed"; @@ -430,7 +430,8 @@ function EventList({ }: EventListProps) { const { data: config } = useSWR("config"); - const { selectedObjectIds, toggleObjectSelection } = useDetailStream(); + const { selectedObjectIds, setSelectedObjectIds, toggleObjectSelection } = + useDetailStream(); const isSelected = selectedObjectIds.includes(event.id); @@ -438,13 +439,19 @@ function EventList({ const handleObjectSelect = (event: Event | undefined) => { if (event) { - // onSeek(event.start_time ?? 0); - toggleObjectSelection(event.id); + setSelectedObjectIds([]); + setSelectedObjectIds([event.id]); + onSeek(event.start_time); } else { - toggleObjectSelection(undefined); + setSelectedObjectIds([]); } }; + const handleTimelineClick = (ts: number, play?: boolean) => { + handleObjectSelect(event); + onSeek(ts, play); + }; + // Clear selection when effectiveTime has passed this event's end_time useEffect(() => { if (isSelected && effectiveTime && event.end_time) { @@ -468,11 +475,6 @@ function EventList({ isSelected ? "bg-secondary-highlight" : "outline-transparent duration-500", - !isSelected && - (effectiveTime ?? 0) >= (event.start_time ?? 0) - 0.5 && - (effectiveTime ?? 0) <= - (event.end_time ?? event.start_time ?? 0) + 0.5 && - "bg-secondary-highlight", )} >
@@ -480,12 +482,18 @@ function EventList({
= (event.start_time ?? 0) - 0.5 && + (effectiveTime ?? 0) <= + (event.end_time ?? event.start_time ?? 0) + 0.5 + ? "bg-selected" + : "bg-muted-foreground", )} onClick={(e) => { e.stopPropagation(); - handleObjectSelect(isSelected ? undefined : event); + onSeek(event.start_time); + handleObjectSelect(event); }} + role="button" > {getIconForLabel( event.sub_label ? event.label + "-verified" : event.label, @@ -496,7 +504,8 @@ function EventList({ className="flex flex-1 items-center gap-2" onClick={(e) => { e.stopPropagation(); - onSeek(event.start_time ?? 0); + onSeek(event.start_time); + handleObjectSelect(event); }} role="button" > @@ -532,8 +541,10 @@ function EventList({
@@ -542,10 +553,11 @@ function EventList({ } type LifecycleItemProps = { - item: ObjectLifecycleSequence; + item: TrackingDetailsSequence; isActive?: boolean; onSeek?: (timestamp: number, play?: boolean) => void; effectiveTime?: number; + isTimelineActive?: boolean; }; function LifecycleItem({ @@ -553,6 +565,7 @@ function LifecycleItem({ isActive, onSeek, effectiveTime, + isTimelineActive = false, }: LifecycleItemProps) { const { t } = useTranslation("views/events"); const { data: config } = useSWR("config"); @@ -605,7 +618,7 @@ function LifecycleItem({
{ - onSeek?.(item.timestamp ?? 0, false); + onSeek?.(item.timestamp, false); }} className={cn( "flex cursor-pointer items-center gap-2 text-sm text-primary-variant", @@ -617,8 +630,9 @@ function LifecycleItem({
= (item?.timestamp ?? 0)) && + isTimelineActive && "fill-selected duration-300", )} /> @@ -636,14 +650,14 @@ function LifecycleItem({
- {t("objectLifecycle.lifecycleItemDesc.header.ratio")} + {t("trackingDetails.lifecycleItemDesc.header.ratio")} {ratio}
- {t("objectLifecycle.lifecycleItemDesc.header.area")} + {t("trackingDetails.lifecycleItemDesc.header.area")} {areaPx !== undefined && areaPct !== undefined ? ( @@ -673,13 +687,17 @@ function ObjectTimeline({ eventId, onSeek, effectiveTime, + startTime, + endTime, }: { eventId: string; onSeek: (ts: number, play?: boolean) => void; effectiveTime?: number; + startTime?: number; + endTime?: number; }) { const { t } = useTranslation("views/events"); - const { data: timeline, isValidating } = useSWR([ + const { data: timeline, isValidating } = useSWR([ "timeline", { source_id: eventId, @@ -698,9 +716,17 @@ function ObjectTimeline({ ); } + // Check if current time is within the event's start/stop range + const isWithinEventRange = + effectiveTime !== undefined && + startTime !== undefined && + endTime !== undefined && + effectiveTime >= startTime && + effectiveTime <= endTime; + // Calculate how far down the blue line should extend based on effectiveTime const calculateLineHeight = () => { - if (!timeline || timeline.length === 0) return 0; + if (!timeline || timeline.length === 0 || !isWithinEventRange) return 0; const currentTime = effectiveTime ?? 0; @@ -742,15 +768,19 @@ function ObjectTimeline({ ); }; - const blueLineHeight = calculateLineHeight(); + const activeLineHeight = calculateLineHeight(); return (
-
+ {isWithinEventRange && ( +
+ )}
{timeline.map((event, idx) => { const isActive = @@ -763,6 +793,7 @@ function ObjectTimeline({ onSeek={onSeek} isActive={isActive} effectiveTime={effectiveTime} + isTimelineActive={isWithinEventRange} /> ); })} diff --git a/web/src/components/ui/carousel.tsx b/web/src/components/ui/carousel.tsx index ea38705ee..98b7d6cbd 100644 --- a/web/src/components/ui/carousel.tsx +++ b/web/src/components/ui/carousel.tsx @@ -212,13 +212,13 @@ const CarouselPrevious = React.forwardRef< : "-top-12 left-1/2 -translate-x-1/2 rotate-90", className, )} - aria-label={t("objectLifecycle.carousel.previous")} + aria-label={t("trackingDetails.carousel.previous")} disabled={!canScrollPrev} onClick={scrollPrev} {...props} > - {t("objectLifecycle.carousel.previous")} + {t("trackingDetails.carousel.previous")} ); }); @@ -243,13 +243,13 @@ const CarouselNext = React.forwardRef< : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90", className, )} - aria-label={t("objectLifecycle.carousel.next")} + aria-label={t("trackingDetails.carousel.next")} disabled={!canScrollNext} onClick={scrollNext} {...props} > - {t("objectLifecycle.carousel.next")} + {t("trackingDetails.carousel.next")} ); }); diff --git a/web/src/context/detail-stream-context.tsx b/web/src/context/detail-stream-context.tsx index a148833e7..c1faae12c 100644 --- a/web/src/context/detail-stream-context.tsx +++ b/web/src/context/detail-stream-context.tsx @@ -7,6 +7,7 @@ export interface DetailStreamContextType { currentTime: number; camera: string; annotationOffset: number; // milliseconds + setSelectedObjectIds: React.Dispatch>; setAnnotationOffset: (ms: number) => void; toggleObjectSelection: (id: string | undefined) => void; isDetailMode: boolean; @@ -69,6 +70,7 @@ export function DetailStreamProvider({ camera, annotationOffset, setAnnotationOffset, + setSelectedObjectIds, toggleObjectSelection, isDetailMode, }; diff --git a/web/src/types/timeline.ts b/web/src/types/timeline.ts index 561e64f4f..4551937d9 100644 --- a/web/src/types/timeline.ts +++ b/web/src/types/timeline.ts @@ -10,7 +10,7 @@ export enum LifecycleClassType { PATH_POINT = "path_point", } -export type ObjectLifecycleSequence = { +export type TrackingDetailsSequence = { camera: string; timestamp: number; data: { @@ -38,5 +38,5 @@ export type Position = { x: number; y: number; timestamp: number; - lifecycle_item?: ObjectLifecycleSequence; + lifecycle_item?: TrackingDetailsSequence; }; diff --git a/web/src/utils/lifecycleUtil.ts b/web/src/utils/lifecycleUtil.ts index e0016ccd8..fa83436c5 100644 --- a/web/src/utils/lifecycleUtil.ts +++ b/web/src/utils/lifecycleUtil.ts @@ -1,10 +1,10 @@ -import { ObjectLifecycleSequence } from "@/types/timeline"; +import { TrackingDetailsSequence } from "@/types/timeline"; import { t } from "i18next"; import { getTranslatedLabel } from "./i18n"; import { capitalizeFirstLetter } from "./stringUtil"; export function getLifecycleItemDescription( - lifecycleItem: ObjectLifecycleSequence, + lifecycleItem: TrackingDetailsSequence, ) { const rawLabel = Array.isArray(lifecycleItem.data.sub_label) ? lifecycleItem.data.sub_label[0] @@ -16,23 +16,23 @@ export function getLifecycleItemDescription( switch (lifecycleItem.class_type) { case "visible": - return t("objectLifecycle.lifecycleItemDesc.visible", { + return t("trackingDetails.lifecycleItemDesc.visible", { ns: "views/explore", label, }); case "entered_zone": - return t("objectLifecycle.lifecycleItemDesc.entered_zone", { + return t("trackingDetails.lifecycleItemDesc.entered_zone", { ns: "views/explore", label, zones: lifecycleItem.data.zones.join(" and ").replaceAll("_", " "), }); case "active": - return t("objectLifecycle.lifecycleItemDesc.active", { + return t("trackingDetails.lifecycleItemDesc.active", { ns: "views/explore", label, }); case "stationary": - return t("objectLifecycle.lifecycleItemDesc.stationary", { + return t("trackingDetails.lifecycleItemDesc.stationary", { ns: "views/explore", label, }); @@ -43,7 +43,7 @@ export function getLifecycleItemDescription( lifecycleItem.data.attribute == "license_plate" ) { title = t( - "objectLifecycle.lifecycleItemDesc.attribute.faceOrLicense_plate", + "trackingDetails.lifecycleItemDesc.attribute.faceOrLicense_plate", { ns: "views/explore", label, @@ -53,7 +53,7 @@ export function getLifecycleItemDescription( }, ); } else { - title = t("objectLifecycle.lifecycleItemDesc.attribute.other", { + title = t("trackingDetails.lifecycleItemDesc.attribute.other", { ns: "views/explore", label: lifecycleItem.data.label, attribute: getTranslatedLabel( @@ -64,17 +64,17 @@ export function getLifecycleItemDescription( return title; } case "gone": - return t("objectLifecycle.lifecycleItemDesc.gone", { + return t("trackingDetails.lifecycleItemDesc.gone", { ns: "views/explore", label, }); case "heard": - return t("objectLifecycle.lifecycleItemDesc.heard", { + return t("trackingDetails.lifecycleItemDesc.heard", { ns: "views/explore", label, }); case "external": - return t("objectLifecycle.lifecycleItemDesc.external", { + return t("trackingDetails.lifecycleItemDesc.external", { ns: "views/explore", label, }); diff --git a/web/src/views/explore/ExploreView.tsx b/web/src/views/explore/ExploreView.tsx index f78f59256..cb53e74e0 100644 --- a/web/src/views/explore/ExploreView.tsx +++ b/web/src/views/explore/ExploreView.tsx @@ -232,8 +232,8 @@ function ExploreThumbnailImage({ } }; - const handleShowObjectLifecycle = () => { - onSelectSearch(event, false, "object_lifecycle"); + const handleShowTrackingDetails = () => { + onSelectSearch(event, false, "tracking_details"); }; const handleShowSnapshot = () => { @@ -251,7 +251,7 @@ function ExploreThumbnailImage({ searchResult={event} findSimilar={handleFindSimilar} refreshResults={mutate} - showObjectLifecycle={handleShowObjectLifecycle} + showTrackingDetails={handleShowTrackingDetails} showSnapshot={handleShowSnapshot} addTrigger={handleAddTrigger} isContextMenu={true} diff --git a/web/src/views/search/SearchView.tsx b/web/src/views/search/SearchView.tsx index 6f2b3f86c..4a824b4e8 100644 --- a/web/src/views/search/SearchView.tsx +++ b/web/src/views/search/SearchView.tsx @@ -644,8 +644,8 @@ export default function SearchView({ } }} refreshResults={refresh} - showObjectLifecycle={() => - onSelectSearch(value, false, "object_lifecycle") + showTrackingDetails={() => + onSelectSearch(value, false, "tracking_details") } showSnapshot={() => onSelectSearch(value, false, "snapshot")