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
9 changed files with 287 additions and 127 deletions

View File

@@ -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>
</>
);
}

View File

@@ -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,

View File

@@ -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)}
>

View File

@@ -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,
],
);

View File

@@ -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>
)}
</>
);
}

View File

@@ -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 };