mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
option to show motion only on motion timeline (#10626)
This commit is contained in:
parent
8e1d18d06b
commit
4159334520
@ -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,6 +116,12 @@ export default function ReviewFilterGroup({
|
|||||||
}
|
}
|
||||||
updateSelectedDay={onUpdateSelectedDay}
|
updateSelectedDay={onUpdateSelectedDay}
|
||||||
/>
|
/>
|
||||||
|
{severity == "significant_motion" ? (
|
||||||
|
<ShowMotionOnlyButton
|
||||||
|
motionOnly={motionOnly}
|
||||||
|
setMotionOnly={setMotionOnly}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<GeneralFilterButton
|
<GeneralFilterButton
|
||||||
allLabels={filterValues.labels}
|
allLabels={filterValues.labels}
|
||||||
selectedLabels={filter?.labels}
|
selectedLabels={filter?.labels}
|
||||||
@ -121,6 +133,7 @@ export default function ReviewFilterGroup({
|
|||||||
onUpdateFilter({ ...filter, showReviewed: 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -236,11 +236,13 @@ 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) {
|
||||||
|
scrollIntoView(element, {
|
||||||
scrollMode: "if-needed",
|
scrollMode: "if-needed",
|
||||||
behavior: "smooth",
|
behavior: "smooth",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}, [
|
}, [
|
||||||
selectedTimelineRef,
|
selectedTimelineRef,
|
||||||
segments,
|
segments,
|
||||||
|
@ -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)}
|
||||||
>
|
>
|
||||||
|
@ -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,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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,6 +182,11 @@ export function MotionSegment({
|
|||||||
}, [segmentTime, setHandlebarTime]);
|
}, [segmentTime, setHandlebarTime]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
{(((firstHalfSegmentWidth > 1 || secondHalfSegmentWidth > 1) &&
|
||||||
|
motionOnly &&
|
||||||
|
severity[0] < 2) ||
|
||||||
|
!motionOnly) && (
|
||||||
<div
|
<div
|
||||||
key={segmentKey}
|
key={segmentKey}
|
||||||
data-segment-id={segmentKey}
|
data-segment-id={segmentKey}
|
||||||
@ -187,6 +194,8 @@ export function MotionSegment({
|
|||||||
onClick={segmentClick}
|
onClick={segmentClick}
|
||||||
onTouchEnd={(event) => handleTouchStart(event, segmentClick)}
|
onTouchEnd={(event) => handleTouchStart(event, segmentClick)}
|
||||||
>
|
>
|
||||||
|
{!motionOnly && (
|
||||||
|
<>
|
||||||
<MinimapBounds
|
<MinimapBounds
|
||||||
isFirstSegmentInMinimap={isFirstSegmentInMinimap}
|
isFirstSegmentInMinimap={isFirstSegmentInMinimap}
|
||||||
isLastSegmentInMinimap={isLastSegmentInMinimap}
|
isLastSegmentInMinimap={isLastSegmentInMinimap}
|
||||||
@ -195,15 +204,22 @@ export function MotionSegment({
|
|||||||
firstMinimapSegmentRef={firstMinimapSegmentRef}
|
firstMinimapSegmentRef={firstMinimapSegmentRef}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Tick timestamp={timestamp} timestampSpread={timestampSpread} />
|
<Tick
|
||||||
|
key={`${segmentKey}_tick`}
|
||||||
|
timestamp={timestamp}
|
||||||
|
timestampSpread={timestampSpread}
|
||||||
|
/>
|
||||||
|
|
||||||
<Timestamp
|
<Timestamp
|
||||||
|
key={`${segmentKey}_timestamp`}
|
||||||
isFirstSegmentInMinimap={isFirstSegmentInMinimap}
|
isFirstSegmentInMinimap={isFirstSegmentInMinimap}
|
||||||
isLastSegmentInMinimap={isLastSegmentInMinimap}
|
isLastSegmentInMinimap={isLastSegmentInMinimap}
|
||||||
timestamp={timestamp}
|
timestamp={timestamp}
|
||||||
timestampSpread={timestampSpread}
|
timestampSpread={timestampSpread}
|
||||||
segmentKey={segmentKey}
|
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]">
|
||||||
@ -231,7 +247,8 @@ export function MotionSegment({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{severity.map((severityValue: number, index: number) => {
|
{!motionOnly &&
|
||||||
|
severity.map((severityValue: number, index: number) => {
|
||||||
if (severityValue > 1) {
|
if (severityValue > 1) {
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={index}>
|
<React.Fragment key={index}>
|
||||||
@ -253,6 +270,8 @@ export function MotionSegment({
|
|||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 };
|
||||||
|
@ -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,14 +366,39 @@ 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 -
|
if (!segmentElement) {
|
||||||
2; // height of draggableElement horizontal line
|
// 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(
|
updateDraggableElementPosition(
|
||||||
newElementPosition,
|
newElementPosition,
|
||||||
@ -345,13 +407,19 @@ function useDraggableElement({
|
|||||||
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 };
|
||||||
|
@ -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}
|
||||||
|
@ -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%;
|
||||||
|
Loading…
Reference in New Issue
Block a user