mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-09 04:35:25 +03:00
hooks
This commit is contained in:
parent
81397c79a5
commit
2a975cf51b
236
web/src/components/timeline/EventSegment.tsx
Normal file
236
web/src/components/timeline/EventSegment.tsx
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
import { useEventUtils } from "@/hooks/use-event-utils";
|
||||||
|
import { useSegmentUtils } from "@/hooks/use-segment-utils";
|
||||||
|
import { Event } from "@/types/event";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
type EventSegmentProps = {
|
||||||
|
events: Event[];
|
||||||
|
segmentTime: number;
|
||||||
|
segmentDuration: number;
|
||||||
|
timestampSpread: number;
|
||||||
|
showMinimap: boolean;
|
||||||
|
minimapStartTime?: number;
|
||||||
|
minimapEndTime?: number;
|
||||||
|
severityType: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function EventSegment({
|
||||||
|
events,
|
||||||
|
segmentTime,
|
||||||
|
segmentDuration,
|
||||||
|
timestampSpread,
|
||||||
|
showMinimap,
|
||||||
|
minimapStartTime,
|
||||||
|
minimapEndTime,
|
||||||
|
severityType,
|
||||||
|
}: EventSegmentProps) {
|
||||||
|
const { isStartOfEvent, isEndOfEvent } = useEventUtils(
|
||||||
|
events,
|
||||||
|
segmentDuration
|
||||||
|
);
|
||||||
|
const {
|
||||||
|
getSeverity,
|
||||||
|
getReviewed,
|
||||||
|
displaySeverityType,
|
||||||
|
shouldShowRoundedCorners,
|
||||||
|
} = useSegmentUtils(segmentDuration, events, severityType);
|
||||||
|
|
||||||
|
const { alignDateToTimeline } = useEventUtils(events, segmentDuration);
|
||||||
|
|
||||||
|
const severity = useMemo(
|
||||||
|
() => getSeverity(segmentTime),
|
||||||
|
[getSeverity, segmentTime]
|
||||||
|
);
|
||||||
|
const reviewed = useMemo(
|
||||||
|
() => getReviewed(segmentTime),
|
||||||
|
[getReviewed, segmentTime]
|
||||||
|
);
|
||||||
|
const showRoundedCorners = useMemo(
|
||||||
|
() => shouldShowRoundedCorners(segmentTime),
|
||||||
|
[shouldShowRoundedCorners, segmentTime]
|
||||||
|
);
|
||||||
|
|
||||||
|
const timestamp = useMemo(() => new Date(segmentTime), [segmentTime]);
|
||||||
|
const segmentKey = useMemo(
|
||||||
|
() => Math.floor(segmentTime / 1000),
|
||||||
|
[segmentTime]
|
||||||
|
);
|
||||||
|
|
||||||
|
const alignedMinimapStartTime = useMemo(
|
||||||
|
() => alignDateToTimeline(minimapStartTime ?? 0),
|
||||||
|
[minimapStartTime, alignDateToTimeline]
|
||||||
|
);
|
||||||
|
const alignedMinimapEndTime = useMemo(
|
||||||
|
() => alignDateToTimeline(minimapEndTime ?? 0),
|
||||||
|
[minimapEndTime, alignDateToTimeline]
|
||||||
|
);
|
||||||
|
|
||||||
|
const isInMinimapRange = useMemo(() => {
|
||||||
|
return (
|
||||||
|
showMinimap &&
|
||||||
|
minimapStartTime &&
|
||||||
|
minimapEndTime &&
|
||||||
|
segmentTime > minimapStartTime &&
|
||||||
|
segmentTime < minimapEndTime
|
||||||
|
);
|
||||||
|
}, [showMinimap, minimapStartTime, minimapEndTime, segmentTime]);
|
||||||
|
|
||||||
|
const isFirstSegmentInMinimap = useMemo(() => {
|
||||||
|
return showMinimap && segmentTime === alignedMinimapStartTime;
|
||||||
|
}, [showMinimap, segmentTime, alignedMinimapStartTime]);
|
||||||
|
|
||||||
|
const isLastSegmentInMinimap = useMemo(() => {
|
||||||
|
return showMinimap && segmentTime === alignedMinimapEndTime;
|
||||||
|
}, [showMinimap, segmentTime, alignedMinimapEndTime]);
|
||||||
|
|
||||||
|
const segmentClasses = `flex flex-row ${
|
||||||
|
showMinimap
|
||||||
|
? isInMinimapRange
|
||||||
|
? "bg-muted"
|
||||||
|
: isLastSegmentInMinimap
|
||||||
|
? ""
|
||||||
|
: "opacity-80"
|
||||||
|
: ""
|
||||||
|
} ${
|
||||||
|
isFirstSegmentInMinimap || isLastSegmentInMinimap
|
||||||
|
? "relative h-2 border-b border-gray-500"
|
||||||
|
: ""
|
||||||
|
}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={segmentKey} className={segmentClasses}>
|
||||||
|
{isFirstSegmentInMinimap && (
|
||||||
|
<div className="absolute inset-0 -bottom-5 w-full flex items-center justify-center text-xs text-primary font-medium z-20 text-center text-[9px]">
|
||||||
|
{new Date(alignedMinimapStartTime).toLocaleTimeString([], {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
month: "short",
|
||||||
|
day: "2-digit",
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isLastSegmentInMinimap && (
|
||||||
|
<div className="absolute inset-0 -top-1 w-full flex items-center justify-center text-xs text-primary font-medium z-20 text-center text-[9px]">
|
||||||
|
{new Date(alignedMinimapEndTime).toLocaleTimeString([], {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
month: "short",
|
||||||
|
day: "2-digit",
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="w-5 h-2 flex justify-left items-end">
|
||||||
|
{!isFirstSegmentInMinimap && !isLastSegmentInMinimap && (
|
||||||
|
<div
|
||||||
|
className={`h-0.5 ${
|
||||||
|
timestamp.getMinutes() % timestampSpread === 0 &&
|
||||||
|
timestamp.getSeconds() === 0
|
||||||
|
? "w-4 bg-gray-400"
|
||||||
|
: "w-2 bg-gray-600"
|
||||||
|
}`}
|
||||||
|
></div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="w-10 h-2 flex justify-left items-top z-10">
|
||||||
|
{!isFirstSegmentInMinimap && !isLastSegmentInMinimap && (
|
||||||
|
<div
|
||||||
|
key={`${segmentKey}_timestamp`}
|
||||||
|
className="text-[8px] text-gray-400"
|
||||||
|
>
|
||||||
|
{timestamp.getMinutes() % timestampSpread === 0 &&
|
||||||
|
timestamp.getSeconds() === 0 &&
|
||||||
|
timestamp.toLocaleTimeString([], {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{severity == displaySeverityType && (
|
||||||
|
<div className="mr-3 w-2 h-2 flex justify-left items-end">
|
||||||
|
<div
|
||||||
|
key={`${segmentKey}_primary_data`}
|
||||||
|
className={`
|
||||||
|
w-full h-2 bg-gradient-to-r
|
||||||
|
${
|
||||||
|
showRoundedCorners &&
|
||||||
|
isStartOfEvent(segmentTime, events, segmentDuration)
|
||||||
|
? "rounded-bl-full rounded-br-full"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
${
|
||||||
|
showRoundedCorners &&
|
||||||
|
isEndOfEvent(segmentTime, events, segmentDuration)
|
||||||
|
? "rounded-tl-full rounded-tr-full"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
${
|
||||||
|
reviewed
|
||||||
|
? severity === 1
|
||||||
|
? "from-yellow-200/30 to-yellow-400/30"
|
||||||
|
: severity === 2
|
||||||
|
? "from-orange-400/30 to-orange-600/30"
|
||||||
|
: severity === 3
|
||||||
|
? "from-red-500/30 to-red-800/30"
|
||||||
|
: ""
|
||||||
|
: severity === 1
|
||||||
|
? "from-yellow-200 to-yellow-400"
|
||||||
|
: severity === 2
|
||||||
|
? "from-orange-400 to-orange-600"
|
||||||
|
: severity === 3
|
||||||
|
? "from-red-500 to-red-800"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
`}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{severity != displaySeverityType && (
|
||||||
|
<div className="h-2 flex flex-grow justify-end items-end">
|
||||||
|
<div
|
||||||
|
key={`${segmentKey}_secondary_data`}
|
||||||
|
className={`
|
||||||
|
w-1 h-2 bg-gradient-to-r
|
||||||
|
${
|
||||||
|
showRoundedCorners &&
|
||||||
|
isStartOfEvent(segmentTime, events, segmentDuration)
|
||||||
|
? "rounded-bl-full rounded-br-full"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
${
|
||||||
|
showRoundedCorners &&
|
||||||
|
isEndOfEvent(segmentTime, events, segmentDuration)
|
||||||
|
? "rounded-tl-full rounded-tr-full"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
${
|
||||||
|
reviewed
|
||||||
|
? severity === 1
|
||||||
|
? "from-yellow-200/30 to-yellow-400/30"
|
||||||
|
: severity === 2
|
||||||
|
? "from-orange-400/30 to-orange-600/30"
|
||||||
|
: severity === 3
|
||||||
|
? "from-red-500/30 to-red-800/30"
|
||||||
|
: ""
|
||||||
|
: severity === 1
|
||||||
|
? "from-yellow-200 to-yellow-400"
|
||||||
|
: severity === 2
|
||||||
|
? "from-orange-400 to-orange-600"
|
||||||
|
: severity === 3
|
||||||
|
? "from-red-500 to-red-800"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
`}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EventSegment;
|
||||||
243
web/src/components/timeline/ReviewTimeline.tsx
Normal file
243
web/src/components/timeline/ReviewTimeline.tsx
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
import useDraggableHandler from "@/hooks/use-handle-dragging";
|
||||||
|
import {
|
||||||
|
useEffect,
|
||||||
|
useCallback,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
RefObject,
|
||||||
|
} from "react";
|
||||||
|
import EventSegment from "./EventSegment";
|
||||||
|
import { useEventUtils } from "@/hooks/use-event-utils";
|
||||||
|
import { Event } from "@/types/event";
|
||||||
|
|
||||||
|
export type ReviewTimelineProps = {
|
||||||
|
segmentDuration: number;
|
||||||
|
timestampSpread: number;
|
||||||
|
timelineStart: number;
|
||||||
|
timelineDuration?: number;
|
||||||
|
showHandlebar?: boolean;
|
||||||
|
handlebarTime?: number;
|
||||||
|
showMinimap?: boolean;
|
||||||
|
minimapStartTime?: number;
|
||||||
|
minimapEndTime?: number;
|
||||||
|
events: Event[];
|
||||||
|
severityType: string;
|
||||||
|
contentRef: RefObject<HTMLDivElement>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ReviewTimeline({
|
||||||
|
segmentDuration,
|
||||||
|
timestampSpread,
|
||||||
|
timelineStart,
|
||||||
|
timelineDuration = 24 * 60 * 60,
|
||||||
|
showHandlebar = false,
|
||||||
|
handlebarTime,
|
||||||
|
showMinimap = false,
|
||||||
|
minimapStartTime,
|
||||||
|
minimapEndTime,
|
||||||
|
events,
|
||||||
|
severityType,
|
||||||
|
contentRef,
|
||||||
|
}: ReviewTimelineProps) {
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const [currentTimeSegment, setCurrentTimeSegment] = useState<number>(0);
|
||||||
|
const scrollTimeRef = useRef<HTMLDivElement>(null);
|
||||||
|
const timelineRef = useRef<HTMLDivElement>(null);
|
||||||
|
const currentTimeRef = useRef<HTMLDivElement>(null);
|
||||||
|
const observer = useRef<ResizeObserver | null>(null);
|
||||||
|
|
||||||
|
const { alignDateToTimeline } = useEventUtils(events, segmentDuration);
|
||||||
|
|
||||||
|
const { handleMouseDown, handleMouseUp, handleMouseMove } =
|
||||||
|
useDraggableHandler({
|
||||||
|
contentRef,
|
||||||
|
timelineRef,
|
||||||
|
scrollTimeRef,
|
||||||
|
alignDateToTimeline,
|
||||||
|
segmentDuration,
|
||||||
|
showHandlebar,
|
||||||
|
timelineDuration,
|
||||||
|
timelineStart,
|
||||||
|
isDragging,
|
||||||
|
setIsDragging,
|
||||||
|
currentTimeRef,
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleResize() {
|
||||||
|
// TODO: handle screen resize for mobile
|
||||||
|
if (timelineRef.current && contentRef.current) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (contentRef.current) {
|
||||||
|
const content = contentRef.current;
|
||||||
|
observer.current = new ResizeObserver(() => {
|
||||||
|
handleResize();
|
||||||
|
});
|
||||||
|
observer.current.observe(content);
|
||||||
|
return () => {
|
||||||
|
observer.current?.unobserve(content);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Generate segments for the timeline
|
||||||
|
const generateSegments = useCallback(() => {
|
||||||
|
const segmentCount = timelineDuration / segmentDuration;
|
||||||
|
const segmentAlignedTime = alignDateToTimeline(timelineStart);
|
||||||
|
|
||||||
|
return Array.from({ length: segmentCount }, (_, index) => {
|
||||||
|
const segmentTime = segmentAlignedTime - index * segmentDuration * 1000;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EventSegment
|
||||||
|
events={events}
|
||||||
|
segmentDuration={segmentDuration}
|
||||||
|
segmentTime={segmentTime}
|
||||||
|
timestampSpread={timestampSpread}
|
||||||
|
showMinimap={showMinimap}
|
||||||
|
minimapStartTime={minimapStartTime}
|
||||||
|
minimapEndTime={minimapEndTime}
|
||||||
|
severityType={severityType}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
segmentDuration,
|
||||||
|
timestampSpread,
|
||||||
|
timelineStart,
|
||||||
|
timelineDuration,
|
||||||
|
showMinimap,
|
||||||
|
minimapStartTime,
|
||||||
|
minimapEndTime,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const segments = useMemo(
|
||||||
|
() => generateSegments(),
|
||||||
|
[
|
||||||
|
segmentDuration,
|
||||||
|
timestampSpread,
|
||||||
|
timelineStart,
|
||||||
|
timelineDuration,
|
||||||
|
showMinimap,
|
||||||
|
minimapStartTime,
|
||||||
|
minimapEndTime,
|
||||||
|
events,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (showHandlebar) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (currentTimeRef.current && currentTimeSegment) {
|
||||||
|
currentTimeRef.current.textContent = new Date(
|
||||||
|
currentTimeSegment
|
||||||
|
).toLocaleTimeString([], {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
...(segmentDuration < 60 && { second: "2-digit" }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [currentTimeSegment, showHandlebar]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (timelineRef.current && handlebarTime && showHandlebar) {
|
||||||
|
const { scrollHeight: timelineHeight } = timelineRef.current;
|
||||||
|
|
||||||
|
// Calculate the height of an individual segment
|
||||||
|
const segmentHeight =
|
||||||
|
timelineHeight / (timelineDuration / segmentDuration);
|
||||||
|
|
||||||
|
// Calculate the segment index corresponding to the target time
|
||||||
|
const alignedHandlebarTime = alignDateToTimeline(handlebarTime);
|
||||||
|
const segmentIndex = Math.ceil(
|
||||||
|
(timelineStart - alignedHandlebarTime) / (segmentDuration * 1000)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate the top position based on the segment index
|
||||||
|
const newTopPosition = Math.max(0, segmentIndex * segmentHeight);
|
||||||
|
|
||||||
|
// Set the top position of the handle
|
||||||
|
const thumb = scrollTimeRef.current;
|
||||||
|
if (thumb) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
thumb.style.top = `${newTopPosition}px`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentTimeSegment(alignedHandlebarTime);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
handlebarTime,
|
||||||
|
segmentDuration,
|
||||||
|
showHandlebar,
|
||||||
|
timelineDuration,
|
||||||
|
timelineStart,
|
||||||
|
alignDateToTimeline,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
generateSegments();
|
||||||
|
if (!currentTimeSegment && !handlebarTime) {
|
||||||
|
setCurrentTimeSegment(timelineStart);
|
||||||
|
}
|
||||||
|
// TODO: touch events for mobile
|
||||||
|
document.addEventListener("mousemove", handleMouseMove);
|
||||||
|
document.addEventListener("mouseup", handleMouseUp);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousemove", handleMouseMove);
|
||||||
|
document.removeEventListener("mouseup", handleMouseUp);
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
currentTimeSegment,
|
||||||
|
generateSegments,
|
||||||
|
timelineStart,
|
||||||
|
handleMouseUp,
|
||||||
|
handleMouseMove,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={timelineRef}
|
||||||
|
className={`relative w-[100px] h-screen overflow-y-scroll no-scrollbar ${
|
||||||
|
isDragging && showHandlebar ? "cursor-grabbing" : "cursor-auto"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col">{segments}</div>
|
||||||
|
{showHandlebar && (
|
||||||
|
<div className={`absolute left-0 top-0 z-20 w-full `} role="scrollbar">
|
||||||
|
<div className={`flex items-center justify-center `}>
|
||||||
|
<div
|
||||||
|
ref={scrollTimeRef}
|
||||||
|
className={`relative w-full ${
|
||||||
|
isDragging ? "cursor-grabbing" : "cursor-grab"
|
||||||
|
}`}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`bg-red-500 rounded-full mx-auto ${
|
||||||
|
segmentDuration < 60 ? "w-20" : "w-16"
|
||||||
|
} h-5 flex items-center justify-center`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={currentTimeRef}
|
||||||
|
className="text-white text-xs z-10"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div className="absolute h-1 w-full bg-red-500 top-1/2 transform -translate-y-1/2"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReviewTimeline;
|
||||||
|
|
||||||
|
// TODO: more minor tick marks for segmentDuration < 60
|
||||||
|
// theme colors
|
||||||
37
web/src/hooks/use-event-utils.ts
Normal file
37
web/src/hooks/use-event-utils.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import { Event } from '@/types/event';
|
||||||
|
|
||||||
|
export const useEventUtils = (events: Event[], segmentDuration: number) => {
|
||||||
|
const isStartOfEvent = useCallback((time: number): boolean => {
|
||||||
|
return events.some((event) => {
|
||||||
|
const segmentStart = getSegmentStart(event.start_time);
|
||||||
|
return time >= segmentStart && time < segmentStart + segmentDuration * 1000;
|
||||||
|
});
|
||||||
|
}, [events, segmentDuration]);
|
||||||
|
|
||||||
|
const isEndOfEvent = useCallback((time: number): boolean => {
|
||||||
|
return events.some((event) => {
|
||||||
|
if (typeof event.end_time === 'number') {
|
||||||
|
const segmentEnd = getSegmentEnd(event.end_time);
|
||||||
|
return time >= segmentEnd - segmentDuration * 1000 && time < segmentEnd;
|
||||||
|
}
|
||||||
|
return false; // Return false if end_time is undefined
|
||||||
|
});
|
||||||
|
}, [events, segmentDuration]);
|
||||||
|
|
||||||
|
const getSegmentStart = useCallback((time: number): number => {
|
||||||
|
return Math.floor(time / (segmentDuration * 1000)) * (segmentDuration * 1000);
|
||||||
|
}, [segmentDuration]);
|
||||||
|
|
||||||
|
const getSegmentEnd = useCallback((time: number): number => {
|
||||||
|
return Math.ceil(time / (segmentDuration * 1000)) * (segmentDuration * 1000);
|
||||||
|
}, [segmentDuration]);
|
||||||
|
|
||||||
|
const alignDateToTimeline = useCallback((time: number): number => {
|
||||||
|
const remainder = time % (segmentDuration * 1000);
|
||||||
|
const adjustment = remainder !== 0 ? segmentDuration * 1000 - remainder : 0;
|
||||||
|
return time + adjustment;
|
||||||
|
}, [segmentDuration]);
|
||||||
|
|
||||||
|
return { isStartOfEvent, isEndOfEvent, getSegmentStart, getSegmentEnd, alignDateToTimeline };
|
||||||
|
};
|
||||||
127
web/src/hooks/use-handle-dragging.ts
Normal file
127
web/src/hooks/use-handle-dragging.ts
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
|
||||||
|
interface DragHandlerProps {
|
||||||
|
contentRef: React.RefObject<HTMLElement>;
|
||||||
|
timelineRef: React.RefObject<HTMLDivElement>;
|
||||||
|
scrollTimeRef: React.RefObject<HTMLDivElement>;
|
||||||
|
alignDateToTimeline: (time: number) => number;
|
||||||
|
segmentDuration: number;
|
||||||
|
showHandlebar: boolean;
|
||||||
|
timelineDuration: number;
|
||||||
|
timelineStart: number;
|
||||||
|
isDragging: boolean;
|
||||||
|
setIsDragging: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
currentTimeRef: React.MutableRefObject<HTMLDivElement | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: handle mobile touch events
|
||||||
|
function useDraggableHandler({
|
||||||
|
contentRef,
|
||||||
|
timelineRef,
|
||||||
|
scrollTimeRef,
|
||||||
|
alignDateToTimeline,
|
||||||
|
segmentDuration,
|
||||||
|
showHandlebar,
|
||||||
|
timelineDuration,
|
||||||
|
timelineStart,
|
||||||
|
isDragging,
|
||||||
|
setIsDragging,
|
||||||
|
currentTimeRef,
|
||||||
|
}: DragHandlerProps) {
|
||||||
|
const handleMouseDown = useCallback(
|
||||||
|
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsDragging(true);
|
||||||
|
},
|
||||||
|
[setIsDragging]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleMouseUp = useCallback(
|
||||||
|
(e: MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (isDragging) {
|
||||||
|
setIsDragging(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[isDragging, setIsDragging]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleMouseMove = useCallback(
|
||||||
|
(e: MouseEvent) => {
|
||||||
|
if (!contentRef.current || !timelineRef.current || !scrollTimeRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (isDragging) {
|
||||||
|
const {
|
||||||
|
scrollHeight: timelineHeight,
|
||||||
|
clientHeight: visibleTimelineHeight,
|
||||||
|
scrollTop: scrolled,
|
||||||
|
offsetTop: timelineTop,
|
||||||
|
} = timelineRef.current;
|
||||||
|
|
||||||
|
const segmentHeight =
|
||||||
|
timelineHeight / (timelineDuration / segmentDuration);
|
||||||
|
|
||||||
|
const getCumulativeScrollTop = (
|
||||||
|
element: HTMLElement | null
|
||||||
|
) => {
|
||||||
|
let scrollTop = 0;
|
||||||
|
while (element) {
|
||||||
|
scrollTop += element.scrollTop;
|
||||||
|
element = element.parentElement;
|
||||||
|
}
|
||||||
|
return scrollTop;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parentScrollTop = getCumulativeScrollTop(timelineRef.current);
|
||||||
|
|
||||||
|
const newHandlePosition = Math.min(
|
||||||
|
visibleTimelineHeight - timelineTop + parentScrollTop,
|
||||||
|
Math.max(
|
||||||
|
segmentHeight + scrolled,
|
||||||
|
e.clientY - timelineTop + parentScrollTop
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const segmentIndex = Math.floor(newHandlePosition / segmentHeight);
|
||||||
|
const segmentStartTime = alignDateToTimeline(
|
||||||
|
Math.floor(timelineStart - segmentIndex * segmentDuration * 1000)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (showHandlebar) {
|
||||||
|
const thumb = scrollTimeRef.current;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
thumb.style.top = `${newHandlePosition - segmentHeight}px`;
|
||||||
|
if (currentTimeRef.current) {
|
||||||
|
currentTimeRef.current.textContent = new Date(
|
||||||
|
segmentStartTime
|
||||||
|
).toLocaleTimeString([], {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
...(segmentDuration < 60 && { second: "2-digit" }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
isDragging,
|
||||||
|
contentRef,
|
||||||
|
segmentDuration,
|
||||||
|
showHandlebar,
|
||||||
|
timelineDuration,
|
||||||
|
timelineStart,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return { handleMouseDown, handleMouseUp, handleMouseMove };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useDraggableHandler;
|
||||||
89
web/src/hooks/use-segment-utils.ts
Normal file
89
web/src/hooks/use-segment-utils.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import { Event } from '@/types/event';
|
||||||
|
|
||||||
|
export const useSegmentUtils = (
|
||||||
|
segmentDuration: number,
|
||||||
|
events: Event[],
|
||||||
|
severityType: string,
|
||||||
|
) => {
|
||||||
|
const getSegmentStart = useCallback((time: number): number => {
|
||||||
|
return Math.floor(time / (segmentDuration * 1000)) * (segmentDuration * 1000);
|
||||||
|
}, [segmentDuration]);
|
||||||
|
|
||||||
|
const getSegmentEnd = useCallback((time: number | undefined): number => {
|
||||||
|
if (time) {
|
||||||
|
return Math.ceil(time / (segmentDuration * 1000)) * (segmentDuration * 1000);
|
||||||
|
} else {
|
||||||
|
return Date.now()+(segmentDuration*1000);
|
||||||
|
}
|
||||||
|
}, [segmentDuration]);
|
||||||
|
|
||||||
|
const mapSeverityToNumber = useCallback((severity: string): number => {
|
||||||
|
switch (severity) {
|
||||||
|
case "motion":
|
||||||
|
return 1;
|
||||||
|
case "detection":
|
||||||
|
return 2;
|
||||||
|
case "alert":
|
||||||
|
return 3;
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const displaySeverityType = useMemo(
|
||||||
|
() => mapSeverityToNumber(severityType ?? ""),
|
||||||
|
[severityType]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getSeverity = useCallback((time: number): number => {
|
||||||
|
const activeEvents = events?.filter((event) => {
|
||||||
|
const segmentStart = getSegmentStart(event.start_time);
|
||||||
|
const segmentEnd = getSegmentEnd(event.end_time);
|
||||||
|
return time >= segmentStart && time < segmentEnd;
|
||||||
|
});
|
||||||
|
if (activeEvents?.length === 0) return 0; // No event at this time
|
||||||
|
const severityValues = activeEvents?.map((event) =>
|
||||||
|
mapSeverityToNumber(event.severity)
|
||||||
|
);
|
||||||
|
return Math.max(...severityValues);
|
||||||
|
}, [events, getSegmentStart, getSegmentEnd, mapSeverityToNumber]);
|
||||||
|
|
||||||
|
const getReviewed = useCallback((time: number): boolean => {
|
||||||
|
return events.some((event) => {
|
||||||
|
const segmentStart = getSegmentStart(event.start_time);
|
||||||
|
const segmentEnd = getSegmentEnd(event.end_time);
|
||||||
|
return (
|
||||||
|
time >= segmentStart && time < segmentEnd && event.has_been_reviewed
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, [events, getSegmentStart, getSegmentEnd]);
|
||||||
|
|
||||||
|
const shouldShowRoundedCorners = useCallback(
|
||||||
|
(segmentTime: number): boolean => {
|
||||||
|
const prevSegmentTime = segmentTime - segmentDuration * 1000;
|
||||||
|
const nextSegmentTime = segmentTime + segmentDuration * 1000;
|
||||||
|
|
||||||
|
const hasPrevEvent = events.some((e) => {
|
||||||
|
return (
|
||||||
|
prevSegmentTime >= getSegmentStart(e.start_time) &&
|
||||||
|
prevSegmentTime < getSegmentEnd(e.end_time) &&
|
||||||
|
e.severity === severityType
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasNextEvent = events.some((e) => {
|
||||||
|
return (
|
||||||
|
nextSegmentTime >= getSegmentStart(e.start_time) &&
|
||||||
|
nextSegmentTime < getSegmentEnd(e.end_time) &&
|
||||||
|
e.severity === severityType
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return !hasPrevEvent || !hasNextEvent;
|
||||||
|
},
|
||||||
|
[events, getSegmentStart, getSegmentEnd, segmentDuration, severityType]
|
||||||
|
);
|
||||||
|
|
||||||
|
return { getSegmentStart, getSegmentEnd, getSeverity, displaySeverityType, getReviewed, shouldShowRoundedCorners };
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user