option to show motion only on motion timeline (#10626)

This commit is contained in:
Josh Hawkins 2024-03-23 08:33:50 -05:00 committed by GitHub
parent 8e1d18d06b
commit 4159334520
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 287 additions and 127 deletions

View File

@ -10,10 +10,10 @@ import {
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "../ui/dropdown-menu"; } from "../ui/dropdown-menu";
import { ReviewFilter, ReviewSummary } from "@/types/review"; import { ReviewFilter, ReviewSeverity, ReviewSummary } from "@/types/review";
import { getEndOfDayTimestamp } from "@/utils/dateUtil"; import { getEndOfDayTimestamp } from "@/utils/dateUtil";
import { useFormattedTimestamp } from "@/hooks/use-date-utils"; import { useFormattedTimestamp } from "@/hooks/use-date-utils";
import { FaCalendarAlt, FaFilter, FaVideo } from "react-icons/fa"; import { FaCalendarAlt, FaFilter, FaRunning, FaVideo } from "react-icons/fa";
import { isMobile } from "react-device-detect"; import { isMobile } from "react-device-detect";
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer"; import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
import { Switch } from "../ui/switch"; import { Switch } from "../ui/switch";
@ -27,12 +27,18 @@ type ReviewFilterGroupProps = {
reviewSummary?: ReviewSummary; reviewSummary?: ReviewSummary;
filter?: ReviewFilter; filter?: ReviewFilter;
onUpdateFilter: (filter: ReviewFilter) => void; onUpdateFilter: (filter: ReviewFilter) => void;
severity: ReviewSeverity;
motionOnly: boolean;
setMotionOnly: React.Dispatch<React.SetStateAction<boolean>>;
}; };
export default function ReviewFilterGroup({ export default function ReviewFilterGroup({
reviewSummary, reviewSummary,
filter, filter,
onUpdateFilter, onUpdateFilter,
severity,
motionOnly,
setMotionOnly,
}: ReviewFilterGroupProps) { }: ReviewFilterGroupProps) {
const { data: config } = useSWR<FrigateConfig>("config"); const { data: config } = useSWR<FrigateConfig>("config");
@ -94,7 +100,7 @@ export default function ReviewFilterGroup({
); );
return ( return (
<div> <div className="flex justify-center">
<CamerasFilterButton <CamerasFilterButton
allCameras={filterValues.cameras} allCameras={filterValues.cameras}
groups={groups} groups={groups}
@ -110,17 +116,24 @@ export default function ReviewFilterGroup({
} }
updateSelectedDay={onUpdateSelectedDay} updateSelectedDay={onUpdateSelectedDay}
/> />
<GeneralFilterButton {severity == "significant_motion" ? (
allLabels={filterValues.labels} <ShowMotionOnlyButton
selectedLabels={filter?.labels} motionOnly={motionOnly}
updateLabelFilter={(newLabels) => { setMotionOnly={setMotionOnly}
onUpdateFilter({ ...filter, labels: newLabels }); />
}} ) : (
showReviewed={filter?.showReviewed || 0} <GeneralFilterButton
setShowReviewed={(reviewed) => allLabels={filterValues.labels}
onUpdateFilter({ ...filter, showReviewed: reviewed }) selectedLabels={filter?.labels}
} updateLabelFilter={(newLabels) => {
/> onUpdateFilter({ ...filter, labels: newLabels });
}}
showReviewed={filter?.showReviewed || 0}
setShowReviewed={(reviewed) =>
onUpdateFilter({ ...filter, showReviewed: reviewed })
}
/>
)}
</div> </div>
); );
} }
@ -485,3 +498,46 @@ function GeneralFilterButton({
</Popover> </Popover>
); );
} }
type ShowMotionOnlyButtonProps = {
motionOnly: boolean;
setMotionOnly: React.Dispatch<React.SetStateAction<boolean>>;
};
function ShowMotionOnlyButton({
motionOnly,
setMotionOnly,
}: ShowMotionOnlyButtonProps) {
return (
<>
<div className="hidden md:inline-flex items-center justify-center whitespace-nowrap text-sm bg-secondary text-secondary-foreground h-9 rounded-md md:px-3 md:mx-1">
<Switch
className="ml-1"
id="collapse-motion"
checked={motionOnly}
onCheckedChange={() => {
setMotionOnly(!motionOnly);
}}
/>
<Label
className="mx-2 text-secondary-foreground"
htmlFor="collapse-motion"
>
Motion only
</Label>
</div>
<div className="block md:hidden">
<Button
size="sm"
className="ml-1"
variant="secondary"
onClick={() => setMotionOnly(!motionOnly)}
>
<FaRunning
className={`${motionOnly ? "text-selected" : "text-muted-foreground"}`}
/>
</Button>
</div>
</>
);
}

