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:
Josh Hawkins 2024-03-01 09:36:13 -06:00 committed by GitHub
parent 49530dc2e4
commit a49e1bbc64
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 86 additions and 113 deletions

View File

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

View File

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

View File

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

View File

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