diff --git a/web/src/components/overlay/detail/SearchDetailDialog.tsx b/web/src/components/overlay/detail/SearchDetailDialog.tsx index c38c184af..1ee4819ae 100644 --- a/web/src/components/overlay/detail/SearchDetailDialog.tsx +++ b/web/src/components/overlay/detail/SearchDetailDialog.tsx @@ -31,10 +31,9 @@ import { FaDownload, FaHistory, FaImage, - FaRegListAlt, - FaVideo, } from "react-icons/fa"; -import TrackingDetails from "./TrackingDetails"; +import { TrackingDetails } from "./TrackingDetails"; +import { DetailStreamProvider } from "@/context/detail-stream-context"; import { MobilePage, MobilePageContent, @@ -80,13 +79,9 @@ import { getTranslatedLabel } from "@/utils/i18n"; import { CgTranscript } from "react-icons/cg"; import { CameraNameLabel } from "@/components/camera/CameraNameLabel"; import { PiPath } from "react-icons/pi"; +import Heading from "@/components/ui/heading"; -const SEARCH_TABS = [ - "details", - "snapshot", - "video", - "tracking_details", -] as const; +const SEARCH_TABS = ["snapshot", "tracking_details"] as const; export type SearchTab = (typeof SEARCH_TABS)[number]; type SearchDetailDialogProps = { @@ -150,16 +145,6 @@ export default function SearchDetailDialog({ 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) { const index = views.indexOf("tracking_details"); views.splice(index, 1); @@ -174,7 +159,7 @@ export default function SearchDetailDialog({ } if (!searchTabs.includes(pageToggle)) { - setSearchPage("details"); + setSearchPage("snapshot"); } }, [pageToggle, searchTabs, setSearchPage]); @@ -196,16 +181,20 @@ export default function SearchDetailDialog({ {Object.values(searchTabs).map((item) => ( - {item == "details" && } {item == "snapshot" && } - {item == "video" && } {item == "tracking_details" && } -
{t(`type.${item}`)}
+
+ {item === "snapshot" + ? search?.has_snapshot + ? t("type.snapshot") + : t("type.thumbnail") + : t(`type.${item}`)} +
))} @@ -227,186 +216,191 @@ export default function SearchDetailDialog({ const Description = isDesktop ? DialogDescription : MobilePageDescription; return ( - - -
- {t("trackedObjectDetails")} - - {t("trackedObjectDetails")} - -
- {isDesktop ? ( - page === "tracking_details" ? ( - - ) : ( -
-
- {page === "snapshot" && search.has_snapshot && ( - { - search.plus_id = "new_upload"; - }} - /> - )} - {page === "video" && search.has_clip && ( - - )} - {(page === "details" || - (!search.has_snapshot && page === "snapshot") || - (!search.has_clip && page === "video")) && ( - - )} -
-
- {tabsComponent} -
- {page == "details" && ( - +
+ {t("trackedObjectDetails")} + + {t("trackedObjectDetails")} + +
+ {isDesktop ? ( + page === "tracking_details" ? ( + + ) : ( +
+
+ {page === "snapshot" && search.has_snapshot && ( + { + search.plus_id = "new_upload"; + }} /> )} - {page == "snapshot" && ( - - )} - {page == "video" && ( - )}
+
+ {tabsComponent} +
+ {page == "snapshot" && ( + + )} +
+
-
- ) - ) : ( - <> - -
- { - if (value) { - setPageToggle(value); - } - }} - > - {Object.values(searchTabs).map((item) => ( - - {item == "details" && } - {item == "snapshot" && } - {item == "video" && } - {item == "tracking_details" && ( - - )} -
- {t(`type.${item}`)} -
-
- ))} -
- -
-
- {page == "details" && ( - - )} - {page == "snapshot" && ( - { - search.plus_id = "new_upload"; - }} - /> - )} - {page == "video" && } - {page == "tracking_details" && ( - - )} - - )} - - + ) + ) : ( + <> + +
+ { + if (value) { + setPageToggle(value); + } + }} + > + {Object.values(searchTabs).map((item) => ( + + {item == "snapshot" && } + {item == "tracking_details" && ( + + )} +
+ {t(`type.${item}`)} +
+
+ ))} +
+ +
+
+ {page == "snapshot" && ( + <> + {search.has_snapshot && ( + { + search.plus_id = "new_upload"; + }} + /> + )} + {page == "snapshot" && !search.has_snapshot && ( + + )} + + {t("type.details")} + + + + )} + {page == "tracking_details" && ( + + )} + + )} + + + ); } @@ -1305,7 +1299,7 @@ export function ObjectSnapshotTab({ search.label != "on_demand" && ( -
+
{t("explore.plus.submitToPlus.label")}
@@ -1314,7 +1308,7 @@ export function ObjectSnapshotTab({
-
+
{state == "reviewing" && ( <>
diff --git a/web/src/components/overlay/detail/TrackingDetails.tsx b/web/src/components/overlay/detail/TrackingDetails.tsx index 3723358a8..f3f58f9a6 100644 --- a/web/src/components/overlay/detail/TrackingDetails.tsx +++ b/web/src/components/overlay/detail/TrackingDetails.tsx @@ -8,20 +8,7 @@ import Heading from "@/components/ui/heading"; import { FrigateConfig } from "@/types/frigateConfig"; import { formatUnixTimestampToDateTime } from "@/utils/dateUtil"; import { getIconForLabel } from "@/utils/iconUtil"; -import { - 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 { LuCircle, LuSettings } from "react-icons/lu"; import { cn } from "@/lib/utils"; import { Tooltip, @@ -43,17 +30,13 @@ import { } from "@/components/ui/dropdown-menu"; import { Link, useNavigate } from "react-router-dom"; import { getLifecycleItemDescription } from "@/utils/lifecycleUtil"; -import { IoPlayCircleOutline } from "react-icons/io5"; import { useTranslation } from "react-i18next"; import { getTranslatedLabel } from "@/utils/i18n"; import { Badge } from "@/components/ui/badge"; import { HiDotsHorizontal } from "react-icons/hi"; import axios from "axios"; import { toast } from "sonner"; -import { - DetailStreamProvider, - useDetailStream, -} from "@/context/detail-stream-context"; +import { useDetailStream } from "@/context/detail-stream-context"; import { isDesktop } from "react-device-detect"; type TrackingDetailsProps = { @@ -63,36 +46,16 @@ type TrackingDetailsProps = { tabs?: React.ReactNode; }; -// Wrapper component that provides DetailStreamContext -export default function TrackingDetails(props: TrackingDetailsProps) { - const [currentTime, setCurrentTime] = useState(props.event.start_time ?? 0); - - return ( - - - - ); -} - -// Inner component with access to DetailStreamContext -function TrackingDetailsInner({ +export function TrackingDetails({ className, event, tabs, - onTimeUpdate, -}: TrackingDetailsProps & { onTimeUpdate: (time: number) => void }) { +}: TrackingDetailsProps) { const videoRef = useRef(null); const { t } = useTranslation(["views/explore"]); - const { - setSelectedObjectIds, - annotationOffset, - setAnnotationOffset, - currentTime, - } = useDetailStream(); + const { setSelectedObjectIds, annotationOffset, setAnnotationOffset } = + useDetailStream(); + const [currentTime, setCurrentTime] = useState(event.start_time ?? 0); const { data: eventSequence } = useSWR([ "timeline", @@ -110,7 +73,7 @@ function TrackingDetailsInner({ ); const containerRef = useRef(null); - const [_selectedZone, _setSelectedZone] = useState(""); + const [_selectedZone, setSelectedZone] = useState(""); const [_lifecycleZones, setLifecycleZones] = useState([]); const [showControls, setShowControls] = useState(false); const [showZones, setShowZones] = useState(true); @@ -151,7 +114,7 @@ function TrackingDetailsInner({ const handleLifecycleClick = useCallback((item: TrackingDetailsSequence) => { const timestamp = item.timestamp ?? 0; setLifecycleZones(item.data.zones); - _setSelectedZone(""); + setSelectedZone(""); // Set the target timestamp to seek to setSeekToTimestamp(timestamp); @@ -287,8 +250,6 @@ function TrackingDetailsInner({ } }, [aspectRatio]); - // Container layout classes - no longer needed, handled in return JSX - const handleSeekToTime = useCallback((timestamp: number, _play?: boolean) => { // Set the target timestamp to seek to setSeekToTimestamp(timestamp); @@ -296,16 +257,16 @@ function TrackingDetailsInner({ const handleTimeUpdate = useCallback( (time: number) => { - // Convert video time to absolute timestamp const absoluteTime = time - REVIEW_PADDING + event.start_time; - onTimeUpdate(absoluteTime); + setCurrentTime(absoluteTime); }, - [event.start_time, onTimeUpdate], + [event.start_time], ); if (!config) { return ; } + return (
handleLifecycleClick(item)} - setSelectedZone={_setSelectedZone} + setSelectedZone={setSelectedZone} 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 ; - case "gone": - return ; - case "active": - return ; - case "stationary": - return ; - case "entered_zone": - return ; - case "attribute": - return lifecycleItem.data.attribute === "face" ? ( - - ) : lifecycleItem.data.attribute === "license_plate" ? ( - - ) : ( - - ); - case "heard": - return ; - case "external": - return ; - default: - return null; - } -} - type LifecycleIconRowProps = { item: TrackingDetailsSequence; isActive?: boolean; @@ -598,6 +524,8 @@ type LifecycleIconRowProps = { onClick: () => void; setSelectedZone: (z: string) => void; getZoneColor: (zoneName: string) => number[] | undefined; + effectiveTime?: number; + isTimelineActive?: boolean; }; function LifecycleIconRow({ @@ -610,6 +538,8 @@ function LifecycleIconRow({ onClick, setSelectedZone, getZoneColor, + effectiveTime, + isTimelineActive, }: LifecycleIconRowProps) { const { t } = useTranslation(["views/explore", "components/player"]); const { data: config } = useSWR("config"); @@ -632,7 +562,9 @@ function LifecycleIconRow({ = (item?.timestamp ?? 0)) && + isTimelineActive && + "fill-selected duration-300", )} />
diff --git a/web/src/context/detail-stream-context.tsx b/web/src/context/detail-stream-context.tsx index c1faae12c..ff909a30e 100644 --- a/web/src/context/detail-stream-context.tsx +++ b/web/src/context/detail-stream-context.tsx @@ -22,6 +22,7 @@ interface DetailStreamProviderProps { isDetailMode: boolean; currentTime: number; camera: string; + initialSelectedObjectIds?: string[]; } export function DetailStreamProvider({ @@ -29,8 +30,11 @@ export function DetailStreamProvider({ isDetailMode, currentTime, camera, + initialSelectedObjectIds, }: DetailStreamProviderProps) { - const [selectedObjectIds, setSelectedObjectIds] = useState([]); + const [selectedObjectIds, setSelectedObjectIds] = useState( + () => initialSelectedObjectIds ?? [], + ); const toggleObjectSelection = (id: string | undefined) => { if (id === undefined) {