mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-12-19 19:06:16 +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,
|
||||
DropdownMenuTrigger,
|
||||
} from "../ui/dropdown-menu";
|
||||
import { ReviewFilter, ReviewSummary } from "@/types/review";
|
||||
import { ReviewFilter, ReviewSeverity, ReviewSummary } from "@/types/review";
|
||||
import { getEndOfDayTimestamp } from "@/utils/dateUtil";
|
||||
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 { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
|
||||
import { Switch } from "../ui/switch";
|
||||
@ -27,12 +27,18 @@ type ReviewFilterGroupProps = {
|
||||
reviewSummary?: ReviewSummary;
|
||||
filter?: ReviewFilter;
|
||||
onUpdateFilter: (filter: ReviewFilter) => void;
|
||||
severity: ReviewSeverity;
|
||||
motionOnly: boolean;
|
||||
setMotionOnly: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
export default function ReviewFilterGroup({
|
||||
reviewSummary,
|
||||
filter,
|
||||
onUpdateFilter,
|
||||
severity,
|
||||
motionOnly,
|
||||
setMotionOnly,
|
||||
}: ReviewFilterGroupProps) {
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
|
||||
@ -94,7 +100,7 @@ export default function ReviewFilterGroup({
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-center">
|
||||
<CamerasFilterButton
|
||||
allCameras={filterValues.cameras}
|
||||
groups={groups}
|
||||
@ -110,17 +116,24 @@ export default function ReviewFilterGroup({
|
||||
}
|
||||
updateSelectedDay={onUpdateSelectedDay}
|
||||
/>
|
||||
<GeneralFilterButton
|
||||
allLabels={filterValues.labels}
|
||||
selectedLabels={filter?.labels}
|
||||
updateLabelFilter={(newLabels) => {
|
||||
onUpdateFilter({ ...filter, labels: newLabels });
|
||||
}}
|
||||
showReviewed={filter?.showReviewed || 0}
|
||||
setShowReviewed={(reviewed) =>
|
||||
onUpdateFilter({ ...filter, showReviewed: reviewed })
|
||||
}
|
||||
/>
|
||||
{severity == "significant_motion" ? (
|
||||
<ShowMotionOnlyButton
|
||||
motionOnly={motionOnly}
|
||||
setMotionOnly={setMotionOnly}
|
||||
/>
|
||||
) : (
|
||||
<GeneralFilterButton
|
||||
allLabels={filterValues.labels}
|
||||
selectedLabels={filter?.labels}
|
||||
updateLabelFilter={(newLabels) => {
|
||||
onUpdateFilter({ ...filter, labels: newLabels });
|
||||
}}
|
||||
showReviewed={filter?.showReviewed || 0}
|
||||
setShowReviewed={(reviewed) =>
|
||||
onUpdateFilter({ ...filter, showReviewed: reviewed })
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -485,3 +498,46 @@ function GeneralFilterButton({
|
||||
</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,10 +236,12 @@ export function EventReviewTimeline({
|
||||
const element = selectedTimelineRef.current?.querySelector(
|
||||
`[data-segment-id="${Math.max(...alignedVisibleTimestamps)}"]`,
|
||||
);
|
||||
scrollIntoView(element as HTMLDivElement, {
|
||||
scrollMode: "if-needed",
|
||||
behavior: "smooth",
|
||||
});
|
||||
if (element) {
|
||||
scrollIntoView(element, {
|
||||
scrollMode: "if-needed",
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [
|
||||
selectedTimelineRef,
|
||||
|
@ -201,7 +201,7 @@ export function EventSegment({
|
||||
<div
|
||||
key={segmentKey}
|
||||
data-segment-id={segmentKey}
|
||||
className={segmentClasses}
|
||||
className={`segment ${segmentClasses}`}
|
||||
onClick={segmentClick}
|
||||
onTouchEnd={(event) => handleTouchStart(event, segmentClick)}
|
||||
>
|
||||
|
@ -21,6 +21,7 @@ export type MotionReviewTimelineProps = {
|
||||
showHandlebar?: boolean;
|
||||
handlebarTime?: number;
|
||||
setHandlebarTime?: React.Dispatch<React.SetStateAction<number>>;
|
||||
motionOnly?: boolean;
|
||||
showMinimap?: boolean;
|
||||
minimapStartTime?: number;
|
||||
minimapEndTime?: number;
|
||||
@ -45,6 +46,7 @@ export function MotionReviewTimeline({
|
||||
showHandlebar = false,
|
||||
handlebarTime,
|
||||
setHandlebarTime,
|
||||
motionOnly = false,
|
||||
showMinimap = false,
|
||||
minimapStartTime,
|
||||
minimapEndTime,
|
||||
@ -113,6 +115,7 @@ export function MotionReviewTimeline({
|
||||
draggableElementTime: handlebarTime,
|
||||
setDraggableElementTime: setHandlebarTime,
|
||||
timelineDuration,
|
||||
timelineCollapsed: motionOnly,
|
||||
timelineStartAligned,
|
||||
isDragging,
|
||||
setIsDragging,
|
||||
@ -176,6 +179,7 @@ export function MotionReviewTimeline({
|
||||
segmentDuration={segmentDuration}
|
||||
segmentTime={segmentTime}
|
||||
timestampSpread={timestampSpread}
|
||||
motionOnly={motionOnly}
|
||||
showMinimap={showMinimap}
|
||||
minimapStartTime={minimapStartTime}
|
||||
minimapEndTime={minimapEndTime}
|
||||
@ -195,6 +199,7 @@ export function MotionReviewTimeline({
|
||||
minimapEndTime,
|
||||
events,
|
||||
motion_events,
|
||||
motionOnly,
|
||||
]);
|
||||
|
||||
const segments = useMemo(
|
||||
@ -211,6 +216,7 @@ export function MotionReviewTimeline({
|
||||
minimapEndTime,
|
||||
events,
|
||||
motion_events,
|
||||
motionOnly,
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -14,6 +14,7 @@ type MotionSegmentProps = {
|
||||
segmentTime: number;
|
||||
segmentDuration: number;
|
||||
timestampSpread: number;
|
||||
motionOnly: boolean;
|
||||
showMinimap: boolean;
|
||||
minimapStartTime?: number;
|
||||
minimapEndTime?: number;
|
||||
@ -26,6 +27,7 @@ export function MotionSegment({
|
||||
segmentTime,
|
||||
segmentDuration,
|
||||
timestampSpread,
|
||||
motionOnly,
|
||||
showMinimap,
|
||||
minimapStartTime,
|
||||
minimapEndTime,
|
||||
@ -180,79 +182,96 @@ export function MotionSegment({
|
||||
}, [segmentTime, setHandlebarTime]);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={segmentKey}
|
||||
data-segment-id={segmentKey}
|
||||
className={`segment ${firstHalfSegmentWidth > 1 || secondHalfSegmentWidth > 1 ? "has-data" : ""} ${segmentClasses}`}
|
||||
onClick={segmentClick}
|
||||
onTouchEnd={(event) => handleTouchStart(event, segmentClick)}
|
||||
>
|
||||
<MinimapBounds
|
||||
isFirstSegmentInMinimap={isFirstSegmentInMinimap}
|
||||
isLastSegmentInMinimap={isLastSegmentInMinimap}
|
||||
alignedMinimapStartTime={alignedMinimapStartTime}
|
||||
alignedMinimapEndTime={alignedMinimapEndTime}
|
||||
firstMinimapSegmentRef={firstMinimapSegmentRef}
|
||||
/>
|
||||
<>
|
||||
{(((firstHalfSegmentWidth > 1 || secondHalfSegmentWidth > 1) &&
|
||||
motionOnly &&
|
||||
severity[0] < 2) ||
|
||||
!motionOnly) && (
|
||||
<div
|
||||
key={segmentKey}
|
||||
data-segment-id={segmentKey}
|
||||
className={`segment ${firstHalfSegmentWidth > 1 || secondHalfSegmentWidth > 1 ? "has-data" : ""} ${segmentClasses}`}
|
||||
onClick={segmentClick}
|
||||
onTouchEnd={(event) => handleTouchStart(event, segmentClick)}
|
||||
>
|
||||
{!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
|
||||
isFirstSegmentInMinimap={isFirstSegmentInMinimap}
|
||||
isLastSegmentInMinimap={isLastSegmentInMinimap}
|
||||
timestamp={timestamp}
|
||||
timestampSpread={timestampSpread}
|
||||
segmentKey={segmentKey}
|
||||
/>
|
||||
<Timestamp
|
||||
key={`${segmentKey}_timestamp`}
|
||||
isFirstSegmentInMinimap={isFirstSegmentInMinimap}
|
||||
isLastSegmentInMinimap={isLastSegmentInMinimap}
|
||||
timestamp={timestamp}
|
||||
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="flex flex-row justify-center w-[20px] md:w-[40px] mb-[1px]">
|
||||
<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 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 justify-center">
|
||||
<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]}
|
||||
`}
|
||||
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>
|
||||
</React.Fragment>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
})}
|
||||
</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>
|
||||
|
||||
{!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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||
import * as React from "react";
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
@ -10,18 +10,18 @@ const Switch = React.forwardRef<
|
||||
<SwitchPrimitives.Root
|
||||
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",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
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>
|
||||
))
|
||||
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>>;
|
||||
draggableElementTimeRef: React.MutableRefObject<HTMLDivElement | null>;
|
||||
timelineDuration: number;
|
||||
timelineCollapsed?: boolean;
|
||||
timelineStartAligned: number;
|
||||
isDragging: boolean;
|
||||
setIsDragging: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
@ -33,6 +34,7 @@ function useDraggableElement({
|
||||
setDraggableElementTime,
|
||||
draggableElementTimeRef,
|
||||
timelineDuration,
|
||||
timelineCollapsed,
|
||||
timelineStartAligned,
|
||||
isDragging,
|
||||
setIsDragging,
|
||||
@ -40,6 +42,7 @@ function useDraggableElement({
|
||||
}: DraggableElementProps) {
|
||||
const [clientYPosition, setClientYPosition] = useState<number | null>(null);
|
||||
const [initialClickAdjustment, setInitialClickAdjustment] = useState(0);
|
||||
const [segments, setSegments] = useState<HTMLDivElement[]>([]);
|
||||
const { alignStartDateToTimeline, getCumulativeScrollTop } = useTimelineUtils(
|
||||
{
|
||||
segmentDuration: segmentDuration,
|
||||
@ -101,7 +104,7 @@ function useDraggableElement({
|
||||
} else if (e.nativeEvent instanceof MouseEvent) {
|
||||
clientY = e.nativeEvent.clientY;
|
||||
}
|
||||
if (clientY && draggableElementRef.current && isDesktop) {
|
||||
if (clientY && draggableElementRef.current) {
|
||||
const draggableElementRect =
|
||||
draggableElementRef.current.getBoundingClientRect();
|
||||
if (!isDragging) {
|
||||
@ -203,6 +206,12 @@ function useDraggableElement({
|
||||
[contentRef, draggableElementRef, timelineRef, getClientYPosition],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (timelineRef.current) {
|
||||
setSegments(Array.from(timelineRef.current.querySelectorAll(".segment")));
|
||||
}
|
||||
}, [timelineRef, segmentDuration, timelineDuration, timelineCollapsed]);
|
||||
|
||||
useEffect(() => {
|
||||
let animationFrameId: number | null = null;
|
||||
|
||||
@ -211,13 +220,11 @@ function useDraggableElement({
|
||||
timelineRef.current &&
|
||||
showDraggableElement &&
|
||||
isDragging &&
|
||||
clientYPosition
|
||||
clientYPosition &&
|
||||
segments
|
||||
) {
|
||||
const {
|
||||
scrollHeight: timelineHeight,
|
||||
scrollTop: scrolled,
|
||||
offsetTop: timelineTop,
|
||||
} = timelineRef.current;
|
||||
const { scrollHeight: timelineHeight, scrollTop: scrolled } =
|
||||
timelineRef.current;
|
||||
|
||||
const segmentHeight =
|
||||
timelineHeight / (timelineDuration / segmentDuration);
|
||||
@ -235,22 +242,53 @@ function useDraggableElement({
|
||||
? timestampToPixels(draggableElementLatestTime)
|
||||
: segmentHeight * 2 + scrolled;
|
||||
|
||||
const timelineRect = timelineRef.current.getBoundingClientRect();
|
||||
const timelineTopAbsolute = timelineRect.top;
|
||||
|
||||
const newElementPosition = Math.min(
|
||||
elementEarliest,
|
||||
Math.max(
|
||||
elementLatest,
|
||||
// current Y position
|
||||
clientYPosition -
|
||||
timelineTop +
|
||||
timelineTopAbsolute +
|
||||
parentScrollTop -
|
||||
initialClickAdjustment,
|
||||
),
|
||||
);
|
||||
|
||||
const segmentIndex = Math.floor(newElementPosition / segmentHeight);
|
||||
const segmentStartTime = alignStartDateToTimeline(
|
||||
timelineStartAligned - segmentIndex * segmentDuration,
|
||||
);
|
||||
if (
|
||||
newElementPosition >= elementEarliest ||
|
||||
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) {
|
||||
let newPosition = clientYPosition;
|
||||
@ -267,17 +305,15 @@ function useDraggableElement({
|
||||
}
|
||||
|
||||
updateDraggableElementPosition(
|
||||
newElementPosition - segmentHeight,
|
||||
segmentStartTime,
|
||||
newElementPosition,
|
||||
targetSegmentId,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
|
||||
if (setDraggableElementTime) {
|
||||
setDraggableElementTime(
|
||||
timelineStartAligned -
|
||||
((newElementPosition - segmentHeight / 2 - 2) / segmentHeight) *
|
||||
segmentDuration,
|
||||
targetSegmentId + segmentDuration * (offset / segmentHeight),
|
||||
);
|
||||
}
|
||||
|
||||
@ -321,7 +357,8 @@ function useDraggableElement({
|
||||
draggableElementRef.current &&
|
||||
showDraggableElement &&
|
||||
draggableElementTime &&
|
||||
!isDragging
|
||||
!isDragging &&
|
||||
segments.length > 0
|
||||
) {
|
||||
const { scrollHeight: timelineHeight, scrollTop: scrolled } =
|
||||
timelineRef.current;
|
||||
@ -329,29 +366,60 @@ function useDraggableElement({
|
||||
const segmentHeight =
|
||||
timelineHeight / (timelineDuration / segmentDuration);
|
||||
|
||||
const parentScrollTop = getCumulativeScrollTop(timelineRef.current);
|
||||
const alignedSegmentTime = alignStartDateToTimeline(draggableElementTime);
|
||||
|
||||
const newElementPosition =
|
||||
((timelineStartAligned - draggableElementTime) / segmentDuration) *
|
||||
segmentHeight +
|
||||
parentScrollTop -
|
||||
scrolled -
|
||||
2; // height of draggableElement horizontal line
|
||||
|
||||
updateDraggableElementPosition(
|
||||
newElementPosition,
|
||||
draggableElementTime,
|
||||
true,
|
||||
true,
|
||||
let segmentElement = timelineRef.current.querySelector(
|
||||
`[data-segment-id="${alignedSegmentTime}"]`,
|
||||
);
|
||||
|
||||
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
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
draggableElementTime,
|
||||
timelineDuration,
|
||||
segmentDuration,
|
||||
showDraggableElement,
|
||||
draggableElementRef,
|
||||
timelineStartAligned,
|
||||
timelineRef,
|
||||
timelineCollapsed,
|
||||
segments,
|
||||
]);
|
||||
|
||||
return { handleMouseDown, handleMouseUp, handleMouseMove };
|
||||
|
@ -196,6 +196,8 @@ export default function EventView({
|
||||
[reviewItems],
|
||||
);
|
||||
|
||||
const [motionOnly, setMotionOnly] = useState(false);
|
||||
|
||||
if (!config) {
|
||||
return <ActivityIndicator />;
|
||||
}
|
||||
@ -253,6 +255,9 @@ export default function EventView({
|
||||
reviewSummary={reviewSummary}
|
||||
filter={filter}
|
||||
onUpdateFilter={updateFilter}
|
||||
severity={severity}
|
||||
motionOnly={motionOnly}
|
||||
setMotionOnly={setMotionOnly}
|
||||
/>
|
||||
) : (
|
||||
<ReviewActionGroup
|
||||
@ -290,6 +295,7 @@ export default function EventView({
|
||||
timeRange={timeRange}
|
||||
startTime={startTime}
|
||||
filter={filter}
|
||||
motionOnly={motionOnly}
|
||||
onOpenRecording={onOpenRecording}
|
||||
/>
|
||||
)}
|
||||
@ -603,6 +609,7 @@ type MotionReviewProps = {
|
||||
timeRange: { before: number; after: number };
|
||||
startTime?: number;
|
||||
filter?: ReviewFilter;
|
||||
motionOnly?: boolean;
|
||||
onOpenRecording: (data: RecordingStartingPoint) => void;
|
||||
};
|
||||
function MotionReview({
|
||||
@ -612,6 +619,7 @@ function MotionReview({
|
||||
timeRange,
|
||||
startTime,
|
||||
filter,
|
||||
motionOnly = false,
|
||||
onOpenRecording,
|
||||
}: MotionReviewProps) {
|
||||
const segmentDuration = 30;
|
||||
@ -784,6 +792,7 @@ function MotionReview({
|
||||
timestampSpread={15}
|
||||
timelineStart={timeRangeSegments.end}
|
||||
timelineEnd={timeRangeSegments.start}
|
||||
motionOnly={motionOnly}
|
||||
showHandlebar
|
||||
handlebarTime={currentTime}
|
||||
setHandlebarTime={setCurrentTime}
|
||||
|
@ -55,7 +55,7 @@
|
||||
--border: 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: 222.2 84% 4.9%;
|
||||
@ -140,7 +140,7 @@
|
||||
--border: 0 0% 32%;
|
||||
|
||||
--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: 212.7 26.8% 83.9%;
|
||||
|
Loading…
Reference in New Issue
Block a user