From cfe4dc59b7cf86d161d3f17262d48b32da2df754 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Mon, 26 Feb 2024 14:59:08 -0600 Subject: [PATCH 1/4] make event bars clickable --- .../timeline/EventReviewTimeline.tsx | 4 ++- web/src/components/timeline/EventSegment.tsx | 14 +++++++++- web/src/views/events/DesktopEventView.tsx | 26 +++++++++++++------ 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/web/src/components/timeline/EventReviewTimeline.tsx b/web/src/components/timeline/EventReviewTimeline.tsx index d84b3ed75..d8c3a3250 100644 --- a/web/src/components/timeline/EventReviewTimeline.tsx +++ b/web/src/components/timeline/EventReviewTimeline.tsx @@ -102,7 +102,7 @@ export function EventReviewTimeline({ return ( ); }); @@ -122,6 +123,7 @@ export function EventReviewTimeline({ showMinimap, minimapStartTime, minimapEndTime, + events, ]); const segments = useMemo( diff --git a/web/src/components/timeline/EventSegment.tsx b/web/src/components/timeline/EventSegment.tsx index 7113e660e..f11f1ffca 100644 --- a/web/src/components/timeline/EventSegment.tsx +++ b/web/src/components/timeline/EventSegment.tsx @@ -1,7 +1,7 @@ import { useEventUtils } from "@/hooks/use-event-utils"; import { useSegmentUtils } from "@/hooks/use-segment-utils"; import { ReviewSegment, ReviewSeverity } from "@/types/review"; -import React, { useEffect, useMemo, useRef } from "react"; +import React, { RefObject, useEffect, useMemo, useRef } from "react"; type EventSegmentProps = { events: ReviewSegment[]; @@ -12,6 +12,7 @@ type EventSegmentProps = { minimapStartTime?: number; minimapEndTime?: number; severityType: ReviewSeverity; + contentRef: RefObject; }; type MinimapSegmentProps = { @@ -131,6 +132,7 @@ export function EventSegment({ minimapStartTime, minimapEndTime, severityType, + contentRef, }: EventSegmentProps) { const { getSeverity, @@ -269,6 +271,16 @@ export function EventSegment({ ${roundTop ? "rounded-tl-full rounded-tr-full" : ""} ${severityColors[severityValue]} `} + onClick={() => { + if (contentRef.current) { + const element = contentRef.current.querySelector( + `[data-segment-start="${segmentTime}"]` + ); + if (element instanceof HTMLElement) { + debounceScrollIntoView(element); + } + } + }} > )} diff --git a/web/src/views/events/DesktopEventView.tsx b/web/src/views/events/DesktopEventView.tsx index 631dcd83a..7d60aeca0 100644 --- a/web/src/views/events/DesktopEventView.tsx +++ b/web/src/views/events/DesktopEventView.tsx @@ -5,6 +5,7 @@ import EventReviewTimeline from "@/components/timeline/EventReviewTimeline"; import ActivityIndicator from "@/components/ui/activity-indicator"; import { Button } from "@/components/ui/button"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; +import { useEventUtils } from "@/hooks/use-event-utils"; import { FrigateConfig } from "@/types/frigateConfig"; import { ReviewFilter, ReviewSegment, ReviewSeverity } from "@/types/review"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; @@ -41,6 +42,7 @@ export default function DesktopEventView({ const { data: config } = useSWR("config"); const [severity, setSeverity] = useState("alert"); const contentRef = useRef(null); + const segmentDuration = 60; // review paging @@ -76,6 +78,11 @@ export default function DesktopEventView({ }; }, [reviewPages]); + const { alignDateToTimeline } = useEventUtils( + reviewItems.all, + segmentDuration + ); + const currentItems = useMemo(() => { const current = reviewItems[severity]; @@ -91,8 +98,8 @@ export default function DesktopEventView({ return false; } - return contentRef.current.scrollHeight > contentRef.current.clientHeight - }, [contentRef.current?.scrollHeight]) + return contentRef.current.scrollHeight > contentRef.current.clientHeight; + }, [contentRef.current?.scrollHeight]); // review interaction @@ -244,10 +251,7 @@ export default function DesktopEventView({
-
+
{hasUpdate && (
@@ -276,7 +280,10 @@ export default function DesktopEventView({
)} -
+
{currentItems ? ( currentItems.map((value, segIdx) => { const lastRow = segIdx == reviewItems[severity].length - 1; @@ -294,6 +301,9 @@ export default function DesktopEventView({ key={value.id} ref={lastRow ? lastReviewRef : minimapRef} data-start={value.start_time} + data-segment-start={ + alignDateToTimeline(value.start_time) - segmentDuration + } >
Date: Mon, 26 Feb 2024 16:38:08 -0600 Subject: [PATCH 2/4] outline and scroll when segment is clicked --- web/src/components/timeline/EventSegment.tsx | 45 ++++++++++++++++---- web/src/hooks/use-segment-utils.ts | 16 +++++++ web/src/views/events/DesktopEventView.tsx | 5 ++- 3 files changed, 55 insertions(+), 11 deletions(-) diff --git a/web/src/components/timeline/EventSegment.tsx b/web/src/components/timeline/EventSegment.tsx index f11f1ffca..671b66aea 100644 --- a/web/src/components/timeline/EventSegment.tsx +++ b/web/src/components/timeline/EventSegment.tsx @@ -1,7 +1,13 @@ import { useEventUtils } from "@/hooks/use-event-utils"; import { useSegmentUtils } from "@/hooks/use-segment-utils"; import { ReviewSegment, ReviewSeverity } from "@/types/review"; -import React, { RefObject, useEffect, useMemo, useRef } from "react"; +import React, { + RefObject, + useCallback, + useEffect, + useMemo, + useRef, +} from "react"; type EventSegmentProps = { events: ReviewSegment[]; @@ -139,6 +145,7 @@ export function EventSegment({ getReviewed, displaySeverityType, shouldShowRoundedCorners, + getEventStart, } = useSegmentUtils(segmentDuration, events, severityType); const { alignDateToTimeline } = useEventUtils(events, segmentDuration); @@ -155,6 +162,13 @@ export function EventSegment({ () => shouldShowRoundedCorners(segmentTime), [shouldShowRoundedCorners, segmentTime] ); + const startTimestamp = useMemo(() => { + const eventStart = getEventStart(segmentTime); + if (eventStart) { + console.log("event start: " + new Date(eventStart * 1000)); + return alignDateToTimeline(eventStart); + } + }, [getEventStart, segmentTime]); const timestamp = useMemo(() => new Date(segmentTime * 1000), [segmentTime]); const segmentKey = useMemo(() => segmentTime, [segmentTime]); @@ -231,6 +245,26 @@ export function EventSegment({ : "from-severity_alert-dimmed to-severity_alert", }; + const segmentClick = useCallback(() => { + if (contentRef.current && startTimestamp) { + console.log(new Date(startTimestamp * 1000)); + const element = contentRef.current.querySelector( + `[data-segment-start="${startTimestamp - segmentDuration}"]` + ); + if (element instanceof HTMLElement) { + debounceScrollIntoView(element); + element.classList.add("outline-4", "shadow-[0_0_6px_1px]"); + element.classList.remove("outline-0", "shadow-none"); + + // Remove the classes after a short timeout + setTimeout(() => { + element.classList.remove("outline-4", "shadow-[0_0_6px_1px]"); + element.classList.add("outline-0", "shadow-none"); + }, 3000); + } + } + }, [startTimestamp]); + return (
{ - if (contentRef.current) { - const element = contentRef.current.querySelector( - `[data-segment-start="${segmentTime}"]` - ); - if (element instanceof HTMLElement) { - debounceScrollIntoView(element); - } - } + segmentClick(); }} >
diff --git a/web/src/hooks/use-segment-utils.ts b/web/src/hooks/use-segment-utils.ts index 967bd3586..60da2fb35 100644 --- a/web/src/hooks/use-segment-utils.ts +++ b/web/src/hooks/use-segment-utils.ts @@ -156,6 +156,21 @@ export const useSegmentUtils = ( [events, getSegmentStart, getSegmentEnd, segmentDuration, severityType] ); + const getEventStart = useCallback( + (time: number): number => { + const matchingEvent = events.find((event) => { + return ( + time >= getSegmentStart(event.start_time) && + time < getSegmentEnd(event.end_time) && + event.severity == severityType + ); + }); + + return matchingEvent?.start_time ?? 0; + }, + [events, getSegmentStart, getSegmentEnd, severityType] + ); + return { getSegmentStart, getSegmentEnd, @@ -163,5 +178,6 @@ export const useSegmentUtils = ( displaySeverityType, getReviewed, shouldShowRoundedCorners, + getEventStart, }; }; \ No newline at end of file diff --git a/web/src/views/events/DesktopEventView.tsx b/web/src/views/events/DesktopEventView.tsx index 7d60aeca0..44e26a83d 100644 --- a/web/src/views/events/DesktopEventView.tsx +++ b/web/src/views/events/DesktopEventView.tsx @@ -281,7 +281,7 @@ export default function DesktopEventView({ )}
{currentItems ? ( @@ -304,6 +304,7 @@ export default function DesktopEventView({ data-segment-start={ alignDateToTimeline(value.start_time) - segmentDuration } + className="outline outline-destructive outline-offset-1 outline-0 rounded-lg shadow-none shadow-destructive transition-all duration-500" >
-
+
Date: Mon, 26 Feb 2024 17:08:33 -0600 Subject: [PATCH 3/4] match outline colors to event type --- web/src/components/player/LivePlayer.tsx | 2 +- web/src/components/timeline/EventSegment.tsx | 4 ++++ web/src/views/events/DesktopEventView.tsx | 2 +- web/tailwind.config.js | 5 +++++ 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/web/src/components/player/LivePlayer.tsx b/web/src/components/player/LivePlayer.tsx index 093410de0..0d9925f45 100644 --- a/web/src/components/player/LivePlayer.tsx +++ b/web/src/components/player/LivePlayer.tsx @@ -126,7 +126,7 @@ export default function LivePlayer({
diff --git a/web/src/components/timeline/EventSegment.tsx b/web/src/components/timeline/EventSegment.tsx index 671b66aea..da9b3aa61 100644 --- a/web/src/components/timeline/EventSegment.tsx +++ b/web/src/components/timeline/EventSegment.tsx @@ -253,6 +253,10 @@ export function EventSegment({ ); if (element instanceof HTMLElement) { debounceScrollIntoView(element); + element.classList.add( + `outline-severity_${severityType}`, + `shadow-severity_${severityType}` + ); element.classList.add("outline-4", "shadow-[0_0_6px_1px]"); element.classList.remove("outline-0", "shadow-none"); diff --git a/web/src/views/events/DesktopEventView.tsx b/web/src/views/events/DesktopEventView.tsx index 44e26a83d..08b5f5d78 100644 --- a/web/src/views/events/DesktopEventView.tsx +++ b/web/src/views/events/DesktopEventView.tsx @@ -304,7 +304,7 @@ export default function DesktopEventView({ data-segment-start={ alignDateToTimeline(value.start_time) - segmentDuration } - className="outline outline-destructive outline-offset-1 outline-0 rounded-lg shadow-none shadow-destructive transition-all duration-500" + className="outline outline-offset-1 outline-0 rounded-lg shadow-none transition-all duration-500" >
Date: Tue, 27 Feb 2024 09:51:41 -0600 Subject: [PATCH 4/4] hover thumbnails --- .../timeline/EventReviewTimeline.tsx | 60 +++++++++------- web/src/components/timeline/EventSegment.tsx | 72 ++++++++++++------- web/src/hooks/use-segment-utils.ts | 54 ++++++++++---- 3 files changed, 121 insertions(+), 65 deletions(-) diff --git a/web/src/components/timeline/EventReviewTimeline.tsx b/web/src/components/timeline/EventReviewTimeline.tsx index d8c3a3250..428a9ec37 100644 --- a/web/src/components/timeline/EventReviewTimeline.tsx +++ b/web/src/components/timeline/EventReviewTimeline.tsx @@ -10,6 +10,7 @@ import { import EventSegment from "./EventSegment"; import { useEventUtils } from "@/hooks/use-event-utils"; import { ReviewSegment, ReviewSeverity } from "@/types/review"; +import { TooltipProvider } from "../ui/tooltip"; export type EventReviewTimelineProps = { segmentDuration: number; @@ -212,39 +213,44 @@ export function EventReviewTimeline({ ]); return ( -
-
{segments}
- {showHandlebar && ( -
-
-
+ +
+
{segments}
+ {showHandlebar && ( +
+
+ className={`bg-destructive rounded-full mx-auto ${ + segmentDuration < 60 ? "w-20" : "w-16" + } h-5 flex items-center justify-center`} + > +
+
+
-
-
- )} -
+ )} +
+ ); } diff --git a/web/src/components/timeline/EventSegment.tsx b/web/src/components/timeline/EventSegment.tsx index da9b3aa61..30b11e200 100644 --- a/web/src/components/timeline/EventSegment.tsx +++ b/web/src/components/timeline/EventSegment.tsx @@ -1,3 +1,4 @@ +import { useApiHost } from "@/api"; import { useEventUtils } from "@/hooks/use-event-utils"; import { useSegmentUtils } from "@/hooks/use-segment-utils"; import { ReviewSegment, ReviewSeverity } from "@/types/review"; @@ -8,6 +9,8 @@ import React, { useMemo, useRef, } from "react"; +import { Tooltip, TooltipContent } from "../ui/tooltip"; +import { TooltipTrigger } from "@radix-ui/react-tooltip"; type EventSegmentProps = { events: ReviewSegment[]; @@ -146,6 +149,7 @@ export function EventSegment({ displaySeverityType, shouldShowRoundedCorners, getEventStart, + getEventThumbnail, } = useSegmentUtils(segmentDuration, events, severityType); const { alignDateToTimeline } = useEventUtils(events, segmentDuration); @@ -154,22 +158,35 @@ export function EventSegment({ () => getSeverity(segmentTime, displaySeverityType), [getSeverity, segmentTime] ); + const reviewed = useMemo( () => getReviewed(segmentTime), [getReviewed, segmentTime] ); - const { roundTop, roundBottom } = useMemo( + + const { + roundTopPrimary, + roundBottomPrimary, + roundTopSecondary, + roundBottomSecondary, + } = useMemo( () => shouldShowRoundedCorners(segmentTime), [shouldShowRoundedCorners, segmentTime] ); + const startTimestamp = useMemo(() => { const eventStart = getEventStart(segmentTime); if (eventStart) { - console.log("event start: " + new Date(eventStart * 1000)); return alignDateToTimeline(eventStart); } }, [getEventStart, segmentTime]); + const apiHost = useApiHost(); + + const eventThumbnail = useMemo(() => { + return getEventThumbnail(segmentTime); + }, [getEventThumbnail, segmentTime]); + const timestamp = useMemo(() => new Date(segmentTime * 1000), [segmentTime]); const segmentKey = useMemo(() => segmentTime, [segmentTime]); @@ -247,7 +264,6 @@ export function EventSegment({ const segmentClick = useCallback(() => { if (contentRef.current && startTimestamp) { - console.log(new Date(startTimestamp * 1000)); const element = contentRef.current.querySelector( `[data-segment-start="${startTimestamp - segmentDuration}"]` ); @@ -297,23 +313,31 @@ export function EventSegment({ {severity.map((severityValue, index) => ( {severityValue === displaySeverityType && ( -
+
{ - segmentClick(); - }} - >
-
+ className="mr-3 w-[8px] h-2 flex justify-left items-end" + data-severity={severityValue} + > + +
+
+ + + +
+ )} {severityValue !== displaySeverityType && ( @@ -321,11 +345,11 @@ export function EventSegment({
)} diff --git a/web/src/hooks/use-segment-utils.ts b/web/src/hooks/use-segment-utils.ts index 60da2fb35..57025a42b 100644 --- a/web/src/hooks/use-segment-utils.ts +++ b/web/src/hooks/use-segment-utils.ts @@ -84,7 +84,14 @@ export const useSegmentUtils = ( ); const shouldShowRoundedCorners = useCallback( - (segmentTime: number): { roundTop: boolean; roundBottom: boolean } => { + ( + segmentTime: number + ): { + roundTopPrimary: boolean; + roundBottomPrimary: boolean; + roundTopSecondary: boolean; + roundBottomSecondary: boolean; + } => { const prevSegmentTime = segmentTime - segmentDuration; const nextSegmentTime = segmentTime + segmentDuration; @@ -134,23 +141,26 @@ export const useSegmentUtils = ( ); }); - let roundTop = false; - let roundBottom = false; + let roundTopPrimary = false; + let roundBottomPrimary = false; + let roundTopSecondary = false; + let roundBottomSecondary = false; if (hasOverlappingSeverityEvent) { - roundBottom = !hasPrevSeverityEvent; - roundTop = !hasNextSeverityEvent; - } else if (hasOverlappingOtherEvent) { - roundBottom = !hasPrevOtherEvent; - roundTop = !hasNextOtherEvent; - } else { - roundTop = !hasNextSeverityEvent || !hasNextOtherEvent; - roundBottom = !hasPrevSeverityEvent || !hasPrevOtherEvent; + roundBottomPrimary = !hasPrevSeverityEvent; + roundTopPrimary = !hasNextSeverityEvent; + } + + if (hasOverlappingOtherEvent) { + roundBottomSecondary = !hasPrevOtherEvent; + roundTopSecondary = !hasNextOtherEvent; } return { - roundTop, - roundBottom, + roundTopPrimary, + roundBottomPrimary, + roundTopSecondary, + roundBottomSecondary, }; }, [events, getSegmentStart, getSegmentEnd, segmentDuration, severityType] @@ -171,6 +181,21 @@ export const useSegmentUtils = ( [events, getSegmentStart, getSegmentEnd, severityType] ); + const getEventThumbnail = useCallback( + (time: number): string => { + const matchingEvent = events.find((event) => { + return ( + time >= getSegmentStart(event.start_time) && + time < getSegmentEnd(event.end_time) && + event.severity == severityType + ); + }); + + return matchingEvent?.thumb_path ?? ""; + }, + [events, getSegmentStart, getSegmentEnd, severityType] + ); + return { getSegmentStart, getSegmentEnd, @@ -179,5 +204,6 @@ export const useSegmentUtils = ( getReviewed, shouldShowRoundedCorners, getEventStart, + getEventThumbnail }; -}; \ No newline at end of file +};