Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Timeline handlebar changes #10170

Merged
merged 4 commits into from
Mar 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion web/src/components/player/PreviewThumbnailPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { useSwipeable } from "react-swipeable";
type PreviewPlayerProps = {
review: ReviewSegment;
allPreviews?: Preview[];
onTimeUpdate?: (time: number | undefined) => void;
onTimeUpdate?: React.Dispatch<React.SetStateAction<number | undefined>>;
setReviewed: (reviewId: string) => void;
markAboveReviewed: () => void;
onClick: (reviewId: string, ctrl: boolean) => void;
Expand Down
75 changes: 9 additions & 66 deletions web/src/components/timeline/EventReviewTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,9 @@ export function EventReviewTimeline({
onHandlebarDraggingChange,
}: EventReviewTimelineProps) {
const [isDragging, setIsDragging] = useState(false);
const [currentTimeSegment, setCurrentTimeSegment] = useState<number>(0);
const scrollTimeRef = 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 timelineDuration = useMemo(
() => timelineStart - timelineEnd,
Expand All @@ -69,12 +68,13 @@ export function EventReviewTimeline({
alignEndDateToTimeline,
segmentDuration,
showHandlebar,
handlebarTime,
setHandlebarTime,
timelineDuration,
timelineStart,
isDragging,
setIsDragging,
currentTimeRef,
setHandlebarTime,
handlebarTimeRef,
});

function handleResize() {
Expand Down Expand Up @@ -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(() => {
if (onHandlebarDraggingChange) {
onHandlebarDraggingChange(isDragging);
}
}, [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(() => {
generateSegments();
if (!currentTimeSegment && !handlebarTime) {
setCurrentTimeSegment(timelineStart);
}

// TODO: touch events for mobile
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
Expand All @@ -220,13 +169,7 @@ export function EventReviewTimeline({
};
// we know that these deps are correct
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
currentTimeSegment,
generateSegments,
timelineStart,
handleMouseUp,
handleMouseMove,
]);
}, [generateSegments, timelineStart, handleMouseUp, handleMouseMove]);

return (
<div
Expand All @@ -248,12 +191,12 @@ export function EventReviewTimeline({
>
<div
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`}
>
<div
ref={currentTimeRef}
className="text-white text-xs z-10"
ref={handlebarTimeRef}
className="text-white text-[8px] md:text-xs z-10"
></div>
</div>
<div className="absolute h-1 w-full bg-destructive top-1/2 transform -translate-y-1/2"></div>
Expand Down
118 changes: 74 additions & 44 deletions web/src/hooks/use-handle-dragging.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,36 @@
import { useCallback, useEffect } from "react";

interface DragHandlerProps {
type DragHandlerProps = {
contentRef: React.RefObject<HTMLElement>;
timelineRef: React.RefObject<HTMLDivElement>;
scrollTimeRef: React.RefObject<HTMLDivElement>;
alignStartDateToTimeline: (time: number) => number;
alignEndDateToTimeline: (time: number) => number;
segmentDuration: number;
showHandlebar: boolean;
handlebarTime?: number;
setHandlebarTime?: React.Dispatch<React.SetStateAction<number>>;
handlebarTimeRef: React.MutableRefObject<HTMLDivElement | null>;
timelineDuration: number;
timelineStart: number;
isDragging: 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({
contentRef,
timelineRef,
scrollTimeRef,
alignStartDateToTimeline,
segmentDuration,
showHandlebar,
handlebarTime,
setHandlebarTime,
handlebarTimeRef,
timelineDuration,
timelineStart,
isDragging,
setIsDragging,
currentTimeRef,
setHandlebarTime,
}: DragHandlerProps) {
const handleMouseDown = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
Expand All @@ -51,6 +52,39 @@ function useDraggableHandler({
[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(
(e: MouseEvent) => {
if (
Expand All @@ -64,7 +98,7 @@ function useDraggableHandler({
e.preventDefault();
e.stopPropagation();

if (isDragging) {
if (showHandlebar && isDragging) {
const {
scrollHeight: timelineHeight,
clientHeight: visibleTimelineHeight,
Expand All @@ -75,15 +109,6 @@ function useDraggableHandler({
const segmentHeight =
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 newHandlePosition = Math.min(
Expand All @@ -99,27 +124,10 @@ function useDraggableHandler({
timelineStart - segmentIndex * segmentDuration,
);

if (showHandlebar) {
const thumb = scrollTimeRef.current;
requestAnimationFrame(() => {
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,
);
}
}
updateHandlebarPosition(
newHandlePosition - segmentHeight,
segmentStartTime,
);
}
},
// we know that these deps are correct
Expand All @@ -131,21 +139,43 @@ function useDraggableHandler({
showHandlebar,
timelineDuration,
timelineStart,
updateHandlebarPosition,
alignStartDateToTimeline,
getCumulativeScrollTop,
],
);

useEffect(() => {
// TODO: determine when we want to do this
const handlebar = scrollTimeRef.current;
if (handlebar && showHandlebar) {
handlebar.scrollIntoView({
if (
timelineRef.current &&
scrollTimeRef.current &&
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",
block: "center",
});
}
// temporary until behavior is decided
// we know that these deps are correct
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [handlebarTime, showHandlebar, scrollTimeRef, timelineStart]);

return { handleMouseDown, handleMouseUp, handleMouseMove };
}
Expand Down
4 changes: 2 additions & 2 deletions web/src/views/events/EventView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ export default function EventView({
)}

<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}
>
{currentItems ? (
Expand Down Expand Up @@ -366,7 +366,7 @@ export default function EventView({
timestampSpread={15}
timelineStart={timeRange.before}
timelineEnd={timeRange.after}
showMinimap={showMinimap}
showMinimap={showMinimap && !previewTime}
minimapStartTime={minimapBounds.start}
minimapEndTime={minimapBounds.end}
showHandlebar={previewTime != undefined}
Expand Down
Loading