mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
Timeline handlebar changes (#10170)
* auto scrolling handlebar with preview time * tablets can show 2 columns on the event view grid * font sizes * hide minimap when previewing
This commit is contained in:
parent
49530dc2e4
commit
a49e1bbc64
@ -33,7 +33,7 @@ import { useSwipeable } from "react-swipeable";
|
|||||||
type PreviewPlayerProps = {
|
type PreviewPlayerProps = {
|
||||||
review: ReviewSegment;
|
review: ReviewSegment;
|
||||||
allPreviews?: Preview[];
|
allPreviews?: Preview[];
|
||||||
onTimeUpdate?: (time: number | undefined) => void;
|
onTimeUpdate?: React.Dispatch<React.SetStateAction<number | undefined>>;
|
||||||
setReviewed: (reviewId: string) => void;
|
setReviewed: (reviewId: string) => void;
|
||||||
markAboveReviewed: () => void;
|
markAboveReviewed: () => void;
|
||||||
onClick: (reviewId: string, ctrl: boolean) => void;
|
onClick: (reviewId: string, ctrl: boolean) => void;
|
||||||
|
@ -45,10 +45,9 @@ export function EventReviewTimeline({
|
|||||||
onHandlebarDraggingChange,
|
onHandlebarDraggingChange,
|
||||||
}: EventReviewTimelineProps) {
|
}: EventReviewTimelineProps) {
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const [currentTimeSegment, setCurrentTimeSegment] = useState<number>(0);
|
|
||||||
const scrollTimeRef = useRef<HTMLDivElement>(null);
|
const scrollTimeRef = useRef<HTMLDivElement>(null);
|
||||||
const timelineRef = useRef<HTMLDivElement>(null);
|
const timelineRef = useRef<HTMLDivElement>(null);
|
||||||
const currentTimeRef = useRef<HTMLDivElement>(null);
|
const handlebarTimeRef = useRef<HTMLDivElement>(null);
|
||||||
const observer = useRef<ResizeObserver | null>(null);
|
const observer = useRef<ResizeObserver | null>(null);
|
||||||
const timelineDuration = useMemo(
|
const timelineDuration = useMemo(
|
||||||
() => timelineStart - timelineEnd,
|
() => timelineStart - timelineEnd,
|
||||||
@ -69,12 +68,13 @@ export function EventReviewTimeline({
|
|||||||
alignEndDateToTimeline,
|
alignEndDateToTimeline,
|
||||||
segmentDuration,
|
segmentDuration,
|
||||||
showHandlebar,
|
showHandlebar,
|
||||||
|
handlebarTime,
|
||||||
|
setHandlebarTime,
|
||||||
timelineDuration,
|
timelineDuration,
|
||||||
timelineStart,
|
timelineStart,
|
||||||
isDragging,
|
isDragging,
|
||||||
setIsDragging,
|
setIsDragging,
|
||||||
currentTimeRef,
|
handlebarTimeRef,
|
||||||
setHandlebarTime,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleResize() {
|
function handleResize() {
|
||||||
@ -151,66 +151,15 @@ export function EventReviewTimeline({
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (showHandlebar) {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
if (currentTimeRef.current && currentTimeSegment) {
|
|
||||||
currentTimeRef.current.textContent = new Date(
|
|
||||||
currentTimeSegment * 1000,
|
|
||||||
).toLocaleTimeString([], {
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
...(segmentDuration < 60 && { second: "2-digit" }),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// we know that these deps are correct
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [currentTimeSegment, showHandlebar]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (onHandlebarDraggingChange) {
|
if (onHandlebarDraggingChange) {
|
||||||
onHandlebarDraggingChange(isDragging);
|
onHandlebarDraggingChange(isDragging);
|
||||||
}
|
}
|
||||||
}, [isDragging, onHandlebarDraggingChange]);
|
}, [isDragging, onHandlebarDraggingChange]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (timelineRef.current && handlebarTime && showHandlebar) {
|
|
||||||
const { scrollHeight: timelineHeight } = timelineRef.current;
|
|
||||||
|
|
||||||
// Calculate the height of an individual segment
|
|
||||||
const segmentHeight =
|
|
||||||
timelineHeight / (timelineDuration / segmentDuration);
|
|
||||||
|
|
||||||
// Calculate the segment index corresponding to the target time
|
|
||||||
const alignedHandlebarTime = alignStartDateToTimeline(handlebarTime);
|
|
||||||
const segmentIndex = Math.ceil(
|
|
||||||
(timelineStart - alignedHandlebarTime) / segmentDuration,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Calculate the top position based on the segment index
|
|
||||||
const newTopPosition = Math.max(0, segmentIndex * segmentHeight);
|
|
||||||
|
|
||||||
// Set the top position of the handle
|
|
||||||
const thumb = scrollTimeRef.current;
|
|
||||||
if (thumb) {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
thumb.style.top = `${newTopPosition}px`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setCurrentTimeSegment(alignedHandlebarTime);
|
|
||||||
}
|
|
||||||
// should only be run once
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
generateSegments();
|
generateSegments();
|
||||||
if (!currentTimeSegment && !handlebarTime) {
|
|
||||||
setCurrentTimeSegment(timelineStart);
|
|
||||||
}
|
|
||||||
// TODO: touch events for mobile
|
// TODO: touch events for mobile
|
||||||
document.addEventListener("mousemove", handleMouseMove);
|
document.addEventListener("mousemove", handleMouseMove);
|
||||||
document.addEventListener("mouseup", handleMouseUp);
|
document.addEventListener("mouseup", handleMouseUp);
|
||||||
@ -220,13 +169,7 @@ export function EventReviewTimeline({
|
|||||||
};
|
};
|
||||||
// we know that these deps are correct
|
// we know that these deps are correct
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [
|
}, [generateSegments, timelineStart, handleMouseUp, handleMouseMove]);
|
||||||
currentTimeSegment,
|
|
||||||
generateSegments,
|
|
||||||
timelineStart,
|
|
||||||
handleMouseUp,
|
|
||||||
handleMouseMove,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -248,12 +191,12 @@ export function EventReviewTimeline({
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`bg-destructive rounded-full mx-auto ${
|
className={`bg-destructive rounded-full mx-auto ${
|
||||||
segmentDuration < 60 ? "w-20" : "w-16"
|
segmentDuration < 60 ? "w-14 md:w-20" : "w-12 md:w-16"
|
||||||
} h-5 flex items-center justify-center`}
|
} h-5 flex items-center justify-center`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref={currentTimeRef}
|
ref={handlebarTimeRef}
|
||||||
className="text-white text-xs z-10"
|
className="text-white text-[8px] md:text-xs z-10"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute h-1 w-full bg-destructive top-1/2 transform -translate-y-1/2"></div>
|
<div className="absolute h-1 w-full bg-destructive top-1/2 transform -translate-y-1/2"></div>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useEffect } from "react";
|
import { useCallback, useEffect } from "react";
|
||||||
|
|
||||||
interface DragHandlerProps {
|
type 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>;
|
||||||
@ -8,15 +8,15 @@ interface DragHandlerProps {
|
|||||||
alignEndDateToTimeline: (time: number) => number;
|
alignEndDateToTimeline: (time: number) => number;
|
||||||
segmentDuration: number;
|
segmentDuration: number;
|
||||||
showHandlebar: boolean;
|
showHandlebar: boolean;
|
||||||
|
handlebarTime?: number;
|
||||||
|
setHandlebarTime?: React.Dispatch<React.SetStateAction<number>>;
|
||||||
|
handlebarTimeRef: React.MutableRefObject<HTMLDivElement | null>;
|
||||||
timelineDuration: number;
|
timelineDuration: number;
|
||||||
timelineStart: number;
|
timelineStart: number;
|
||||||
isDragging: boolean;
|
isDragging: boolean;
|
||||||
setIsDragging: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsDragging: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
currentTimeRef: React.MutableRefObject<HTMLDivElement | null>;
|
};
|
||||||
setHandlebarTime?: React.Dispatch<React.SetStateAction<number>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: handle mobile touch events
|
|
||||||
function useDraggableHandler({
|
function useDraggableHandler({
|
||||||
contentRef,
|
contentRef,
|
||||||
timelineRef,
|
timelineRef,
|
||||||
@ -24,12 +24,13 @@ function useDraggableHandler({
|
|||||||
alignStartDateToTimeline,
|
alignStartDateToTimeline,
|
||||||
segmentDuration,
|
segmentDuration,
|
||||||
showHandlebar,
|
showHandlebar,
|
||||||
|
handlebarTime,
|
||||||
|
setHandlebarTime,
|
||||||
|
handlebarTimeRef,
|
||||||
timelineDuration,
|
timelineDuration,
|
||||||
timelineStart,
|
timelineStart,
|
||||||
isDragging,
|
isDragging,
|
||||||
setIsDragging,
|
setIsDragging,
|
||||||
currentTimeRef,
|
|
||||||
setHandlebarTime,
|
|
||||||
}: DragHandlerProps) {
|
}: DragHandlerProps) {
|
||||||
const handleMouseDown = useCallback(
|
const handleMouseDown = useCallback(
|
||||||
(e: React.MouseEvent<HTMLDivElement>) => {
|
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
@ -51,6 +52,39 @@ function useDraggableHandler({
|
|||||||
[isDragging, setIsDragging],
|
[isDragging, setIsDragging],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const getCumulativeScrollTop = useCallback((element: HTMLElement | null) => {
|
||||||
|
let scrollTop = 0;
|
||||||
|
while (element) {
|
||||||
|
scrollTop += element.scrollTop;
|
||||||
|
element = element.parentElement;
|
||||||
|
}
|
||||||
|
return scrollTop;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updateHandlebarPosition = useCallback(
|
||||||
|
(newHandlePosition: number, segmentStartTime: number) => {
|
||||||
|
const thumb = scrollTimeRef.current;
|
||||||
|
if (thumb) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
thumb.style.top = `${newHandlePosition}px`;
|
||||||
|
if (handlebarTimeRef.current) {
|
||||||
|
handlebarTimeRef.current.textContent = new Date(
|
||||||
|
segmentStartTime * 1000,
|
||||||
|
).toLocaleTimeString([], {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
...(segmentDuration < 60 && { second: "2-digit" }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (setHandlebarTime) {
|
||||||
|
setHandlebarTime(segmentStartTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[segmentDuration, handlebarTimeRef, scrollTimeRef, setHandlebarTime],
|
||||||
|
);
|
||||||
|
|
||||||
const handleMouseMove = useCallback(
|
const handleMouseMove = useCallback(
|
||||||
(e: MouseEvent) => {
|
(e: MouseEvent) => {
|
||||||
if (
|
if (
|
||||||
@ -64,7 +98,7 @@ function useDraggableHandler({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
if (isDragging) {
|
if (showHandlebar && isDragging) {
|
||||||
const {
|
const {
|
||||||
scrollHeight: timelineHeight,
|
scrollHeight: timelineHeight,
|
||||||
clientHeight: visibleTimelineHeight,
|
clientHeight: visibleTimelineHeight,
|
||||||
@ -75,15 +109,6 @@ function useDraggableHandler({
|
|||||||
const segmentHeight =
|
const segmentHeight =
|
||||||
timelineHeight / (timelineDuration / segmentDuration);
|
timelineHeight / (timelineDuration / segmentDuration);
|
||||||
|
|
||||||
const getCumulativeScrollTop = (element: HTMLElement | null) => {
|
|
||||||
let scrollTop = 0;
|
|
||||||
while (element) {
|
|
||||||
scrollTop += element.scrollTop;
|
|
||||||
element = element.parentElement;
|
|
||||||
}
|
|
||||||
return scrollTop;
|
|
||||||
};
|
|
||||||
|
|
||||||
const parentScrollTop = getCumulativeScrollTop(timelineRef.current);
|
const parentScrollTop = getCumulativeScrollTop(timelineRef.current);
|
||||||
|
|
||||||
const newHandlePosition = Math.min(
|
const newHandlePosition = Math.min(
|
||||||
@ -99,28 +124,11 @@ function useDraggableHandler({
|
|||||||
timelineStart - segmentIndex * segmentDuration,
|
timelineStart - segmentIndex * segmentDuration,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (showHandlebar) {
|
updateHandlebarPosition(
|
||||||
const thumb = scrollTimeRef.current;
|
newHandlePosition - segmentHeight,
|
||||||
requestAnimationFrame(() => {
|
segmentStartTime,
|
||||||
thumb.style.top = `${newHandlePosition - segmentHeight}px`;
|
|
||||||
if (currentTimeRef.current) {
|
|
||||||
currentTimeRef.current.textContent = new Date(
|
|
||||||
segmentStartTime * 1000,
|
|
||||||
).toLocaleTimeString([], {
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
...(segmentDuration < 60 && { second: "2-digit" }),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (setHandlebarTime) {
|
|
||||||
setHandlebarTime(
|
|
||||||
timelineStart -
|
|
||||||
(newHandlePosition / segmentHeight) * segmentDuration,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
// we know that these deps are correct
|
// we know that these deps are correct
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
@ -131,21 +139,43 @@ function useDraggableHandler({
|
|||||||
showHandlebar,
|
showHandlebar,
|
||||||
timelineDuration,
|
timelineDuration,
|
||||||
timelineStart,
|
timelineStart,
|
||||||
|
updateHandlebarPosition,
|
||||||
|
alignStartDateToTimeline,
|
||||||
|
getCumulativeScrollTop,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// TODO: determine when we want to do this
|
if (
|
||||||
const handlebar = scrollTimeRef.current;
|
timelineRef.current &&
|
||||||
if (handlebar && showHandlebar) {
|
scrollTimeRef.current &&
|
||||||
handlebar.scrollIntoView({
|
showHandlebar &&
|
||||||
|
handlebarTime &&
|
||||||
|
!isDragging
|
||||||
|
) {
|
||||||
|
const { scrollHeight: timelineHeight, scrollTop: scrolled } =
|
||||||
|
timelineRef.current;
|
||||||
|
|
||||||
|
const segmentHeight =
|
||||||
|
timelineHeight / (timelineDuration / segmentDuration);
|
||||||
|
|
||||||
|
const parentScrollTop = getCumulativeScrollTop(timelineRef.current);
|
||||||
|
|
||||||
|
const newHandlePosition =
|
||||||
|
((timelineStart - handlebarTime) / segmentDuration) * segmentHeight +
|
||||||
|
parentScrollTop -
|
||||||
|
scrolled;
|
||||||
|
|
||||||
|
updateHandlebarPosition(newHandlePosition - segmentHeight, handlebarTime);
|
||||||
|
|
||||||
|
scrollTimeRef.current.scrollIntoView({
|
||||||
behavior: "smooth",
|
behavior: "smooth",
|
||||||
block: "center",
|
block: "center",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// temporary until behavior is decided
|
// we know that these deps are correct
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, [handlebarTime, showHandlebar, scrollTimeRef, timelineStart]);
|
||||||
|
|
||||||
return { handleMouseDown, handleMouseUp, handleMouseMove };
|
return { handleMouseDown, handleMouseUp, handleMouseMove };
|
||||||
}
|
}
|
||||||
|
@ -322,7 +322,7 @@ export default function EventView({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="w-full m-2 grid md:grid-cols-3 3xl:grid-cols-4 gap-2 md:gap-4"
|
className="w-full m-2 grid sm:grid-cols-2 md:grid-cols-3 3xl:grid-cols-4 gap-2 md:gap-4"
|
||||||
ref={contentRef}
|
ref={contentRef}
|
||||||
>
|
>
|
||||||
{currentItems ? (
|
{currentItems ? (
|
||||||
@ -366,7 +366,7 @@ export default function EventView({
|
|||||||
timestampSpread={15}
|
timestampSpread={15}
|
||||||
timelineStart={timeRange.before}
|
timelineStart={timeRange.before}
|
||||||
timelineEnd={timeRange.after}
|
timelineEnd={timeRange.after}
|
||||||
showMinimap={showMinimap}
|
showMinimap={showMinimap && !previewTime}
|
||||||
minimapStartTime={minimapBounds.start}
|
minimapStartTime={minimapBounds.start}
|
||||||
minimapEndTime={minimapBounds.end}
|
minimapEndTime={minimapBounds.end}
|
||||||
showHandlebar={previewTime != undefined}
|
showHandlebar={previewTime != undefined}
|
||||||
|
Loading…
Reference in New Issue
Block a user