mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
Adapt review timeline for mobile devices (#10120)
* adapt timeline to mobile * remove unused * tweaks * pointer cursor on segments
This commit is contained in:
parent
5edaaceaf2
commit
485057abc1
32
web/package-lock.json
generated
32
web/package-lock.json
generated
@ -15,6 +15,7 @@
|
|||||||
"@radix-ui/react-context-menu": "^2.1.5",
|
"@radix-ui/react-context-menu": "^2.1.5",
|
||||||
"@radix-ui/react-dialog": "^1.0.5",
|
"@radix-ui/react-dialog": "^1.0.5",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||||
|
"@radix-ui/react-hover-card": "^1.0.7",
|
||||||
"@radix-ui/react-label": "^2.0.2",
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
"@radix-ui/react-popover": "^1.0.7",
|
"@radix-ui/react-popover": "^1.0.7",
|
||||||
"@radix-ui/react-radio-group": "^1.1.3",
|
"@radix-ui/react-radio-group": "^1.1.3",
|
||||||
@ -1392,6 +1393,37 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-hover-card": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-OcUN2FU0YpmajD/qkph3XzMcK/NmSk9hGWnjV68p6QiZMgILugusgQwnLSDs3oFSJYGKf3Y49zgFedhGh04k9A==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.13.10",
|
||||||
|
"@radix-ui/primitive": "1.0.1",
|
||||||
|
"@radix-ui/react-compose-refs": "1.0.1",
|
||||||
|
"@radix-ui/react-context": "1.0.1",
|
||||||
|
"@radix-ui/react-dismissable-layer": "1.0.5",
|
||||||
|
"@radix-ui/react-popper": "1.1.3",
|
||||||
|
"@radix-ui/react-portal": "1.0.4",
|
||||||
|
"@radix-ui/react-presence": "1.0.1",
|
||||||
|
"@radix-ui/react-primitive": "1.0.3",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-id": {
|
"node_modules/@radix-ui/react-id": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz",
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
"@radix-ui/react-context-menu": "^2.1.5",
|
"@radix-ui/react-context-menu": "^2.1.5",
|
||||||
"@radix-ui/react-dialog": "^1.0.5",
|
"@radix-ui/react-dialog": "^1.0.5",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||||
|
"@radix-ui/react-hover-card": "^1.0.7",
|
||||||
"@radix-ui/react-label": "^2.0.2",
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
"@radix-ui/react-popover": "^1.0.7",
|
"@radix-ui/react-popover": "^1.0.7",
|
||||||
"@radix-ui/react-radio-group": "^1.1.3",
|
"@radix-ui/react-radio-group": "^1.1.3",
|
||||||
|
@ -127,8 +127,8 @@ export default function LivePlayer({
|
|||||||
<div
|
<div
|
||||||
className={`relative flex justify-center w-full outline ${
|
className={`relative flex justify-center w-full outline ${
|
||||||
activeTracking
|
activeTracking
|
||||||
? "outline-severity_alert outline-1 rounded-2xl shadow-[0_0_6px_1px] shadow-severity_alert"
|
? "outline-severity_alert outline-1 rounded-2xl shadow-[0_0_6px_2px] shadow-severity_alert"
|
||||||
: "outline-0"
|
: "outline-0 outline-background"
|
||||||
} transition-all duration-500 ${className}`}
|
} transition-all duration-500 ${className}`}
|
||||||
>
|
>
|
||||||
<div className="absolute top-0 inset-x-0 rounded-2xl z-10 w-full h-[30%] bg-gradient-to-b from-black/20 to-transparent pointer-events-none"></div>
|
<div className="absolute top-0 inset-x-0 rounded-2xl z-10 w-full h-[30%] bg-gradient-to-b from-black/20 to-transparent pointer-events-none"></div>
|
||||||
|
@ -10,7 +10,6 @@ import {
|
|||||||
import EventSegment from "./EventSegment";
|
import EventSegment from "./EventSegment";
|
||||||
import { useEventUtils } from "@/hooks/use-event-utils";
|
import { useEventUtils } from "@/hooks/use-event-utils";
|
||||||
import { ReviewSegment, ReviewSeverity } from "@/types/review";
|
import { ReviewSegment, ReviewSeverity } from "@/types/review";
|
||||||
import { TooltipProvider } from "../ui/tooltip";
|
|
||||||
|
|
||||||
export type EventReviewTimelineProps = {
|
export type EventReviewTimelineProps = {
|
||||||
segmentDuration: number;
|
segmentDuration: number;
|
||||||
@ -56,14 +55,18 @@ export function EventReviewTimeline({
|
|||||||
[timelineEnd, timelineStart]
|
[timelineEnd, timelineStart]
|
||||||
);
|
);
|
||||||
|
|
||||||
const { alignDateToTimeline } = useEventUtils(events, segmentDuration);
|
const { alignStartDateToTimeline, alignEndDateToTimeline } = useEventUtils(
|
||||||
|
events,
|
||||||
|
segmentDuration
|
||||||
|
);
|
||||||
|
|
||||||
const { handleMouseDown, handleMouseUp, handleMouseMove } =
|
const { handleMouseDown, handleMouseUp, handleMouseMove } =
|
||||||
useDraggableHandler({
|
useDraggableHandler({
|
||||||
contentRef,
|
contentRef,
|
||||||
timelineRef,
|
timelineRef,
|
||||||
scrollTimeRef,
|
scrollTimeRef,
|
||||||
alignDateToTimeline,
|
alignStartDateToTimeline,
|
||||||
|
alignEndDateToTimeline,
|
||||||
segmentDuration,
|
segmentDuration,
|
||||||
showHandlebar,
|
showHandlebar,
|
||||||
timelineDuration,
|
timelineDuration,
|
||||||
@ -96,7 +99,7 @@ export function EventReviewTimeline({
|
|||||||
// Generate segments for the timeline
|
// Generate segments for the timeline
|
||||||
const generateSegments = useCallback(() => {
|
const generateSegments = useCallback(() => {
|
||||||
const segmentCount = timelineDuration / segmentDuration;
|
const segmentCount = timelineDuration / segmentDuration;
|
||||||
const segmentAlignedTime = alignDateToTimeline(timelineStart);
|
const segmentAlignedTime = alignStartDateToTimeline(timelineStart);
|
||||||
|
|
||||||
return Array.from({ length: segmentCount }, (_, index) => {
|
return Array.from({ length: segmentCount }, (_, index) => {
|
||||||
const segmentTime = segmentAlignedTime - index * segmentDuration;
|
const segmentTime = segmentAlignedTime - index * segmentDuration;
|
||||||
@ -172,7 +175,7 @@ export function EventReviewTimeline({
|
|||||||
timelineHeight / (timelineDuration / segmentDuration);
|
timelineHeight / (timelineDuration / segmentDuration);
|
||||||
|
|
||||||
// Calculate the segment index corresponding to the target time
|
// Calculate the segment index corresponding to the target time
|
||||||
const alignedHandlebarTime = alignDateToTimeline(handlebarTime);
|
const alignedHandlebarTime = alignStartDateToTimeline(handlebarTime);
|
||||||
const segmentIndex = Math.ceil(
|
const segmentIndex = Math.ceil(
|
||||||
(timelineStart - alignedHandlebarTime) / segmentDuration
|
(timelineStart - alignedHandlebarTime) / segmentDuration
|
||||||
);
|
);
|
||||||
@ -213,44 +216,39 @@ export function EventReviewTimeline({
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider skipDelayDuration={3000}>
|
<div
|
||||||
<div
|
ref={timelineRef}
|
||||||
ref={timelineRef}
|
className={`relative h-full overflow-y-scroll no-scrollbar bg-secondary ${
|
||||||
className={`relative w-[120px] md:w-[100px] h-full overflow-y-scroll no-scrollbar bg-secondary ${
|
isDragging && showHandlebar ? "cursor-grabbing" : "cursor-auto"
|
||||||
isDragging && showHandlebar ? "cursor-grabbing" : "cursor-auto"
|
}`}
|
||||||
}`}
|
>
|
||||||
>
|
<div className="flex flex-col">{segments}</div>
|
||||||
<div className="flex flex-col">{segments}</div>
|
{showHandlebar && (
|
||||||
{showHandlebar && (
|
<div className={`absolute left-0 top-0 z-20 w-full `} role="scrollbar">
|
||||||
<div
|
<div className={`flex items-center justify-center `}>
|
||||||
className={`absolute left-0 top-0 z-20 w-full `}
|
<div
|
||||||
role="scrollbar"
|
ref={scrollTimeRef}
|
||||||
>
|
className={`relative w-full ${
|
||||||
<div className={`flex items-center justify-center `}>
|
isDragging ? "cursor-grabbing" : "cursor-grab"
|
||||||
|
}`}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
ref={scrollTimeRef}
|
className={`bg-destructive rounded-full mx-auto ${
|
||||||
className={`relative w-full ${
|
segmentDuration < 60 ? "w-20" : "w-16"
|
||||||
isDragging ? "cursor-grabbing" : "cursor-grab"
|
} h-5 flex items-center justify-center`}
|
||||||
}`}
|
|
||||||
onMouseDown={handleMouseDown}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`bg-destructive rounded-full mx-auto ${
|
ref={currentTimeRef}
|
||||||
segmentDuration < 60 ? "w-20" : "w-16"
|
className="text-white text-xs z-10"
|
||||||
} h-5 flex items-center justify-center`}
|
></div>
|
||||||
>
|
|
||||||
<div
|
|
||||||
ref={currentTimeRef}
|
|
||||||
className="text-white text-xs z-10"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<div className="absolute h-1 w-full bg-destructive top-1/2 transform -translate-y-1/2"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="absolute h-1 w-full bg-destructive top-1/2 transform -translate-y-1/2"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
</TooltipProvider>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,8 +9,13 @@ import React, {
|
|||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { Tooltip, TooltipContent } from "../ui/tooltip";
|
import { isDesktop } from "react-device-detect";
|
||||||
import { TooltipTrigger } from "@radix-ui/react-tooltip";
|
import {
|
||||||
|
HoverCard,
|
||||||
|
HoverCardContent,
|
||||||
|
HoverCardTrigger,
|
||||||
|
} from "../ui/hover-card";
|
||||||
|
import { HoverCardPortal } from "@radix-ui/react-hover-card";
|
||||||
|
|
||||||
type EventSegmentProps = {
|
type EventSegmentProps = {
|
||||||
events: ReviewSegment[];
|
events: ReviewSegment[];
|
||||||
@ -33,8 +38,6 @@ type MinimapSegmentProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type TickSegmentProps = {
|
type TickSegmentProps = {
|
||||||
isFirstSegmentInMinimap: boolean;
|
|
||||||
isLastSegmentInMinimap: boolean;
|
|
||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
timestampSpread: number;
|
timestampSpread: number;
|
||||||
};
|
};
|
||||||
@ -58,25 +61,23 @@ function MinimapBounds({
|
|||||||
<>
|
<>
|
||||||
{isFirstSegmentInMinimap && (
|
{isFirstSegmentInMinimap && (
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 -bottom-5 w-full flex items-center justify-center text-xs text-primary font-medium z-20 text-center text-[8px] scroll-mt-8"
|
className="absolute inset-0 -bottom-7 w-full flex items-center justify-center text-primary font-medium z-20 text-center text-[10px] scroll-mt-8"
|
||||||
ref={firstMinimapSegmentRef}
|
ref={firstMinimapSegmentRef}
|
||||||
>
|
>
|
||||||
{new Date(alignedMinimapStartTime * 1000).toLocaleTimeString([], {
|
{new Date(alignedMinimapStartTime * 1000).toLocaleTimeString([], {
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
month: "short",
|
...(isDesktop && { month: "short", day: "2-digit" }),
|
||||||
day: "2-digit",
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isLastSegmentInMinimap && (
|
{isLastSegmentInMinimap && (
|
||||||
<div className="absolute inset-0 -top-1 w-full flex items-center justify-center text-xs text-primary font-medium z-20 text-center text-[8px]">
|
<div className="absolute inset-0 -top-3 w-full flex items-center justify-center text-primary font-medium z-20 text-center text-[10px]">
|
||||||
{new Date(alignedMinimapEndTime * 1000).toLocaleTimeString([], {
|
{new Date(alignedMinimapEndTime * 1000).toLocaleTimeString([], {
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
month: "short",
|
...(isDesktop && { month: "short", day: "2-digit" }),
|
||||||
day: "2-digit",
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -84,15 +85,10 @@ function MinimapBounds({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Tick({
|
function Tick({ timestamp, timestampSpread }: TickSegmentProps) {
|
||||||
isFirstSegmentInMinimap,
|
|
||||||
isLastSegmentInMinimap,
|
|
||||||
timestamp,
|
|
||||||
timestampSpread,
|
|
||||||
}: TickSegmentProps) {
|
|
||||||
return (
|
return (
|
||||||
<div className="w-[12px] h-2 flex justify-left items-end">
|
<div className="absolute">
|
||||||
{!isFirstSegmentInMinimap && !isLastSegmentInMinimap && (
|
<div className="flex items-end content-end w-[12px] h-2">
|
||||||
<div
|
<div
|
||||||
className={`h-0.5 ${
|
className={`h-0.5 ${
|
||||||
timestamp.getMinutes() % timestampSpread === 0 &&
|
timestamp.getMinutes() % timestampSpread === 0 &&
|
||||||
@ -101,7 +97,7 @@ function Tick({
|
|||||||
: "w-[8px] bg-gray-600"
|
: "w-[8px] bg-gray-600"
|
||||||
}`}
|
}`}
|
||||||
></div>
|
></div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -114,7 +110,7 @@ function Timestamp({
|
|||||||
segmentKey,
|
segmentKey,
|
||||||
}: TimestampSegmentProps) {
|
}: TimestampSegmentProps) {
|
||||||
return (
|
return (
|
||||||
<div className="w-[36px] pl-[3px] leading-[9px] h-2 flex justify-left items-top z-10">
|
<div className="absolute left-[15px] top-[1px] h-2 z-10">
|
||||||
{!isFirstSegmentInMinimap && !isLastSegmentInMinimap && (
|
{!isFirstSegmentInMinimap && !isLastSegmentInMinimap && (
|
||||||
<div
|
<div
|
||||||
key={`${segmentKey}_timestamp`}
|
key={`${segmentKey}_timestamp`}
|
||||||
@ -152,7 +148,10 @@ export function EventSegment({
|
|||||||
getEventThumbnail,
|
getEventThumbnail,
|
||||||
} = useSegmentUtils(segmentDuration, events, severityType);
|
} = useSegmentUtils(segmentDuration, events, severityType);
|
||||||
|
|
||||||
const { alignDateToTimeline } = useEventUtils(events, segmentDuration);
|
const { alignStartDateToTimeline, alignEndDateToTimeline } = useEventUtils(
|
||||||
|
events,
|
||||||
|
segmentDuration
|
||||||
|
);
|
||||||
|
|
||||||
const severity = useMemo(
|
const severity = useMemo(
|
||||||
() => getSeverity(segmentTime, displaySeverityType),
|
() => getSeverity(segmentTime, displaySeverityType),
|
||||||
@ -177,7 +176,7 @@ export function EventSegment({
|
|||||||
const startTimestamp = useMemo(() => {
|
const startTimestamp = useMemo(() => {
|
||||||
const eventStart = getEventStart(segmentTime);
|
const eventStart = getEventStart(segmentTime);
|
||||||
if (eventStart) {
|
if (eventStart) {
|
||||||
return alignDateToTimeline(eventStart);
|
return alignStartDateToTimeline(eventStart);
|
||||||
}
|
}
|
||||||
}, [getEventStart, segmentTime]);
|
}, [getEventStart, segmentTime]);
|
||||||
|
|
||||||
@ -191,23 +190,26 @@ export function EventSegment({
|
|||||||
const segmentKey = useMemo(() => segmentTime, [segmentTime]);
|
const segmentKey = useMemo(() => segmentTime, [segmentTime]);
|
||||||
|
|
||||||
const alignedMinimapStartTime = useMemo(
|
const alignedMinimapStartTime = useMemo(
|
||||||
() => alignDateToTimeline(minimapStartTime ?? 0),
|
() => alignStartDateToTimeline(minimapStartTime ?? 0),
|
||||||
[minimapStartTime, alignDateToTimeline]
|
[minimapStartTime, alignStartDateToTimeline]
|
||||||
);
|
);
|
||||||
const alignedMinimapEndTime = useMemo(
|
const alignedMinimapEndTime = useMemo(
|
||||||
() => alignDateToTimeline(minimapEndTime ?? 0),
|
() => alignEndDateToTimeline(minimapEndTime ?? 0),
|
||||||
[minimapEndTime, alignDateToTimeline]
|
[minimapEndTime, alignEndDateToTimeline]
|
||||||
);
|
);
|
||||||
|
|
||||||
const isInMinimapRange = useMemo(() => {
|
const isInMinimapRange = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
showMinimap &&
|
showMinimap &&
|
||||||
minimapStartTime &&
|
segmentTime >= alignedMinimapStartTime &&
|
||||||
minimapEndTime &&
|
segmentTime < alignedMinimapEndTime
|
||||||
segmentTime > minimapStartTime &&
|
|
||||||
segmentTime < minimapEndTime
|
|
||||||
);
|
);
|
||||||
}, [showMinimap, minimapStartTime, minimapEndTime, segmentTime]);
|
}, [
|
||||||
|
showMinimap,
|
||||||
|
alignedMinimapStartTime,
|
||||||
|
alignedMinimapEndTime,
|
||||||
|
segmentTime,
|
||||||
|
]);
|
||||||
|
|
||||||
const isFirstSegmentInMinimap = useMemo(() => {
|
const isFirstSegmentInMinimap = useMemo(() => {
|
||||||
return showMinimap && segmentTime === alignedMinimapStartTime;
|
return showMinimap && segmentTime === alignedMinimapStartTime;
|
||||||
@ -236,11 +238,17 @@ export function EventSegment({
|
|||||||
}
|
}
|
||||||
}, [showMinimap, isFirstSegmentInMinimap, events, segmentDuration]);
|
}, [showMinimap, isFirstSegmentInMinimap, events, segmentDuration]);
|
||||||
|
|
||||||
const segmentClasses = `flex flex-row ${
|
const segmentClasses = `h-2 relative w-[55px] md:w-[100px] ${
|
||||||
showMinimap ? (isInMinimapRange ? "bg-muted" : "bg-background") : ""
|
showMinimap
|
||||||
|
? isInMinimapRange
|
||||||
|
? "bg-card"
|
||||||
|
: isLastSegmentInMinimap
|
||||||
|
? ""
|
||||||
|
: "opacity-70"
|
||||||
|
: ""
|
||||||
} ${
|
} ${
|
||||||
isFirstSegmentInMinimap || isLastSegmentInMinimap
|
isFirstSegmentInMinimap || isLastSegmentInMinimap
|
||||||
? "relative h-2 border-b border-gray-500"
|
? "relative h-2 border-b-2 border-gray-500"
|
||||||
: ""
|
: ""
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
@ -280,7 +288,11 @@ export function EventSegment({
|
|||||||
}, [startTimestamp]);
|
}, [startTimestamp]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={segmentKey} className={segmentClasses}>
|
<div
|
||||||
|
key={segmentKey}
|
||||||
|
className={segmentClasses}
|
||||||
|
data-segment-time={new Date(segmentTime * 1000)}
|
||||||
|
>
|
||||||
<MinimapBounds
|
<MinimapBounds
|
||||||
isFirstSegmentInMinimap={isFirstSegmentInMinimap}
|
isFirstSegmentInMinimap={isFirstSegmentInMinimap}
|
||||||
isLastSegmentInMinimap={isLastSegmentInMinimap}
|
isLastSegmentInMinimap={isLastSegmentInMinimap}
|
||||||
@ -289,12 +301,7 @@ export function EventSegment({
|
|||||||
firstMinimapSegmentRef={firstMinimapSegmentRef}
|
firstMinimapSegmentRef={firstMinimapSegmentRef}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Tick
|
<Tick timestamp={timestamp} timestampSpread={timestampSpread} />
|
||||||
isFirstSegmentInMinimap={isFirstSegmentInMinimap}
|
|
||||||
isLastSegmentInMinimap={isLastSegmentInMinimap}
|
|
||||||
timestamp={timestamp}
|
|
||||||
timestampSpread={timestampSpread}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Timestamp
|
<Timestamp
|
||||||
isFirstSegmentInMinimap={isFirstSegmentInMinimap}
|
isFirstSegmentInMinimap={isFirstSegmentInMinimap}
|
||||||
@ -307,35 +314,35 @@ export function EventSegment({
|
|||||||
{severity.map((severityValue, index) => (
|
{severity.map((severityValue, index) => (
|
||||||
<React.Fragment key={index}>
|
<React.Fragment key={index}>
|
||||||
{severityValue === displaySeverityType && (
|
{severityValue === displaySeverityType && (
|
||||||
<Tooltip delayDuration={300}>
|
<HoverCard openDelay={200} closeDelay={100}>
|
||||||
<div
|
<div
|
||||||
className="mr-3 w-[8px] h-2 flex justify-left items-end"
|
className="absolute left-1/2 transform -translate-x-1/2 w-[8px] h-2 ml-[2px] z-10 cursor-pointer"
|
||||||
data-severity={severityValue}
|
data-severity={severityValue}
|
||||||
>
|
>
|
||||||
<TooltipTrigger asChild>
|
<HoverCardTrigger asChild>
|
||||||
<div
|
<div
|
||||||
key={`${segmentKey}_${index}_primary_data`}
|
key={`${segmentKey}_${index}_primary_data`}
|
||||||
className={`
|
className={`w-full h-2 bg-gradient-to-r ${roundBottomPrimary ? "rounded-bl-full rounded-br-full" : ""} ${roundTopPrimary ? "rounded-tl-full rounded-tr-full" : ""} ${severityColors[severityValue]}`}
|
||||||
w-full h-2 bg-gradient-to-r
|
|
||||||
${roundBottomPrimary ? "rounded-bl-full rounded-br-full" : ""}
|
|
||||||
${roundTopPrimary ? "rounded-tl-full rounded-tr-full" : ""}
|
|
||||||
${severityColors[severityValue]}
|
|
||||||
`}
|
|
||||||
onClick={segmentClick}
|
onClick={segmentClick}
|
||||||
></div>
|
></div>
|
||||||
</TooltipTrigger>
|
</HoverCardTrigger>
|
||||||
<TooltipContent className="rounded-2xl" side="left">
|
<HoverCardPortal>
|
||||||
<img
|
<HoverCardContent
|
||||||
className="rounded-lg"
|
className="rounded-2xl w-[250px] p-2"
|
||||||
src={`${apiHost}${eventThumbnail.replace("/media/frigate/", "")}`}
|
side="left"
|
||||||
/>
|
>
|
||||||
</TooltipContent>
|
<img
|
||||||
|
className="rounded-lg"
|
||||||
|
src={`${apiHost}${eventThumbnail.replace("/media/frigate/", "")}`}
|
||||||
|
/>
|
||||||
|
</HoverCardContent>
|
||||||
|
</HoverCardPortal>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</HoverCard>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{severityValue !== displaySeverityType && (
|
{severityValue !== displaySeverityType && (
|
||||||
<div className="h-2 flex flex-grow justify-end items-end">
|
<div className="absolute right-0 h-2 z-10">
|
||||||
<div
|
<div
|
||||||
key={`${segmentKey}_${index}_secondary_data`}
|
key={`${segmentKey}_${index}_secondary_data`}
|
||||||
className={`
|
className={`
|
||||||
|
27
web/src/components/ui/hover-card.tsx
Normal file
27
web/src/components/ui/hover-card.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const HoverCard = HoverCardPrimitive.Root
|
||||||
|
|
||||||
|
const HoverCardTrigger = HoverCardPrimitive.Trigger
|
||||||
|
|
||||||
|
const HoverCardContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof HoverCardPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
|
||||||
|
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||||
|
<HoverCardPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
align={align}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
|
||||||
|
|
||||||
|
export { HoverCard, HoverCardTrigger, HoverCardContent }
|
@ -42,7 +42,7 @@ export const useEventUtils = (
|
|||||||
[segmentDuration]
|
[segmentDuration]
|
||||||
);
|
);
|
||||||
|
|
||||||
const alignDateToTimeline = useCallback(
|
const alignEndDateToTimeline = useCallback(
|
||||||
(time: number): number => {
|
(time: number): number => {
|
||||||
const remainder = time % segmentDuration;
|
const remainder = time % segmentDuration;
|
||||||
const adjustment = remainder !== 0 ? segmentDuration - remainder : 0;
|
const adjustment = remainder !== 0 ? segmentDuration - remainder : 0;
|
||||||
@ -51,11 +51,21 @@ export const useEventUtils = (
|
|||||||
[segmentDuration]
|
[segmentDuration]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const alignStartDateToTimeline = useCallback(
|
||||||
|
(time: number): number => {
|
||||||
|
const remainder = time % segmentDuration;
|
||||||
|
const adjustment = remainder === 0 ? 0 : -(remainder);
|
||||||
|
return time + adjustment;
|
||||||
|
},
|
||||||
|
[segmentDuration]
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isStartOfEvent,
|
isStartOfEvent,
|
||||||
isEndOfEvent,
|
isEndOfEvent,
|
||||||
getSegmentStart,
|
getSegmentStart,
|
||||||
getSegmentEnd,
|
getSegmentEnd,
|
||||||
alignDateToTimeline,
|
alignEndDateToTimeline,
|
||||||
|
alignStartDateToTimeline,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -4,7 +4,8 @@ interface DragHandlerProps {
|
|||||||
contentRef: React.RefObject<HTMLElement>;
|
contentRef: React.RefObject<HTMLElement>;
|
||||||
timelineRef: React.RefObject<HTMLDivElement>;
|
timelineRef: React.RefObject<HTMLDivElement>;
|
||||||
scrollTimeRef: React.RefObject<HTMLDivElement>;
|
scrollTimeRef: React.RefObject<HTMLDivElement>;
|
||||||
alignDateToTimeline: (time: number) => number;
|
alignStartDateToTimeline: (time: number) => number;
|
||||||
|
alignEndDateToTimeline: (time: number) => number;
|
||||||
segmentDuration: number;
|
segmentDuration: number;
|
||||||
showHandlebar: boolean;
|
showHandlebar: boolean;
|
||||||
timelineDuration: number;
|
timelineDuration: number;
|
||||||
@ -20,7 +21,7 @@ function useDraggableHandler({
|
|||||||
contentRef,
|
contentRef,
|
||||||
timelineRef,
|
timelineRef,
|
||||||
scrollTimeRef,
|
scrollTimeRef,
|
||||||
alignDateToTimeline,
|
alignStartDateToTimeline,
|
||||||
segmentDuration,
|
segmentDuration,
|
||||||
showHandlebar,
|
showHandlebar,
|
||||||
timelineDuration,
|
timelineDuration,
|
||||||
@ -94,7 +95,7 @@ function useDraggableHandler({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const segmentIndex = Math.floor(newHandlePosition / segmentHeight);
|
const segmentIndex = Math.floor(newHandlePosition / segmentHeight);
|
||||||
const segmentStartTime = alignDateToTimeline(
|
const segmentStartTime = alignStartDateToTimeline(
|
||||||
timelineStart - segmentIndex * segmentDuration
|
timelineStart - segmentIndex * segmentDuration
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -82,7 +82,7 @@ export default function EventView({
|
|||||||
};
|
};
|
||||||
}, [reviewPages]);
|
}, [reviewPages]);
|
||||||
|
|
||||||
const { alignDateToTimeline } = useEventUtils(
|
const { alignStartDateToTimeline } = useEventUtils(
|
||||||
reviewItems.all,
|
reviewItems.all,
|
||||||
segmentDuration
|
segmentDuration
|
||||||
);
|
);
|
||||||
@ -270,7 +270,8 @@ export default function EventView({
|
|||||||
ref={lastRow ? lastReviewRef : minimapRef}
|
ref={lastRow ? lastReviewRef : minimapRef}
|
||||||
data-start={value.start_time}
|
data-start={value.start_time}
|
||||||
data-segment-start={
|
data-segment-start={
|
||||||
alignDateToTimeline(value.start_time) - segmentDuration
|
alignStartDateToTimeline(value.start_time) -
|
||||||
|
segmentDuration
|
||||||
}
|
}
|
||||||
className="outline outline-offset-1 outline-0 rounded-lg shadow-none transition-all duration-500 my-1 md:my-0"
|
className="outline outline-offset-1 outline-0 rounded-lg shadow-none transition-all duration-500 my-1 md:my-0"
|
||||||
>
|
>
|
||||||
@ -291,7 +292,7 @@ export default function EventView({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-[44px] md:w-[100px] mt-2 overflow-y-auto no-scrollbar">
|
<div className="w-[55px] md:w-[100px] mt-2 overflow-y-auto no-scrollbar">
|
||||||
<EventReviewTimeline
|
<EventReviewTimeline
|
||||||
segmentDuration={segmentDuration}
|
segmentDuration={segmentDuration}
|
||||||
timestampSpread={15}
|
timestampSpread={15}
|
||||||
|
Loading…
Reference in New Issue
Block a user