View File

@ -236,10 +236,12 @@ export function EventReviewTimeline({
const element = selectedTimelineRef.current?.querySelector( const element = selectedTimelineRef.current?.querySelector(
`[data-segment-id="${Math.max(...alignedVisibleTimestamps)}"]`, `[data-segment-id="${Math.max(...alignedVisibleTimestamps)}"]`,
); );
scrollIntoView(element as HTMLDivElement, { if (element) {
scrollMode: "if-needed", scrollIntoView(element, {
behavior: "smooth", scrollMode: "if-needed",
}); behavior: "smooth",
});
}
} }
}, [ }, [
selectedTimelineRef, selectedTimelineRef,

View File

@ -201,7 +201,7 @@ export function EventSegment({
<div <div
key={segmentKey} key={segmentKey}
data-segment-id={segmentKey} data-segment-id={segmentKey}
className={segmentClasses} className={`segment ${segmentClasses}`}
onClick={segmentClick} onClick={segmentClick}
onTouchEnd={(event) => handleTouchStart(event, segmentClick)} onTouchEnd={(event) => handleTouchStart(event, segmentClick)}
> >

View File

@ -21,6 +21,7 @@ export type MotionReviewTimelineProps = {
showHandlebar?: boolean; showHandlebar?: boolean;
handlebarTime?: number; handlebarTime?: number;
setHandlebarTime?: React.Dispatch<React.SetStateAction<number>>; setHandlebarTime?: React.Dispatch<React.SetStateAction<number>>;
motionOnly?: boolean;
showMinimap?: boolean; showMinimap?: boolean;
minimapStartTime?: number; minimapStartTime?: number;
minimapEndTime?: number; minimapEndTime?: number;
@ -45,6 +46,7 @@ export function MotionReviewTimeline({
showHandlebar = false, showHandlebar = false,
handlebarTime, handlebarTime,
setHandlebarTime, setHandlebarTime,
motionOnly = false,
showMinimap = false, showMinimap = false,
minimapStartTime, minimapStartTime,
minimapEndTime, minimapEndTime,
@ -113,6 +115,7 @@ export function MotionReviewTimeline({
draggableElementTime: handlebarTime, draggableElementTime: handlebarTime,
setDraggableElementTime: setHandlebarTime, setDraggableElementTime: setHandlebarTime,
timelineDuration, timelineDuration,
timelineCollapsed: motionOnly,
timelineStartAligned, timelineStartAligned,
isDragging, isDragging,
setIsDragging, setIsDragging,
@ -176,6 +179,7 @@ export function MotionReviewTimeline({
segmentDuration={segmentDuration} segmentDuration={segmentDuration}
segmentTime={segmentTime} segmentTime={segmentTime}
timestampSpread={timestampSpread} timestampSpread={timestampSpread}
motionOnly={motionOnly}
showMinimap={showMinimap} showMinimap={showMinimap}
minimapStartTime={minimapStartTime} minimapStartTime={minimapStartTime}
minimapEndTime={minimapEndTime} minimapEndTime={minimapEndTime}
@ -195,6 +199,7 @@ export function MotionReviewTimeline({
minimapEndTime, minimapEndTime,
events, events,
motion_events, motion_events,
motionOnly,
]); ]);
const segments = useMemo( const segments = useMemo(
@ -211,6 +216,7 @@ export function MotionReviewTimeline({
minimapEndTime, minimapEndTime,
events, events,
motion_events, motion_events,
motionOnly,
], ],
); );

View File

@ -14,6 +14,7 @@ type MotionSegmentProps = {
segmentTime: number; segmentTime: number;
segmentDuration: number; segmentDuration: number;
timestampSpread: number; timestampSpread: number;
motionOnly: boolean;
showMinimap: boolean; showMinimap: boolean;
minimapStartTime?: number; minimapStartTime?: number;
minimapEndTime?: number; minimapEndTime?: number;
@ -26,6 +27,7 @@ export function MotionSegment({
segmentTime, segmentTime,
segmentDuration, segmentDuration,
timestampSpread, timestampSpread,
motionOnly,
showMinimap, showMinimap,
minimapStartTime, minimapStartTime,
minimapEndTime, minimapEndTime,
@ -180,79 +182,96 @@ export function MotionSegment({
}, [segmentTime, setHandlebarTime]); }, [segmentTime, setHandlebarTime]);
return ( return (
<div <>
key={segmentKey} {(((firstHalfSegmentWidth > 1 || secondHalfSegmentWidth > 1) &&
data-segment-id={segmentKey} motionOnly &&
className={`segment ${firstHalfSegmentWidth > 1 || secondHalfSegmentWidth > 1 ? "has-data" : ""} ${segmentClasses}`} severity[0] < 2) ||
onClick={segmentClick} !motionOnly) && (
onTouchEnd={(event) => handleTouchStart(event, segmentClick)} <div
> key={segmentKey}
<MinimapBounds data-segment-id={segmentKey}
isFirstSegmentInMinimap={isFirstSegmentInMinimap} className={`segment ${firstHalfSegmentWidth > 1 || secondHalfSegmentWidth > 1 ? "has-data" : ""} ${segmentClasses}`}
isLastSegmentInMinimap={isLastSegmentInMinimap} onClick={segmentClick}
alignedMinimapStartTime={alignedMinimapStartTime} onTouchEnd={(event) => handleTouchStart(event, segmentClick)}
alignedMinimapEndTime={alignedMinimapEndTime} >
firstMinimapSegmentRef={firstMinimapSegmentRef} {!motionOnly && (
/> <>
<MinimapBounds
isFirstSegmentInMinimap={isFirstSegmentInMinimap}
isLastSegmentInMinimap={isLastSegmentInMinimap}
alignedMinimapStartTime={alignedMinimapStartTime}
alignedMinimapEndTime={alignedMinimapEndTime}
firstMinimapSegmentRef={firstMinimapSegmentRef}
/>
<Tick timestamp={timestamp} timestampSpread={timestampSpread} /> <Tick
key={`${segmentKey}_tick`}
timestamp={timestamp}
timestampSpread={timestampSpread}
/>
<Timestamp <Timestamp
isFirstSegmentInMinimap={isFirstSegmentInMinimap} key={`${segmentKey}_timestamp`}
isLastSegmentInMinimap={isLastSegmentInMinimap} isFirstSegmentInMinimap={isFirstSegmentInMinimap}
timestamp={timestamp} isLastSegmentInMinimap={isLastSegmentInMinimap}
timestampSpread={timestampSpread} timestamp={timestamp}
segmentKey={segmentKey} timestampSpread={timestampSpread}
/> segmentKey={segmentKey}
/>
</>
)}
<div className="absolute left-1/2 transform -translate-x-1/2 w-[20px] md:w-[40px] h-2 z-10 cursor-pointer"> <div className="absolute left-1/2 transform -translate-x-1/2 w-[20px] md:w-[40px] h-2 z-10 cursor-pointer">
<div className="flex flex-row justify-center w-[20px] md:w-[40px] mb-[1px]"> <div className="flex flex-row justify-center w-[20px] md:w-[40px] mb-[1px]">
<div className="flex justify-center"> <div className="flex justify-center">
<div
key={`${segmentKey}_motion_data_1`}
className={`${isDesktop && animationClassesSecondHalf} h-[2px] rounded-full ${severity[0] != 0 ? "bg-motion_review-dimmed" : "bg-motion_review"}`}
style={{
width: secondHalfSegmentWidth,
}}
></div>
</div>
</div>
<div className="flex flex-row justify-center w-[20px] md:w-[40px]">
<div className="flex justify-center">
<div
key={`${segmentKey}_motion_data_2`}
className={`${isDesktop && animationClassesFirstHalf} h-[2px] rounded-full ${severity[0] != 0 ? "bg-motion_review-dimmed" : "bg-motion_review"}`}
style={{
width: firstHalfSegmentWidth,
}}
></div>
</div>
</div>
</div>
{severity.map((severityValue: number, index: number) => {
if (severityValue > 1) {
return (
<React.Fragment key={index}>
<div className="absolute right-0 h-2 z-10">
<div <div
key={`${segmentKey}_${index}_secondary_data`} key={`${segmentKey}_motion_data_1`}
className={` className={`${isDesktop && animationClassesSecondHalf} h-[2px] rounded-full ${severity[0] != 0 ? "bg-motion_review-dimmed" : "bg-motion_review"}`}
w-1 h-2 bg-gradient-to-r style={{
${roundBottomSecondary ? "rounded-bl-full rounded-br-full" : ""} width: secondHalfSegmentWidth,
${roundTopSecondary ? "rounded-tl-full rounded-tr-full" : ""} }}
${severityColors[severityValue]}
`}
></div> ></div>
</div> </div>
</React.Fragment> </div>
);
} else { <div className="flex flex-row justify-center w-[20px] md:w-[40px]">
return null; <div className="flex justify-center">
} <div
})} key={`${segmentKey}_motion_data_2`}
</div> className={`${isDesktop && animationClassesFirstHalf} h-[2px] rounded-full ${severity[0] != 0 ? "bg-motion_review-dimmed" : "bg-motion_review"}`}
style={{
width: firstHalfSegmentWidth,
}}
></div>
</div>
</div>
</div>
{!motionOnly &&
severity.map((severityValue: number, index: number) => {
if (severityValue > 1) {
return (
<React.Fragment key={index}>
<div className="absolute right-0 h-2 z-10">
<div
key={`${segmentKey}_${index}_secondary_data`}
className={`
w-1 h-2 bg-gradient-to-r
${roundBottomSecondary ? "rounded-bl-full rounded-br-full" : ""}
${roundTopSecondary ? "rounded-tl-full rounded-tr-full" : ""}
${severityColors[severityValue]}
`}
></div>
</div>
</React.Fragment>
);
} else {
return null;
}
})}
</div>
)}
</>
); );
} }

View File

@ -1,7 +1,7 @@
import * as React from "react" import * as React from "react";
import * as SwitchPrimitives from "@radix-ui/react-switch" import * as SwitchPrimitives from "@radix-ui/react-switch";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const Switch = React.forwardRef< const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>, React.ElementRef<typeof SwitchPrimitives.Root>,
@ -10,18 +10,18 @@ const Switch = React.forwardRef<
<SwitchPrimitives.Root <SwitchPrimitives.Root
className={cn( className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-selected data-[state=unchecked]:bg-input", "peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-selected data-[state=unchecked]:bg-input",
className className,
)} )}
{...props} {...props}
ref={ref} ref={ref}
> >
<SwitchPrimitives.Thumb <SwitchPrimitives.Thumb
className={cn( className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0" "pointer-events-none block h-5 w-5 rounded-full bg-muted-foreground shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0",
)} )}
/> />
</SwitchPrimitives.Root> </SwitchPrimitives.Root>
)) ));
Switch.displayName = SwitchPrimitives.Root.displayName Switch.displayName = SwitchPrimitives.Root.displayName;
export { Switch } export { Switch };

