This commit is contained in:
Josh Hawkins 2025-10-16 14:55:36 -05:00
parent 83354be7f6
commit 2827561238
2 changed files with 34 additions and 25 deletions

View File

@ -5,7 +5,10 @@ import { getLifecycleItemDescription } from "@/utils/lifecycleUtil";
import { useDetailStream } from "@/context/detail-stream-context"; import { useDetailStream } from "@/context/detail-stream-context";
import scrollIntoView from "scroll-into-view-if-needed"; import scrollIntoView from "scroll-into-view-if-needed";
import useUserInteraction from "@/hooks/use-user-interaction"; import useUserInteraction from "@/hooks/use-user-interaction";
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil"; import {
formatUnixTimestampToDateTime,
formatSecondsToDuration,
} from "@/utils/dateUtil";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import AnnotationOffsetSlider from "@/components/overlay/detail/AnnotationOffsetSlider"; import AnnotationOffsetSlider from "@/components/overlay/detail/AnnotationOffsetSlider";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
@ -13,7 +16,7 @@ import useSWR from "swr";
import ActivityIndicator from "../indicators/activity-indicator"; import ActivityIndicator from "../indicators/activity-indicator";
import { Event } from "@/types/event"; import { Event } from "@/types/event";
import { getIconForLabel } from "@/utils/iconUtil"; import { getIconForLabel } from "@/utils/iconUtil";
import { ReviewSegment, REVIEW_PADDING } from "@/types/review"; import { ReviewSegment } from "@/types/review";
import { import {
Collapsible, Collapsible,
CollapsibleTrigger, CollapsibleTrigger,
@ -49,7 +52,6 @@ export default function DetailStream({
}); });
const effectiveTime = currentTime + annotationOffset / 1000; const effectiveTime = currentTime + annotationOffset / 1000;
const PAD = 0; // REVIEW_PADDING ?? 2;
const [upload, setUpload] = useState<Event | undefined>(undefined); const [upload, setUpload] = useState<Event | undefined>(undefined);
// Ensure we initialize the active review when reviewItems first arrive. // Ensure we initialize the active review when reviewItems first arrive.
@ -64,8 +66,8 @@ export default function DetailStream({
let closest: { r: ReviewSegment; diff: number } | undefined; let closest: { r: ReviewSegment; diff: number } | undefined;
for (const r of reviewItems) { for (const r of reviewItems) {
const start = (r.start_time ?? 0) - PAD; const start = r.start_time ?? 0;
const end = (r.end_time ?? r.start_time ?? start) + PAD; const end = r.end_time ?? r.start_time ?? start;
if (effectiveTime >= start && effectiveTime <= end) { if (effectiveTime >= start && effectiveTime <= end) {
target = r; target = r;
break; break;
@ -78,12 +80,12 @@ export default function DetailStream({
if (!target && closest) target = closest.r; if (!target && closest) target = closest.r;
if (target) { if (target) {
const start = (target.start_time ?? 0) - PAD; const start = target.start_time ?? 0;
setActiveReviewId( setActiveReviewId(
`review-${target.id ?? target.start_time ?? Math.floor(start)}`, `review-${target.id ?? target.start_time ?? Math.floor(start)}`,
); );
} }
}, [reviewItems, activeReviewId, effectiveTime, PAD]); }, [reviewItems, activeReviewId, effectiveTime]);
// Auto-scroll to current time // Auto-scroll to current time
useEffect(() => { useEffect(() => {
@ -99,8 +101,8 @@ export default function DetailStream({
let closest: { r: ReviewSegment; diff: number } | undefined; let closest: { r: ReviewSegment; diff: number } | undefined;
for (const r of items) { for (const r of items) {
const start = (r.start_time ?? 0) - PAD; const start = r.start_time ?? 0;
const end = (r.end_time ?? r.start_time ?? start) + PAD; const end = r.end_time ?? r.start_time ?? start;
if (effectiveTime >= start && effectiveTime <= end) { if (effectiveTime >= start && effectiveTime <= end) {
target = r; target = r;
break; break;
@ -113,7 +115,7 @@ export default function DetailStream({
if (!target && closest) target = closest.r; if (!target && closest) target = closest.r;
if (target) { if (target) {
const start = (target.start_time ?? 0) - PAD; const start = target.start_time ?? 0;
const id = `review-${target.id ?? target.start_time ?? Math.floor(start)}`; const id = `review-${target.id ?? target.start_time ?? Math.floor(start)}`;
const element = scrollRef.current.querySelector( const element = scrollRef.current.querySelector(
`[data-review-id="${id}"]`, `[data-review-id="${id}"]`,
@ -132,15 +134,14 @@ export default function DetailStream({
annotationOffset, annotationOffset,
userInteracting, userInteracting,
setProgrammaticScroll, setProgrammaticScroll,
PAD,
]); ]);
// Auto-select active review based on effectiveTime (if inside a review range) // Auto-select active review based on effectiveTime (if inside a review range)
useEffect(() => { useEffect(() => {
if (!reviewItems || reviewItems.length === 0) return; if (!reviewItems || reviewItems.length === 0) return;
for (const r of reviewItems) { for (const r of reviewItems) {
const start = (r.start_time ?? 0) - PAD; const start = r.start_time ?? 0;
const end = (r.end_time ?? r.start_time ?? start) + PAD; const end = r.end_time ?? r.start_time ?? start;
if (effectiveTime >= start && effectiveTime <= end) { if (effectiveTime >= start && effectiveTime <= end) {
setActiveReviewId( setActiveReviewId(
`review-${r.id ?? r.start_time ?? Math.floor(start)}`, `review-${r.id ?? r.start_time ?? Math.floor(start)}`,
@ -148,7 +149,7 @@ export default function DetailStream({
return; return;
} }
} }
}, [effectiveTime, reviewItems, PAD]); }, [effectiveTime, reviewItems]);
if (!config) { if (!config) {
return <ActivityIndicator />; return <ActivityIndicator />;
@ -173,7 +174,7 @@ export default function DetailStream({
</div> </div>
) : ( ) : (
reviewItems?.map((review: ReviewSegment) => { reviewItems?.map((review: ReviewSegment) => {
const id = `review-${review.id ?? review.start_time ?? Math.floor((review.start_time ?? 0) - PAD)}`; const id = `review-${review.id ?? review.start_time ?? Math.floor(review.start_time ?? 0)}`;
return ( return (
<ReviewGroup <ReviewGroup
key={id} key={id}
@ -219,18 +220,14 @@ function ReviewGroup({
effectiveTime, effectiveTime,
}: ReviewGroupProps) { }: ReviewGroupProps) {
const { t } = useTranslation("views/events"); const { t } = useTranslation("views/events");
const PAD = REVIEW_PADDING ?? 2; const start = review.start_time ?? 0;
// derive start timestamp from the review
const start = (review.start_time ?? 0) - PAD;
// display time first in the header
const displayTime = formatUnixTimestampToDateTime(start, { const displayTime = formatUnixTimestampToDateTime(start, {
timezone: config.ui.timezone, timezone: config.ui.timezone,
date_format: date_format:
config.ui.time_format == "24hour" config.ui.time_format == "24hour"
? t("time.formattedTimestamp.24hour", { ns: "common" }) ? t("time.formattedTimestampHourMinuteSecond.24hour", { ns: "common" })
: t("time.formattedTimestamp.12hour", { ns: "common" }), : t("time.formattedTimestampHourMinuteSecond.12hour", { ns: "common" }),
time_style: "medium", time_style: "medium",
date_style: "medium", date_style: "medium",
}); });
@ -268,6 +265,13 @@ function ReviewGroup({
} }
}, [review, t, fetchedEvents]); }, [review, t, fetchedEvents]);
const reviewDuration =
review.end_time != null
? formatSecondsToDuration(
Math.max(0, Math.floor((review.end_time ?? 0) - start)),
)
: null;
return ( return (
<div <div
data-review-id={id} data-review-id={id}
@ -287,6 +291,11 @@ function ReviewGroup({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="flex flex-col"> <div className="flex flex-col">
<div className="text-sm font-medium">{displayTime}</div> <div className="text-sm font-medium">{displayTime}</div>
{reviewDuration && (
<div className="text-xs text-muted-foreground">
{reviewDuration}
</div>
)}
<div className="text-xs text-muted-foreground">{reviewInfo}</div> <div className="text-xs text-muted-foreground">{reviewInfo}</div>
</div> </div>
</div> </div>
@ -398,7 +407,7 @@ function EventCollapsible({
event.id != selectedObjectId && event.id != selectedObjectId &&
(effectiveTime ?? 0) >= (event.start_time ?? 0) && (effectiveTime ?? 0) >= (event.start_time ?? 0) &&
(effectiveTime ?? 0) <= (event.end_time ?? event.start_time ?? 0) && (effectiveTime ?? 0) <= (event.end_time ?? event.start_time ?? 0) &&
"bg-secondary outline-[1px] -outline-offset-[0.8px] outline-primary/40", "bg-secondary-highlight outline-[1.5px] -outline-offset-[1.1px] outline-primary/40",
)} )}
> >
<div className="flex w-full items-center justify-between"> <div className="flex w-full items-center justify-between">

View File

@ -36,8 +36,8 @@
--secondary-foreground: hsl(222.2, 17.4%, 36.2%); --secondary-foreground: hsl(222.2, 17.4%, 36.2%);
--secondary-foreground: 222.2 17.4% 36.2%; --secondary-foreground: 222.2 17.4% 36.2%;
--secondary-highlight: hsl(0, 0%, 94%); --secondary-highlight: hsl(210, 17.4%, 94%);
--secondary-highlight: 0 0% 94%; --secondary-highlight: 210 17.4% 94%;
--neutral: hsl(0, 0%, 45.1%); --neutral: hsl(0, 0%, 45.1%);
--neutral: 0 0% 45.1%; --neutral: 0 0% 45.1%;