forked from Github/frigate
option to show motion only on motion timeline (#10626)
This commit is contained in:
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user