View File

@ -15,6 +15,7 @@ type DraggableElementProps = {
setDraggableElementTime?: React.Dispatch<React.SetStateAction<number>>; setDraggableElementTime?: React.Dispatch<React.SetStateAction<number>>;
draggableElementTimeRef: React.MutableRefObject<HTMLDivElement | null>; draggableElementTimeRef: React.MutableRefObject<HTMLDivElement | null>;
timelineDuration: number; timelineDuration: number;
timelineCollapsed?: boolean;
timelineStartAligned: number; timelineStartAligned: number;
isDragging: boolean; isDragging: boolean;
setIsDragging: React.Dispatch<React.SetStateAction<boolean>>; setIsDragging: React.Dispatch<React.SetStateAction<boolean>>;
@ -33,6 +34,7 @@ function useDraggableElement({
setDraggableElementTime, setDraggableElementTime,
draggableElementTimeRef, draggableElementTimeRef,
timelineDuration, timelineDuration,
timelineCollapsed,
timelineStartAligned, timelineStartAligned,
isDragging, isDragging,
setIsDragging, setIsDragging,
@ -40,6 +42,7 @@ function useDraggableElement({
}: DraggableElementProps) { }: DraggableElementProps) {
const [clientYPosition, setClientYPosition] = useState<number | null>(null); const [clientYPosition, setClientYPosition] = useState<number | null>(null);
const [initialClickAdjustment, setInitialClickAdjustment] = useState(0); const [initialClickAdjustment, setInitialClickAdjustment] = useState(0);
const [segments, setSegments] = useState<HTMLDivElement[]>([]);
const { alignStartDateToTimeline, getCumulativeScrollTop } = useTimelineUtils( const { alignStartDateToTimeline, getCumulativeScrollTop } = useTimelineUtils(
{ {
segmentDuration: segmentDuration, segmentDuration: segmentDuration,
@ -101,7 +104,7 @@ function useDraggableElement({
} else if (e.nativeEvent instanceof MouseEvent) { } else if (e.nativeEvent instanceof MouseEvent) {
clientY = e.nativeEvent.clientY; clientY = e.nativeEvent.clientY;
} }
if (clientY && draggableElementRef.current && isDesktop) { if (clientY && draggableElementRef.current) {
const draggableElementRect = const draggableElementRect =
draggableElementRef.current.getBoundingClientRect(); draggableElementRef.current.getBoundingClientRect();
if (!isDragging) { if (!isDragging) {
@ -203,6 +206,12 @@ function useDraggableElement({
[contentRef, draggableElementRef, timelineRef, getClientYPosition], [contentRef, draggableElementRef, timelineRef, getClientYPosition],
); );
useEffect(() => {
if (timelineRef.current) {
setSegments(Array.from(timelineRef.current.querySelectorAll(".segment")));
}
}, [timelineRef, segmentDuration, timelineDuration, timelineCollapsed]);
useEffect(() => { useEffect(() => {
let animationFrameId: number | null = null; let animationFrameId: number | null = null;
@ -211,13 +220,11 @@ function useDraggableElement({
timelineRef.current && timelineRef.current &&
showDraggableElement && showDraggableElement &&
isDragging && isDragging &&
clientYPosition clientYPosition &&
segments
) { ) {
const { const { scrollHeight: timelineHeight, scrollTop: scrolled } =
scrollHeight: timelineHeight, timelineRef.current;
scrollTop: scrolled,
offsetTop: timelineTop,
} = timelineRef.current;
const segmentHeight = const segmentHeight =
timelineHeight / (timelineDuration / segmentDuration); timelineHeight / (timelineDuration / segmentDuration);
@ -235,22 +242,53 @@ function useDraggableElement({
? timestampToPixels(draggableElementLatestTime) ? timestampToPixels(draggableElementLatestTime)
: segmentHeight * 2 + scrolled; : segmentHeight * 2 + scrolled;
const timelineRect = timelineRef.current.getBoundingClientRect();
const timelineTopAbsolute = timelineRect.top;
const newElementPosition = Math.min( const newElementPosition = Math.min(
elementEarliest, elementEarliest,
Math.max( Math.max(
elementLatest, elementLatest,
// current Y position // current Y position
clientYPosition - clientYPosition -
timelineTop + timelineTopAbsolute +
parentScrollTop - parentScrollTop -
initialClickAdjustment, initialClickAdjustment,
), ),
); );
const segmentIndex = Math.floor(newElementPosition / segmentHeight); if (
const segmentStartTime = alignStartDateToTimeline( newElementPosition >= elementEarliest ||
timelineStartAligned - segmentIndex * segmentDuration, newElementPosition <= elementLatest
); ) {
return;
}
let targetSegmentId = 0;
let offset = 0;
segments.forEach((segmentElement: HTMLDivElement) => {
const rect = segmentElement.getBoundingClientRect();
const segmentTop =
rect.top + scrolled - timelineTopAbsolute - segmentHeight;
const segmentBottom =
rect.bottom + scrolled - timelineTopAbsolute - segmentHeight;
// Check if handlebar position falls within the segment bounds
if (
newElementPosition >= segmentTop &&
newElementPosition <= segmentBottom
) {
targetSegmentId = parseFloat(
segmentElement.getAttribute("data-segment-id") || "0",
);
offset = Math.min(
segmentBottom - newElementPosition,
segmentHeight,
);
return;
}
});
if (draggingAtTopEdge || draggingAtBottomEdge) { if (draggingAtTopEdge || draggingAtBottomEdge) {
let newPosition = clientYPosition; let newPosition = clientYPosition;
@ -267,17 +305,15 @@ function useDraggableElement({
} }
updateDraggableElementPosition( updateDraggableElementPosition(
newElementPosition - segmentHeight, newElementPosition,
segmentStartTime, targetSegmentId,
false, false,
false, false,
); );
if (setDraggableElementTime) { if (setDraggableElementTime) {
setDraggableElementTime( setDraggableElementTime(
timelineStartAligned - targetSegmentId + segmentDuration * (offset / segmentHeight),
((newElementPosition - segmentHeight / 2 - 2) / segmentHeight) *
segmentDuration,
); );
} }
@ -321,7 +357,8 @@ function useDraggableElement({
draggableElementRef.current && draggableElementRef.current &&
showDraggableElement && showDraggableElement &&
draggableElementTime && draggableElementTime &&
!isDragging !isDragging &&
segments.length > 0
) { ) {
const { scrollHeight: timelineHeight, scrollTop: scrolled } = const { scrollHeight: timelineHeight, scrollTop: scrolled } =
timelineRef.current; timelineRef.current;
@ -329,29 +366,60 @@ function useDraggableElement({
const segmentHeight = const segmentHeight =
timelineHeight / (timelineDuration / segmentDuration); timelineHeight / (timelineDuration / segmentDuration);
const parentScrollTop = getCumulativeScrollTop(timelineRef.current); const alignedSegmentTime = alignStartDateToTimeline(draggableElementTime);
const newElementPosition = let segmentElement = timelineRef.current.querySelector(
((timelineStartAligned - draggableElementTime) / segmentDuration) * `[data-segment-id="${alignedSegmentTime}"]`,
segmentHeight +
parentScrollTop -
scrolled -
2; // height of draggableElement horizontal line
updateDraggableElementPosition(
newElementPosition,
draggableElementTime,
true,
true,
); );
if (!segmentElement) {
// segment not found, maybe we collapsed over a collapsible segment
let searchTime = alignedSegmentTime;
while (searchTime >= timelineStartAligned - timelineDuration) {
// Decrement currentTime by segmentDuration
searchTime -= segmentDuration;
segmentElement = timelineRef.current.querySelector(
`[data-segment-id="${searchTime}"]`,
);
if (segmentElement) {
// segmentElement found
break;
}
}
}
if (segmentElement) {
const timelineRect = timelineRef.current.getBoundingClientRect();
const timelineTopAbsolute = timelineRect.top;
const rect = segmentElement.getBoundingClientRect();
const segmentTop =
rect.top + scrolled - timelineTopAbsolute - segmentHeight / 2;
const offset =
((draggableElementTime - alignedSegmentTime) / segmentDuration) *
segmentHeight;
const newElementPosition = segmentTop - offset;
updateDraggableElementPosition(
newElementPosition,
draggableElementTime,
true,
true,
);
}
} }
// we know that these deps are correct // we know that these deps are correct
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [ }, [
draggableElementTime, draggableElementTime,
timelineDuration,
segmentDuration,
showDraggableElement, showDraggableElement,
draggableElementRef, draggableElementRef,
timelineStartAligned, timelineStartAligned,
timelineRef,
timelineCollapsed,
segments,
]); ]);
return { handleMouseDown, handleMouseUp, handleMouseMove }; return { handleMouseDown, handleMouseUp, handleMouseMove };

View File

@ -196,6 +196,8 @@ export default function EventView({
[reviewItems], [reviewItems],
); );
const [motionOnly, setMotionOnly] = useState(false);
if (!config) { if (!config) {
return <ActivityIndicator />; return <ActivityIndicator />;
} }
@ -253,6 +255,9 @@ export default function EventView({
reviewSummary={reviewSummary} reviewSummary={reviewSummary}
filter={filter} filter={filter}
onUpdateFilter={updateFilter} onUpdateFilter={updateFilter}
severity={severity}
motionOnly={motionOnly}
setMotionOnly={setMotionOnly}
/> />
) : ( ) : (
<ReviewActionGroup <ReviewActionGroup
@ -290,6 +295,7 @@ export default function EventView({
timeRange={timeRange} timeRange={timeRange}
startTime={startTime} startTime={startTime}
filter={filter} filter={filter}
motionOnly={motionOnly}
onOpenRecording={onOpenRecording} onOpenRecording={onOpenRecording}
/> />
)} )}
@ -603,6 +609,7 @@ type MotionReviewProps = {
timeRange: { before: number; after: number }; timeRange: { before: number; after: number };
startTime?: number; startTime?: number;
filter?: ReviewFilter; filter?: ReviewFilter;
motionOnly?: boolean;
onOpenRecording: (data: RecordingStartingPoint) => void; onOpenRecording: (data: RecordingStartingPoint) => void;
}; };
function MotionReview({ function MotionReview({
@ -612,6 +619,7 @@ function MotionReview({
timeRange, timeRange,
startTime, startTime,
filter, filter,
motionOnly = false,
onOpenRecording, onOpenRecording,
}: MotionReviewProps) { }: MotionReviewProps) {
const segmentDuration = 30; const segmentDuration = 30;
@ -784,6 +792,7 @@ function MotionReview({
timestampSpread={15} timestampSpread={15}
timelineStart={timeRangeSegments.end} timelineStart={timeRangeSegments.end}
timelineEnd={timeRangeSegments.start} timelineEnd={timeRangeSegments.start}
motionOnly={motionOnly}
showHandlebar showHandlebar
handlebarTime={currentTime} handlebarTime={currentTime}
setHandlebarTime={setCurrentTime} setHandlebarTime={setCurrentTime}

View File

@ -55,7 +55,7 @@
--border: 214.3 31.8% 91.4%; --border: 214.3 31.8% 91.4%;
--input: hsl(214.3 31.8% 91.4%); --input: hsl(214.3 31.8% 91.4%);
--input: 214.3 31.8% 91.4%; --input: 0 0 85%;
--ring: hsl(222.2 84% 4.9%); --ring: hsl(222.2 84% 4.9%);
--ring: 222.2 84% 4.9%; --ring: 222.2 84% 4.9%;
@ -140,7 +140,7 @@
--border: 0 0% 32%; --border: 0 0% 32%;
--input: hsl(217.2 32.6% 17.5%); --input: hsl(217.2 32.6% 17.5%);
--input: 217.2 32.6% 17.5%; --input: 0 0 25%;
--ring: hsl(212.7 26.8% 83.9%); --ring: hsl(212.7 26.8% 83.9%);
--ring: 212.7 26.8% 83.9%; --ring: 212.7 26.8% 83.9%;