mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-06 05:24:11 +03:00
No recordings indicator on History timeline (#20715)
* black background * fix backend logic * fixes * ensure data being sent to api is segment aligned * tweak * tweaks to keep motion review as-is * fix for half segment fractional seconds when using zooming
This commit is contained in:
parent
9917fc3169
commit
61549a0151
@ -589,7 +589,7 @@ async def no_recordings(
|
|||||||
)
|
)
|
||||||
scale = params.scale
|
scale = params.scale
|
||||||
|
|
||||||
clauses = [(Recordings.start_time > after) & (Recordings.end_time < before)]
|
clauses = [(Recordings.end_time >= after) & (Recordings.start_time <= before)]
|
||||||
if cameras != "all":
|
if cameras != "all":
|
||||||
camera_list = cameras.split(",")
|
camera_list = cameras.split(",")
|
||||||
clauses.append((Recordings.camera << camera_list))
|
clauses.append((Recordings.camera << camera_list))
|
||||||
@ -608,33 +608,39 @@ async def no_recordings(
|
|||||||
# Convert recordings to list of (start, end) tuples
|
# Convert recordings to list of (start, end) tuples
|
||||||
recordings = [(r["start_time"], r["end_time"]) for r in data]
|
recordings = [(r["start_time"], r["end_time"]) for r in data]
|
||||||
|
|
||||||
# Generate all time segments
|
# Iterate through time segments and check if each has any recording
|
||||||
current = after
|
|
||||||
no_recording_segments = []
|
no_recording_segments = []
|
||||||
current_start = None
|
current = after
|
||||||
|
current_gap_start = None
|
||||||
|
|
||||||
while current < before:
|
while current < before:
|
||||||
segment_end = current + scale
|
segment_end = min(current + scale, before)
|
||||||
# Check if segment overlaps with any recording
|
|
||||||
|
# Check if this segment overlaps with any recording
|
||||||
has_recording = any(
|
has_recording = any(
|
||||||
start <= segment_end and end >= current for start, end in recordings
|
rec_start < segment_end and rec_end > current
|
||||||
|
for rec_start, rec_end in recordings
|
||||||
)
|
)
|
||||||
|
|
||||||
if not has_recording:
|
if not has_recording:
|
||||||
if current_start is None:
|
# This segment has no recordings
|
||||||
current_start = current # Start a new gap
|
if current_gap_start is None:
|
||||||
|
current_gap_start = current # Start a new gap
|
||||||
else:
|
else:
|
||||||
if current_start is not None:
|
# This segment has recordings
|
||||||
|
if current_gap_start is not None:
|
||||||
# End the current gap and append it
|
# End the current gap and append it
|
||||||
no_recording_segments.append(
|
no_recording_segments.append(
|
||||||
{"start_time": int(current_start), "end_time": int(current)}
|
{"start_time": int(current_gap_start), "end_time": int(current)}
|
||||||
)
|
)
|
||||||
current_start = None
|
current_gap_start = None
|
||||||
|
|
||||||
current = segment_end
|
current = segment_end
|
||||||
|
|
||||||
# Append the last gap if it exists
|
# Append the last gap if it exists
|
||||||
if current_start is not None:
|
if current_gap_start is not None:
|
||||||
no_recording_segments.append(
|
no_recording_segments.append(
|
||||||
{"start_time": int(current_start), "end_time": int(before)}
|
{"start_time": int(current_gap_start), "end_time": int(before)}
|
||||||
)
|
)
|
||||||
|
|
||||||
return JSONResponse(content=no_recording_segments)
|
return JSONResponse(content=no_recording_segments)
|
||||||
|
|||||||
@ -46,6 +46,7 @@ export type MotionReviewTimelineProps = {
|
|||||||
dense?: boolean;
|
dense?: boolean;
|
||||||
isZooming: boolean;
|
isZooming: boolean;
|
||||||
zoomDirection: TimelineZoomDirection;
|
zoomDirection: TimelineZoomDirection;
|
||||||
|
alwaysShowMotionLine?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function MotionReviewTimeline({
|
export function MotionReviewTimeline({
|
||||||
@ -75,6 +76,7 @@ export function MotionReviewTimeline({
|
|||||||
dense = false,
|
dense = false,
|
||||||
isZooming,
|
isZooming,
|
||||||
zoomDirection,
|
zoomDirection,
|
||||||
|
alwaysShowMotionLine = false,
|
||||||
}: MotionReviewTimelineProps) {
|
}: MotionReviewTimelineProps) {
|
||||||
const internalTimelineRef = useRef<HTMLDivElement>(null);
|
const internalTimelineRef = useRef<HTMLDivElement>(null);
|
||||||
const selectedTimelineRef = timelineRef || internalTimelineRef;
|
const selectedTimelineRef = timelineRef || internalTimelineRef;
|
||||||
@ -203,6 +205,7 @@ export function MotionReviewTimeline({
|
|||||||
scrollToSegment={scrollToSegment}
|
scrollToSegment={scrollToSegment}
|
||||||
isZooming={isZooming}
|
isZooming={isZooming}
|
||||||
zoomDirection={zoomDirection}
|
zoomDirection={zoomDirection}
|
||||||
|
getRecordingAvailability={getRecordingAvailability}
|
||||||
>
|
>
|
||||||
<VirtualizedMotionSegments
|
<VirtualizedMotionSegments
|
||||||
ref={virtualizedSegmentsRef}
|
ref={virtualizedSegmentsRef}
|
||||||
@ -221,6 +224,7 @@ export function MotionReviewTimeline({
|
|||||||
motionOnly={motionOnly}
|
motionOnly={motionOnly}
|
||||||
getMotionSegmentValue={getMotionSegmentValue}
|
getMotionSegmentValue={getMotionSegmentValue}
|
||||||
getRecordingAvailability={getRecordingAvailability}
|
getRecordingAvailability={getRecordingAvailability}
|
||||||
|
alwaysShowMotionLine={alwaysShowMotionLine}
|
||||||
/>
|
/>
|
||||||
</ReviewTimeline>
|
</ReviewTimeline>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -16,6 +16,8 @@ type MotionSegmentProps = {
|
|||||||
firstHalfMotionValue: number;
|
firstHalfMotionValue: number;
|
||||||
secondHalfMotionValue: number;
|
secondHalfMotionValue: number;
|
||||||
hasRecording?: boolean;
|
hasRecording?: boolean;
|
||||||
|
prevIsNoRecording?: boolean;
|
||||||
|
nextIsNoRecording?: boolean;
|
||||||
motionOnly: boolean;
|
motionOnly: boolean;
|
||||||
showMinimap: boolean;
|
showMinimap: boolean;
|
||||||
minimapStartTime?: number;
|
minimapStartTime?: number;
|
||||||
@ -23,6 +25,7 @@ type MotionSegmentProps = {
|
|||||||
setHandlebarTime?: React.Dispatch<React.SetStateAction<number>>;
|
setHandlebarTime?: React.Dispatch<React.SetStateAction<number>>;
|
||||||
scrollToSegment: (segmentTime: number, ifNeeded?: boolean) => void;
|
scrollToSegment: (segmentTime: number, ifNeeded?: boolean) => void;
|
||||||
dense: boolean;
|
dense: boolean;
|
||||||
|
alwaysShowMotionLine?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function MotionSegment({
|
export function MotionSegment({
|
||||||
@ -33,6 +36,8 @@ export function MotionSegment({
|
|||||||
firstHalfMotionValue,
|
firstHalfMotionValue,
|
||||||
secondHalfMotionValue,
|
secondHalfMotionValue,
|
||||||
hasRecording,
|
hasRecording,
|
||||||
|
prevIsNoRecording,
|
||||||
|
nextIsNoRecording,
|
||||||
motionOnly,
|
motionOnly,
|
||||||
showMinimap,
|
showMinimap,
|
||||||
minimapStartTime,
|
minimapStartTime,
|
||||||
@ -40,6 +45,7 @@ export function MotionSegment({
|
|||||||
setHandlebarTime,
|
setHandlebarTime,
|
||||||
scrollToSegment,
|
scrollToSegment,
|
||||||
dense,
|
dense,
|
||||||
|
alwaysShowMotionLine = false,
|
||||||
}: MotionSegmentProps) {
|
}: MotionSegmentProps) {
|
||||||
const severityType = "all";
|
const severityType = "all";
|
||||||
const { getSeverity, getReviewed, displaySeverityType } =
|
const { getSeverity, getReviewed, displaySeverityType } =
|
||||||
@ -116,6 +122,16 @@ export function MotionSegment({
|
|||||||
return showMinimap && segmentTime === alignedMinimapEndTime;
|
return showMinimap && segmentTime === alignedMinimapEndTime;
|
||||||
}, [showMinimap, segmentTime, alignedMinimapEndTime]);
|
}, [showMinimap, segmentTime, alignedMinimapEndTime]);
|
||||||
|
|
||||||
|
// Bottom border: current segment HAS recording, but next segment (below/earlier) has NO recording
|
||||||
|
const isFirstSegmentWithoutRecording = useMemo(() => {
|
||||||
|
return hasRecording === true && nextIsNoRecording === true;
|
||||||
|
}, [hasRecording, nextIsNoRecording]);
|
||||||
|
|
||||||
|
// Top border: current segment HAS recording, but prev segment (above/later) has NO recording
|
||||||
|
const isLastSegmentWithoutRecording = useMemo(() => {
|
||||||
|
return hasRecording === true && prevIsNoRecording === true;
|
||||||
|
}, [hasRecording, prevIsNoRecording]);
|
||||||
|
|
||||||
const firstMinimapSegmentRef = useRef<HTMLDivElement>(null);
|
const firstMinimapSegmentRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -178,16 +194,17 @@ export function MotionSegment({
|
|||||||
segmentClasses,
|
segmentClasses,
|
||||||
severity[0] && "bg-gradient-to-r",
|
severity[0] && "bg-gradient-to-r",
|
||||||
severity[0] && severityColorsBg[severity[0]],
|
severity[0] && severityColorsBg[severity[0]],
|
||||||
// TODO: will update this for 0.17
|
hasRecording == false && "bg-background",
|
||||||
false &&
|
|
||||||
hasRecording == false &&
|
|
||||||
firstHalfMotionValue == 0 &&
|
|
||||||
secondHalfMotionValue == 0 &&
|
|
||||||
"bg-slashes",
|
|
||||||
)}
|
)}
|
||||||
onClick={segmentClick}
|
onClick={segmentClick}
|
||||||
onTouchEnd={(event) => handleTouchStart(event, segmentClick)}
|
onTouchEnd={(event) => handleTouchStart(event, segmentClick)}
|
||||||
>
|
>
|
||||||
|
{isFirstSegmentWithoutRecording && (
|
||||||
|
<div className="absolute bottom-[0px] left-0 right-0 h-[1px] bg-primary-variant/40" />
|
||||||
|
)}
|
||||||
|
{isLastSegmentWithoutRecording && (
|
||||||
|
<div className="absolute -top-[1px] left-0 right-0 h-[1px] bg-primary-variant/50" />
|
||||||
|
)}
|
||||||
{!motionOnly && (
|
{!motionOnly && (
|
||||||
<>
|
<>
|
||||||
{showMinimap && (
|
{showMinimap && (
|
||||||
@ -218,45 +235,50 @@ export function MotionSegment({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="absolute left-1/2 z-10 h-[8px] w-[20px] -translate-x-1/2 transform cursor-pointer md:w-[40px]">
|
{(hasRecording ||
|
||||||
<div className="mb-[1px] flex w-[20px] flex-row justify-center pt-[1px] md:w-[40px]">
|
firstHalfSegmentWidth > 0 ||
|
||||||
<div className="mb-[1px] flex justify-center">
|
secondHalfSegmentWidth > 0 ||
|
||||||
<div
|
alwaysShowMotionLine) && (
|
||||||
key={`${segmentKey}_motion_data_1`}
|
<div className="absolute left-1/2 z-10 h-[8px] w-[20px] -translate-x-1/2 transform cursor-pointer md:w-[40px]">
|
||||||
data-motion-value={secondHalfSegmentWidth}
|
<div className="mb-[1px] flex w-[20px] flex-row justify-center pt-[1px] md:w-[40px]">
|
||||||
className={cn(
|
<div className="mb-[1px] flex justify-center">
|
||||||
"h-[2px]",
|
<div
|
||||||
"rounded-full",
|
key={`${segmentKey}_motion_data_1`}
|
||||||
secondHalfSegmentWidth
|
data-motion-value={secondHalfSegmentWidth}
|
||||||
? "bg-motion_review"
|
className={cn(
|
||||||
: "bg-muted-foreground",
|
"h-[2px]",
|
||||||
)}
|
"rounded-full",
|
||||||
style={{
|
secondHalfSegmentWidth
|
||||||
width: secondHalfSegmentWidth || 1,
|
? "bg-motion_review"
|
||||||
}}
|
: "bg-muted-foreground",
|
||||||
></div>
|
)}
|
||||||
|
style={{
|
||||||
|
width: secondHalfSegmentWidth || 1,
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex w-[20px] flex-row justify-center pb-[1px] md:w-[40px]">
|
<div className="flex w-[20px] flex-row justify-center pb-[1px] md:w-[40px]">
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<div
|
<div
|
||||||
key={`${segmentKey}_motion_data_2`}
|
key={`${segmentKey}_motion_data_2`}
|
||||||
data-motion-value={firstHalfSegmentWidth}
|
data-motion-value={firstHalfSegmentWidth}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-[2px]",
|
"h-[2px]",
|
||||||
"rounded-full",
|
"rounded-full",
|
||||||
firstHalfSegmentWidth
|
firstHalfSegmentWidth
|
||||||
? "bg-motion_review"
|
? "bg-motion_review"
|
||||||
: "bg-muted-foreground",
|
: "bg-muted-foreground",
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
width: firstHalfSegmentWidth || 1,
|
width: firstHalfSegmentWidth || 1,
|
||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -36,6 +36,7 @@ export type ReviewTimelineProps = {
|
|||||||
scrollToSegment: (segmentTime: number, ifNeeded?: boolean) => void;
|
scrollToSegment: (segmentTime: number, ifNeeded?: boolean) => void;
|
||||||
isZooming: boolean;
|
isZooming: boolean;
|
||||||
zoomDirection: TimelineZoomDirection;
|
zoomDirection: TimelineZoomDirection;
|
||||||
|
getRecordingAvailability?: (time: number) => boolean | undefined;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -61,6 +62,7 @@ export function ReviewTimeline({
|
|||||||
scrollToSegment,
|
scrollToSegment,
|
||||||
isZooming,
|
isZooming,
|
||||||
zoomDirection,
|
zoomDirection,
|
||||||
|
getRecordingAvailability,
|
||||||
children,
|
children,
|
||||||
}: ReviewTimelineProps) {
|
}: ReviewTimelineProps) {
|
||||||
const [isDraggingHandlebar, setIsDraggingHandlebar] = useState(false);
|
const [isDraggingHandlebar, setIsDraggingHandlebar] = useState(false);
|
||||||
@ -326,6 +328,25 @@ export function ReviewTimeline({
|
|||||||
}
|
}
|
||||||
}, [isDraggingHandlebar, onHandlebarDraggingChange]);
|
}, [isDraggingHandlebar, onHandlebarDraggingChange]);
|
||||||
|
|
||||||
|
const isHandlebarInNoRecordingPeriod = useMemo(() => {
|
||||||
|
if (!getRecordingAvailability || handlebarTime === undefined) return false;
|
||||||
|
|
||||||
|
// Check current segment
|
||||||
|
const currentAvailability = getRecordingAvailability(handlebarTime);
|
||||||
|
if (currentAvailability !== false) return false;
|
||||||
|
|
||||||
|
// Check if at least one adjacent segment also has no recordings
|
||||||
|
const beforeAvailability = getRecordingAvailability(
|
||||||
|
handlebarTime - segmentDuration,
|
||||||
|
);
|
||||||
|
const afterAvailability = getRecordingAvailability(
|
||||||
|
handlebarTime + segmentDuration,
|
||||||
|
);
|
||||||
|
|
||||||
|
// If current segment has no recordings AND at least one adjacent segment also has no recordings
|
||||||
|
return beforeAvailability === false || afterAvailability === false;
|
||||||
|
}, [getRecordingAvailability, handlebarTime, segmentDuration]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={timelineRef}
|
ref={timelineRef}
|
||||||
@ -380,6 +401,12 @@ export function ReviewTimeline({
|
|||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* TODO: determine if we should keep this tooltip */}
|
||||||
|
{false && isHandlebarInNoRecordingPeriod && (
|
||||||
|
<div className="absolute left-1/2 top-full z-50 mt-2 -translate-x-1/2 rounded-md bg-destructive/80 px-4 py-1 text-center text-xs text-white shadow-lg">
|
||||||
|
No recordings
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{showExportHandles && (
|
{showExportHandles && (
|
||||||
|
|||||||
@ -25,6 +25,7 @@ type VirtualizedMotionSegmentsProps = {
|
|||||||
motionOnly: boolean;
|
motionOnly: boolean;
|
||||||
getMotionSegmentValue: (timestamp: number) => number;
|
getMotionSegmentValue: (timestamp: number) => number;
|
||||||
getRecordingAvailability: (timestamp: number) => boolean | undefined;
|
getRecordingAvailability: (timestamp: number) => boolean | undefined;
|
||||||
|
alwaysShowMotionLine: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface VirtualizedMotionSegmentsRef {
|
export interface VirtualizedMotionSegmentsRef {
|
||||||
@ -57,6 +58,7 @@ export const VirtualizedMotionSegments = forwardRef<
|
|||||||
motionOnly,
|
motionOnly,
|
||||||
getMotionSegmentValue,
|
getMotionSegmentValue,
|
||||||
getRecordingAvailability,
|
getRecordingAvailability,
|
||||||
|
alwaysShowMotionLine,
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
@ -158,6 +160,20 @@ export const VirtualizedMotionSegments = forwardRef<
|
|||||||
|
|
||||||
const hasRecording = getRecordingAvailability(segmentTime);
|
const hasRecording = getRecordingAvailability(segmentTime);
|
||||||
|
|
||||||
|
// Check if previous and next segments have recordings
|
||||||
|
// This is important because in motionOnly mode, the segments array is filtered
|
||||||
|
const prevSegmentTime = segmentTime + segmentDuration;
|
||||||
|
const nextSegmentTime = segmentTime - segmentDuration;
|
||||||
|
|
||||||
|
const prevHasRecording = getRecordingAvailability(prevSegmentTime);
|
||||||
|
const nextHasRecording = getRecordingAvailability(nextSegmentTime);
|
||||||
|
|
||||||
|
// Check if prev/next segments have no recording available
|
||||||
|
// Note: We only check hasRecording, not motion values, because segments can have
|
||||||
|
// recordings available but no motion (eg, start of a recording before motion begins)
|
||||||
|
const prevIsNoRecording = prevHasRecording === false;
|
||||||
|
const nextIsNoRecording = nextHasRecording === false;
|
||||||
|
|
||||||
if ((!segmentMotion || overlappingReviewItems) && motionOnly) {
|
if ((!segmentMotion || overlappingReviewItems) && motionOnly) {
|
||||||
return null; // Skip rendering this segment in motion only mode
|
return null; // Skip rendering this segment in motion only mode
|
||||||
}
|
}
|
||||||
@ -177,6 +193,8 @@ export const VirtualizedMotionSegments = forwardRef<
|
|||||||
firstHalfMotionValue={firstHalfMotionValue}
|
firstHalfMotionValue={firstHalfMotionValue}
|
||||||
secondHalfMotionValue={secondHalfMotionValue}
|
secondHalfMotionValue={secondHalfMotionValue}
|
||||||
hasRecording={hasRecording}
|
hasRecording={hasRecording}
|
||||||
|
prevIsNoRecording={prevIsNoRecording}
|
||||||
|
nextIsNoRecording={nextIsNoRecording}
|
||||||
segmentDuration={segmentDuration}
|
segmentDuration={segmentDuration}
|
||||||
segmentTime={segmentTime}
|
segmentTime={segmentTime}
|
||||||
timestampSpread={timestampSpread}
|
timestampSpread={timestampSpread}
|
||||||
@ -187,6 +205,7 @@ export const VirtualizedMotionSegments = forwardRef<
|
|||||||
setHandlebarTime={setHandlebarTime}
|
setHandlebarTime={setHandlebarTime}
|
||||||
scrollToSegment={scrollToSegment}
|
scrollToSegment={scrollToSegment}
|
||||||
dense={dense}
|
dense={dense}
|
||||||
|
alwaysShowMotionLine={alwaysShowMotionLine}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -205,6 +224,7 @@ export const VirtualizedMotionSegments = forwardRef<
|
|||||||
dense,
|
dense,
|
||||||
timestampSpread,
|
timestampSpread,
|
||||||
visibleRange.start,
|
visibleRange.start,
|
||||||
|
alwaysShowMotionLine,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -43,9 +43,14 @@ export const useMotionSegmentUtils = (
|
|||||||
const segmentStart = getSegmentStart(time);
|
const segmentStart = getSegmentStart(time);
|
||||||
const segmentEnd = getSegmentEnd(time);
|
const segmentEnd = getSegmentEnd(time);
|
||||||
const matchingEvents = motion_events.filter((event) => {
|
const matchingEvents = motion_events.filter((event) => {
|
||||||
return (
|
// Use integer ms math to avoid floating point rounding issues
|
||||||
event.start_time >= segmentStart && event.start_time < segmentEnd
|
// when halfSegmentDuration is not an integer
|
||||||
);
|
// (eg, 2.5 seconds from timeline zooming)
|
||||||
|
const eventMs = Math.round(event.start_time * 1000);
|
||||||
|
const halfMs = Math.round(halfSegmentDuration * 1000);
|
||||||
|
const eventBucketMs = Math.round(eventMs / halfMs) * halfMs;
|
||||||
|
const eventRounded = eventBucketMs / 1000;
|
||||||
|
return eventRounded >= segmentStart && eventRounded < segmentEnd;
|
||||||
});
|
});
|
||||||
|
|
||||||
const totalMotion = matchingEvents.reduce(
|
const totalMotion = matchingEvents.reduce(
|
||||||
@ -55,7 +60,7 @@ export const useMotionSegmentUtils = (
|
|||||||
|
|
||||||
return totalMotion;
|
return totalMotion;
|
||||||
},
|
},
|
||||||
[motion_events, getSegmentStart, getSegmentEnd],
|
[motion_events, getSegmentStart, getSegmentEnd, halfSegmentDuration],
|
||||||
);
|
);
|
||||||
|
|
||||||
const getAudioSegmentValue = useCallback(
|
const getAudioSegmentValue = useCallback(
|
||||||
|
|||||||
@ -895,11 +895,20 @@ function MotionReview({
|
|||||||
|
|
||||||
// motion data
|
// motion data
|
||||||
|
|
||||||
|
const { alignStartDateToTimeline, alignEndDateToTimeline } = useTimelineUtils(
|
||||||
|
{
|
||||||
|
segmentDuration,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const alignedAfter = alignStartDateToTimeline(timeRange.after);
|
||||||
|
const alignedBefore = alignEndDateToTimeline(timeRange.before);
|
||||||
|
|
||||||
const { data: motionData } = useSWR<MotionData[]>([
|
const { data: motionData } = useSWR<MotionData[]>([
|
||||||
"review/activity/motion",
|
"review/activity/motion",
|
||||||
{
|
{
|
||||||
before: timeRange.before,
|
before: alignedBefore,
|
||||||
after: timeRange.after,
|
after: alignedAfter,
|
||||||
scale: segmentDuration / 2,
|
scale: segmentDuration / 2,
|
||||||
cameras: filter?.cameras?.join(",") ?? null,
|
cameras: filter?.cameras?.join(",") ?? null,
|
||||||
},
|
},
|
||||||
@ -1006,10 +1015,6 @@ function MotionReview({
|
|||||||
}
|
}
|
||||||
}, [playing, playbackRate, nextTimestamp, setPlaying, timeRange]);
|
}, [playing, playbackRate, nextTimestamp, setPlaying, timeRange]);
|
||||||
|
|
||||||
const { alignStartDateToTimeline } = useTimelineUtils({
|
|
||||||
segmentDuration,
|
|
||||||
});
|
|
||||||
|
|
||||||
const getDetectionType = useCallback(
|
const getDetectionType = useCallback(
|
||||||
(cameraName: string) => {
|
(cameraName: string) => {
|
||||||
if (motionOnly) {
|
if (motionOnly) {
|
||||||
@ -1159,6 +1164,7 @@ function MotionReview({
|
|||||||
dense={isMobileOnly}
|
dense={isMobileOnly}
|
||||||
isZooming={false}
|
isZooming={false}
|
||||||
zoomDirection={null}
|
zoomDirection={null}
|
||||||
|
alwaysShowMotionLine={true}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Skeleton className="size-full" />
|
<Skeleton className="size-full" />
|
||||||
|
|||||||
@ -56,6 +56,7 @@ import { useFullscreen } from "@/hooks/use-fullscreen";
|
|||||||
import { useTimezone } from "@/hooks/use-date-utils";
|
import { useTimezone } from "@/hooks/use-date-utils";
|
||||||
import { useTimelineZoom } from "@/hooks/use-timeline-zoom";
|
import { useTimelineZoom } from "@/hooks/use-timeline-zoom";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useTimelineUtils } from "@/hooks/use-timeline-utils";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
@ -908,12 +909,20 @@ function Timeline({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// motion data
|
// motion data
|
||||||
|
const { alignStartDateToTimeline, alignEndDateToTimeline } = useTimelineUtils(
|
||||||
|
{
|
||||||
|
segmentDuration: zoomSettings.segmentDuration,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const alignedAfter = alignStartDateToTimeline(timeRange.after);
|
||||||
|
const alignedBefore = alignEndDateToTimeline(timeRange.before);
|
||||||
|
|
||||||
const { data: motionData, isLoading } = useSWR<MotionData[]>([
|
const { data: motionData, isLoading } = useSWR<MotionData[]>([
|
||||||
"review/activity/motion",
|
"review/activity/motion",
|
||||||
{
|
{
|
||||||
before: timeRange.before,
|
before: alignedBefore,
|
||||||
after: timeRange.after,
|
after: alignedAfter,
|
||||||
scale: Math.round(zoomSettings.segmentDuration / 2),
|
scale: Math.round(zoomSettings.segmentDuration / 2),
|
||||||
cameras: mainCamera,
|
cameras: mainCamera,
|
||||||
},
|
},
|
||||||
@ -922,9 +931,9 @@ function Timeline({
|
|||||||
const { data: noRecordings } = useSWR<RecordingSegment[]>([
|
const { data: noRecordings } = useSWR<RecordingSegment[]>([
|
||||||
"recordings/unavailable",
|
"recordings/unavailable",
|
||||||
{
|
{
|
||||||
before: timeRange.before,
|
before: alignedBefore,
|
||||||
after: timeRange.after,
|
after: alignedAfter,
|
||||||
scale: Math.round(zoomSettings.segmentDuration / 2),
|
scale: Math.round(zoomSettings.segmentDuration),
|
||||||
cameras: mainCamera,
|
cameras: mainCamera,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -44,7 +44,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
backgroundImage: {
|
backgroundImage: {
|
||||||
slashes:
|
slashes:
|
||||||
"repeating-linear-gradient(45deg, hsl(var(--primary-variant) / 0.2), hsl(var(--primary-variant) / 0.2) 2px, transparent 2px, transparent 8px)",
|
"repeating-linear-gradient(135deg, hsl(var(--primary-variant) / 0.3), hsl(var(--primary-variant) / 0.3) 2px, transparent 2px, transparent 8px), linear-gradient(to right, hsl(var(--background)), hsl(var(--background)))",
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
border: "hsl(var(--border))",
|
border: "hsl(var(--border))",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user