forked from Github/frigate
Timeline tweaks for mobile (#10726)
* add dense prop, combine duplicate code, fix mobile bug * put segment height in hook * playground
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import useDraggableElement from "@/hooks/use-draggable-element";
|
||||
import { useTimelineUtils } from "@/hooks/use-timeline-utils";
|
||||
import { DraggableElement } from "@/types/draggable-element";
|
||||
import {
|
||||
ReactNode,
|
||||
@@ -12,85 +14,150 @@ import { isIOS, isMobile } from "react-device-detect";
|
||||
|
||||
export type ReviewTimelineProps = {
|
||||
timelineRef: RefObject<HTMLDivElement>;
|
||||
handlebarRef: RefObject<HTMLDivElement>;
|
||||
handlebarTimeRef: RefObject<HTMLDivElement>;
|
||||
handlebarMouseMove: (e: MouseEvent | TouchEvent) => void;
|
||||
handlebarMouseUp: (e: MouseEvent | TouchEvent) => void;
|
||||
handlebarMouseDown: (
|
||||
e:
|
||||
| React.MouseEvent<HTMLDivElement, MouseEvent>
|
||||
| React.TouchEvent<HTMLDivElement>,
|
||||
) => void;
|
||||
contentRef: RefObject<HTMLDivElement>;
|
||||
segmentDuration: number;
|
||||
timelineDuration: number;
|
||||
timelineStartAligned: number;
|
||||
showHandlebar: boolean;
|
||||
showExportHandles: boolean;
|
||||
exportStartRef: RefObject<HTMLDivElement>;
|
||||
exportStartTimeRef: RefObject<HTMLDivElement>;
|
||||
exportEndRef: RefObject<HTMLDivElement>;
|
||||
exportEndTimeRef: RefObject<HTMLDivElement>;
|
||||
exportStartMouseMove: (e: MouseEvent | TouchEvent) => void;
|
||||
exportStartMouseUp: (e: MouseEvent | TouchEvent) => void;
|
||||
exportStartMouseDown: (
|
||||
e:
|
||||
| React.MouseEvent<HTMLDivElement, MouseEvent>
|
||||
| React.TouchEvent<HTMLDivElement>,
|
||||
) => void;
|
||||
exportEndMouseMove: (e: MouseEvent | TouchEvent) => void;
|
||||
exportEndMouseUp: (e: MouseEvent | TouchEvent) => void;
|
||||
exportEndMouseDown: (
|
||||
e:
|
||||
| React.MouseEvent<HTMLDivElement, MouseEvent>
|
||||
| React.TouchEvent<HTMLDivElement>,
|
||||
) => void;
|
||||
isDragging: boolean;
|
||||
exportStartPosition?: number;
|
||||
exportEndPosition?: number;
|
||||
handlebarTime?: number;
|
||||
setHandlebarTime?: React.Dispatch<React.SetStateAction<number>>;
|
||||
onHandlebarDraggingChange?: (isDragging: boolean) => void;
|
||||
onlyInitialHandlebarScroll?: boolean;
|
||||
exportStartTime?: number;
|
||||
exportEndTime?: number;
|
||||
setExportStartTime?: React.Dispatch<React.SetStateAction<number>>;
|
||||
setExportEndTime?: React.Dispatch<React.SetStateAction<number>>;
|
||||
dense: boolean;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export function ReviewTimeline({
|
||||
timelineRef,
|
||||
handlebarRef,
|
||||
handlebarTimeRef,
|
||||
handlebarMouseMove,
|
||||
handlebarMouseUp,
|
||||
handlebarMouseDown,
|
||||
contentRef,
|
||||
segmentDuration,
|
||||
timelineDuration,
|
||||
timelineStartAligned,
|
||||
showHandlebar = false,
|
||||
showExportHandles = false,
|
||||
exportStartRef,
|
||||
exportStartTimeRef,
|
||||
exportEndRef,
|
||||
exportEndTimeRef,
|
||||
exportStartMouseMove,
|
||||
exportStartMouseUp,
|
||||
exportStartMouseDown,
|
||||
exportEndMouseMove,
|
||||
exportEndMouseUp,
|
||||
exportEndMouseDown,
|
||||
isDragging,
|
||||
exportStartPosition,
|
||||
exportEndPosition,
|
||||
handlebarTime,
|
||||
setHandlebarTime,
|
||||
onHandlebarDraggingChange,
|
||||
onlyInitialHandlebarScroll = false,
|
||||
exportStartTime,
|
||||
setExportStartTime,
|
||||
exportEndTime,
|
||||
setExportEndTime,
|
||||
dense,
|
||||
children,
|
||||
}: ReviewTimelineProps) {
|
||||
const [isDraggingHandlebar, setIsDraggingHandlebar] = useState(false);
|
||||
const [isDraggingExportStart, setIsDraggingExportStart] = useState(false);
|
||||
const [isDraggingExportEnd, setIsDraggingExportEnd] = useState(false);
|
||||
const [exportStartPosition, setExportStartPosition] = useState(0);
|
||||
const [exportEndPosition, setExportEndPosition] = useState(0);
|
||||
const handlebarRef = useRef<HTMLDivElement>(null);
|
||||
const handlebarTimeRef = useRef<HTMLDivElement>(null);
|
||||
const exportStartRef = useRef<HTMLDivElement>(null);
|
||||
const exportStartTimeRef = useRef<HTMLDivElement>(null);
|
||||
const exportEndRef = useRef<HTMLDivElement>(null);
|
||||
const exportEndTimeRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const isDragging = useMemo(
|
||||
() => isDraggingHandlebar || isDraggingExportStart || isDraggingExportEnd,
|
||||
[isDraggingHandlebar, isDraggingExportStart, isDraggingExportEnd],
|
||||
);
|
||||
const exportSectionRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const segmentHeight = useMemo(() => {
|
||||
if (timelineRef.current) {
|
||||
const { scrollHeight: timelineHeight } =
|
||||
timelineRef.current as HTMLDivElement;
|
||||
|
||||
return timelineHeight / (timelineDuration / segmentDuration);
|
||||
}
|
||||
// we know that these deps are correct
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [segmentDuration, timelineDuration, timelineRef, showExportHandles]);
|
||||
|
||||
const [draggableElementType, setDraggableElementType] =
|
||||
useState<DraggableElement>();
|
||||
|
||||
const { alignStartDateToTimeline, alignEndDateToTimeline, segmentHeight } =
|
||||
useTimelineUtils({
|
||||
segmentDuration,
|
||||
timelineDuration,
|
||||
timelineRef,
|
||||
});
|
||||
|
||||
const paddedExportStartTime = useMemo(() => {
|
||||
if (exportStartTime) {
|
||||
return alignStartDateToTimeline(exportStartTime) + segmentDuration;
|
||||
}
|
||||
}, [exportStartTime, segmentDuration, alignStartDateToTimeline]);
|
||||
|
||||
const paddedExportEndTime = useMemo(() => {
|
||||
if (exportEndTime) {
|
||||
return alignEndDateToTimeline(exportEndTime);
|
||||
}
|
||||
}, [exportEndTime, alignEndDateToTimeline]);
|
||||
|
||||
const {
|
||||
handleMouseDown: handlebarMouseDown,
|
||||
handleMouseUp: handlebarMouseUp,
|
||||
handleMouseMove: handlebarMouseMove,
|
||||
} = useDraggableElement({
|
||||
contentRef,
|
||||
timelineRef,
|
||||
draggableElementRef: handlebarRef,
|
||||
segmentDuration,
|
||||
showDraggableElement: showHandlebar,
|
||||
draggableElementTime: handlebarTime,
|
||||
setDraggableElementTime: setHandlebarTime,
|
||||
initialScrollIntoViewOnly: onlyInitialHandlebarScroll,
|
||||
timelineDuration,
|
||||
timelineStartAligned,
|
||||
isDragging: isDraggingHandlebar,
|
||||
setIsDragging: setIsDraggingHandlebar,
|
||||
draggableElementTimeRef: handlebarTimeRef,
|
||||
dense,
|
||||
});
|
||||
|
||||
const {
|
||||
handleMouseDown: exportStartMouseDown,
|
||||
handleMouseUp: exportStartMouseUp,
|
||||
handleMouseMove: exportStartMouseMove,
|
||||
} = useDraggableElement({
|
||||
contentRef,
|
||||
timelineRef,
|
||||
draggableElementRef: exportStartRef,
|
||||
segmentDuration,
|
||||
showDraggableElement: showExportHandles,
|
||||
draggableElementTime: exportStartTime,
|
||||
draggableElementLatestTime: paddedExportEndTime,
|
||||
setDraggableElementTime: setExportStartTime,
|
||||
alignSetTimeToSegment: true,
|
||||
timelineDuration,
|
||||
timelineStartAligned,
|
||||
isDragging: isDraggingExportStart,
|
||||
setIsDragging: setIsDraggingExportStart,
|
||||
draggableElementTimeRef: exportStartTimeRef,
|
||||
setDraggableElementPosition: setExportStartPosition,
|
||||
dense,
|
||||
});
|
||||
|
||||
const {
|
||||
handleMouseDown: exportEndMouseDown,
|
||||
handleMouseUp: exportEndMouseUp,
|
||||
handleMouseMove: exportEndMouseMove,
|
||||
} = useDraggableElement({
|
||||
contentRef,
|
||||
timelineRef,
|
||||
draggableElementRef: exportEndRef,
|
||||
segmentDuration,
|
||||
showDraggableElement: showExportHandles,
|
||||
draggableElementTime: exportEndTime,
|
||||
draggableElementEarliestTime: paddedExportStartTime,
|
||||
setDraggableElementTime: setExportEndTime,
|
||||
alignSetTimeToSegment: true,
|
||||
timelineDuration,
|
||||
timelineStartAligned,
|
||||
isDragging: isDraggingExportEnd,
|
||||
setIsDragging: setIsDraggingExportEnd,
|
||||
draggableElementTimeRef: exportEndTimeRef,
|
||||
setDraggableElementPosition: setExportEndPosition,
|
||||
dense,
|
||||
});
|
||||
|
||||
const handleHandlebar = useCallback(
|
||||
(
|
||||
e:
|
||||
@@ -177,6 +244,19 @@ export function ReviewTimeline({
|
||||
],
|
||||
);
|
||||
|
||||
const textSizeClasses = useCallback(
|
||||
(draggableElement: DraggableElement) => {
|
||||
if (isDragging && isMobile && draggableElementType === draggableElement) {
|
||||
return "text-lg";
|
||||
} else if (dense) {
|
||||
return "text-[8px] md:text-xs";
|
||||
} else {
|
||||
return "text-xs";
|
||||
}
|
||||
},
|
||||
[dense, isDragging, draggableElementType],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
exportSectionRef.current &&
|
||||
@@ -218,6 +298,12 @@ export function ReviewTimeline({
|
||||
};
|
||||
}, [handleMouseMove, handleMouseUp, isDragging]);
|
||||
|
||||
useEffect(() => {
|
||||
if (onHandlebarDraggingChange) {
|
||||
onHandlebarDraggingChange(isDraggingHandlebar);
|
||||
}
|
||||
}, [isDraggingHandlebar, onHandlebarDraggingChange]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={timelineRef}
|
||||
@@ -234,7 +320,7 @@ export function ReviewTimeline({
|
||||
</div>
|
||||
{showHandlebar && (
|
||||
<div
|
||||
className={`absolute left-0 top-0 ${isDragging && isIOS ? "" : "z-20"} w-full`}
|
||||
className={`absolute left-0 top-0 ${isDraggingHandlebar && isIOS ? "" : "z-20"} w-full`}
|
||||
role="scrollbar"
|
||||
ref={handlebarRef}
|
||||
>
|
||||
@@ -245,21 +331,25 @@ export function ReviewTimeline({
|
||||
>
|
||||
<div
|
||||
className={`relative w-full ${
|
||||
isDragging ? "cursor-grabbing" : "cursor-grab"
|
||||
isDraggingHandlebar ? "cursor-grabbing" : "cursor-grab"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`bg-destructive rounded-full mx-auto ${
|
||||
segmentDuration < 60 ? "w-12 md:w-20" : "w-12 md:w-16"
|
||||
} h-5 ${isDragging && isMobile && draggableElementType == "handlebar" ? "fixed top-[18px] left-1/2 transform -translate-x-1/2 z-20 w-[30%] h-[30px] bg-destructive/80" : "static"} flex items-center justify-center`}
|
||||
dense
|
||||
? "w-12 md:w-20"
|
||||
: segmentDuration < 60
|
||||
? "w-24"
|
||||
: "w-20"
|
||||
} h-5 ${isDraggingHandlebar && isMobile ? "fixed top-[18px] left-1/2 transform -translate-x-1/2 z-20 w-32 h-[30px] bg-destructive/80" : "static"} flex items-center justify-center`}
|
||||
>
|
||||
<div
|
||||
ref={handlebarTimeRef}
|
||||
className={`text-white pointer-events-none ${isDragging && isMobile && draggableElementType == "handlebar" ? "text-lg" : "text-[8px] md:text-xs"} z-10`}
|
||||
className={`text-white pointer-events-none ${textSizeClasses("handlebar")} z-10`}
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
className={`absolute h-[4px] w-full bg-destructive ${isDragging && isMobile && draggableElementType == "handlebar" ? "top-1" : "top-1/2 transform -translate-y-1/2"}`}
|
||||
className={`absolute h-[4px] w-full bg-destructive ${isDraggingHandlebar && isMobile ? "top-1" : "top-1/2 transform -translate-y-1/2"}`}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -268,7 +358,7 @@ export function ReviewTimeline({
|
||||
{showExportHandles && (
|
||||
<>
|
||||
<div
|
||||
className={`absolute left-0 top-0 ${isDragging && isIOS ? "" : "z-20"} w-full`}
|
||||
className={`export-end absolute left-0 top-0 ${isDraggingExportEnd && isIOS ? "" : "z-20"} w-full`}
|
||||
role="scrollbar"
|
||||
ref={exportEndRef}
|
||||
>
|
||||
@@ -279,21 +369,25 @@ export function ReviewTimeline({
|
||||
>
|
||||
<div
|
||||
className={`relative mt-[6.5px] w-full ${
|
||||
isDragging ? "cursor-grabbing" : "cursor-grab"
|
||||
isDraggingExportEnd ? "cursor-grabbing" : "cursor-grab"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`bg-selected -mt-4 mx-auto ${
|
||||
segmentDuration < 60 ? "w-12 md:w-20" : "w-12 md:w-16"
|
||||
} h-5 ${isDragging && isMobile && draggableElementType == "export_end" ? "fixed mt-0 rounded-full top-[18px] left-1/2 transform -translate-x-1/2 z-20 w-[30%] h-[30px] bg-selected/80" : "rounded-tr-lg rounded-tl-lg static"} flex items-center justify-center`}
|
||||
dense
|
||||
? "w-12 md:w-20"
|
||||
: segmentDuration < 60
|
||||
? "w-24"
|
||||
: "w-20"
|
||||
} h-5 ${isDraggingExportEnd && isMobile ? "fixed mt-0 rounded-full top-[18px] left-1/2 transform -translate-x-1/2 z-20 w-32 h-[30px] bg-selected/80" : "rounded-tr-lg rounded-tl-lg static"} flex items-center justify-center`}
|
||||
>
|
||||
<div
|
||||
ref={exportEndTimeRef}
|
||||
className={`text-white pointer-events-none ${isDragging && isMobile && draggableElementType == "export_end" ? "text-lg mt-0" : "text-[8px] md:text-xs"} z-10`}
|
||||
className={`text-white pointer-events-none ${isDraggingExportEnd && isMobile ? "mt-0" : ""} ${textSizeClasses("export_end")} z-10`}
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
className={`absolute h-[4px] w-full bg-selected ${isDragging && isMobile && draggableElementType == "export_end" ? "top-0" : "top-1/2 transform -translate-y-1/2"}`}
|
||||
className={`absolute h-[4px] w-full bg-selected ${isDraggingExportEnd && isMobile ? "top-0" : "top-1/2 transform -translate-y-1/2"}`}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -303,7 +397,7 @@ export function ReviewTimeline({
|
||||
className="bg-selected/50 absolute w-full"
|
||||
></div>
|
||||
<div
|
||||
className={`absolute left-0 top-0 ${isDragging && isIOS ? "" : "z-20"} w-full`}
|
||||
className={`export-start absolute left-0 top-0 ${isDraggingExportStart && isIOS ? "" : "z-20"} w-full`}
|
||||
role="scrollbar"
|
||||
ref={exportStartRef}
|
||||
>
|
||||
@@ -318,16 +412,20 @@ export function ReviewTimeline({
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`absolute h-[4px] w-full bg-selected ${isDragging && isMobile && draggableElementType == "export_start" ? "top-[12px]" : "top-1/2 transform -translate-y-1/2"}`}
|
||||
className={`absolute h-[4px] w-full bg-selected ${isDraggingExportStart && isMobile ? "top-[12px]" : "top-1/2 transform -translate-y-1/2"}`}
|
||||
></div>
|
||||
<div
|
||||
className={`bg-selected mt-4 mx-auto ${
|
||||
segmentDuration < 60 ? "w-12 md:w-20" : "w-12 md:w-16"
|
||||
} h-5 ${isDragging && isMobile && draggableElementType == "export_start" ? "fixed mt-0 rounded-full top-[4px] left-1/2 transform -translate-x-1/2 z-20 w-[30%] h-[30px] bg-selected/80" : "rounded-br-lg rounded-bl-lg static"} flex items-center justify-center`}
|
||||
dense
|
||||
? "w-12 md:w-20"
|
||||
: segmentDuration < 60
|
||||
? "w-24"
|
||||
: "w-20"
|
||||
} h-5 ${isDraggingExportStart && isMobile ? "fixed mt-0 rounded-full top-[4px] left-1/2 transform -translate-x-1/2 z-20 w-32 h-[30px] bg-selected/80" : "rounded-br-lg rounded-bl-lg static"} flex items-center justify-center`}
|
||||
>
|
||||
<div
|
||||
ref={exportStartTimeRef}
|
||||
className={`text-white pointer-events-none ${isDragging && isMobile && draggableElementType == "export_start" ? "text-lg mt-0" : "text-[8px] md:text-xs"} z-10`}
|
||||
className={`text-white pointer-events-none ${isDraggingExportStart && isMobile ? "mt-0" : ""} ${textSizeClasses("export_start")} z-10`}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user