mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
add comment for hooks
This commit is contained in:
@@ -1,13 +1,32 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* useDisableBrowserZoom
|
||||||
|
*
|
||||||
|
* This React hook prevents users from zooming in/out the browser window using keyboard shortcuts,
|
||||||
|
* mouse wheel, or gesture events (such as pinch-to-zoom on trackpads or touch devices).
|
||||||
|
*
|
||||||
|
* Typical use case: apps with fixed-size UIs, custom editors, or when zooming would break layout.
|
||||||
|
*
|
||||||
|
* NOTE: Disabling browser zoom may negatively affect accessibility and user experience.
|
||||||
|
* Use with caution, especially for public-facing applications.
|
||||||
|
*/
|
||||||
export const useDisableBrowserZoom = () => {
|
export const useDisableBrowserZoom = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
/**
|
||||||
|
* Prevents browser zoom when user holds Ctrl (or Cmd on Mac) and scrolls the mouse wheel.
|
||||||
|
*/
|
||||||
const handleWheel = (e: WheelEvent) => {
|
const handleWheel = (e: WheelEvent) => {
|
||||||
if (e.ctrlKey) {
|
if (e.ctrlKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevents browser zoom via keyboard shortcuts:
|
||||||
|
* Ctrl/Cmd + '+', '-', '=', or '0' (reset zoom)
|
||||||
|
* Some keyboards emit '=' for '+' (without Shift).
|
||||||
|
*/
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if (e.ctrlKey) {
|
if (e.ctrlKey) {
|
||||||
const key = e.key;
|
const key = e.key;
|
||||||
@@ -17,23 +36,22 @@ export const useDisableBrowserZoom = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevents pinch-zoom gestures on supported browsers (mainly Safari/macOS/iOS).
|
||||||
|
* gesturestart, gesturechange, and gestureend are non-standard and not supported everywhere.
|
||||||
|
*/
|
||||||
const handleGesture = (e: Event) => {
|
const handleGesture = (e: Event) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Attach event listeners (passive: false is required for preventDefault to work)
|
||||||
window.addEventListener("wheel", handleWheel, { passive: false });
|
window.addEventListener("wheel", handleWheel, { passive: false });
|
||||||
window.addEventListener("keydown", handleKeyDown, { passive: false });
|
window.addEventListener("keydown", handleKeyDown, { passive: false });
|
||||||
|
window.addEventListener("gesturestart", handleGesture, { passive: false });
|
||||||
|
window.addEventListener("gesturechange", handleGesture, { passive: false });
|
||||||
|
window.addEventListener("gestureend", handleGesture, { passive: false });
|
||||||
|
|
||||||
window.addEventListener("gesturestart", handleGesture, {
|
// Cleanup listeners on unmount to avoid memory leaks
|
||||||
passive: false,
|
|
||||||
});
|
|
||||||
window.addEventListener("gesturechange", handleGesture, {
|
|
||||||
passive: false,
|
|
||||||
});
|
|
||||||
window.addEventListener("gestureend", handleGesture, {
|
|
||||||
passive: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("wheel", handleWheel);
|
window.removeEventListener("wheel", handleWheel);
|
||||||
window.removeEventListener("keydown", handleKeyDown);
|
window.removeEventListener("keydown", handleKeyDown);
|
||||||
|
|||||||
@@ -11,6 +11,13 @@ interface UseTimelinePlayheadProps {
|
|||||||
rulerScrollRef: React.RefObject<HTMLDivElement>;
|
rulerScrollRef: React.RefObject<HTMLDivElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* useTimelinePlayhead
|
||||||
|
*
|
||||||
|
* Custom hook to manage playhead (scrubbing) logic for a timeline editor.
|
||||||
|
* Handles mouse interaction for timeline ruler, calculates time from mouse,
|
||||||
|
* and enables smooth scrubbing without unnecessary re-renders.
|
||||||
|
*/
|
||||||
export function useTimelinePlayhead({
|
export function useTimelinePlayhead({
|
||||||
currentTime,
|
currentTime,
|
||||||
duration,
|
duration,
|
||||||
@@ -19,17 +26,35 @@ export function useTimelinePlayhead({
|
|||||||
rulerRef,
|
rulerRef,
|
||||||
rulerScrollRef,
|
rulerScrollRef,
|
||||||
}: UseTimelinePlayheadProps) {
|
}: UseTimelinePlayheadProps) {
|
||||||
|
// Get current project info (especially FPS) from global store
|
||||||
const { activeProject } = useProjectStore();
|
const { activeProject } = useProjectStore();
|
||||||
|
|
||||||
|
// Ref to track if currently scrubbing
|
||||||
const isScrubbingRef = useRef(false);
|
const isScrubbingRef = useRef(false);
|
||||||
|
|
||||||
|
// Ref to hold the playhead time while scrubbing (does not trigger re-render)
|
||||||
const scrubTimeRef = useRef<number | null>(null);
|
const scrubTimeRef = useRef<number | null>(null);
|
||||||
|
|
||||||
|
// State to force re-render when scrubbing ends
|
||||||
const [_, forceRerender] = useState(0);
|
const [_, forceRerender] = useState(0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the actual playhead position:
|
||||||
|
* - If scrubbing, use the value in scrubTimeRef
|
||||||
|
* - Otherwise, use the currentTime (controlled by external state)
|
||||||
|
*/
|
||||||
const playheadPosition =
|
const playheadPosition =
|
||||||
isScrubbingRef.current && scrubTimeRef.current !== null
|
isScrubbingRef.current && scrubTimeRef.current !== null
|
||||||
? scrubTimeRef.current
|
? scrubTimeRef.current
|
||||||
: currentTime;
|
: currentTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate timeline time (in seconds) based on mouse X position.
|
||||||
|
* - Gets bounding rect of the ruler
|
||||||
|
* - Adjusts for scroll position if ruler is scrollable
|
||||||
|
* - Converts X pixel offset to seconds using current zoom level
|
||||||
|
* - Snaps to nearest frame using FPS
|
||||||
|
*/
|
||||||
const getTimeFromMouse = useCallback(
|
const getTimeFromMouse = useCallback(
|
||||||
(e: MouseEvent | React.MouseEvent) => {
|
(e: MouseEvent | React.MouseEvent) => {
|
||||||
const ruler = rulerRef.current;
|
const ruler = rulerRef.current;
|
||||||
@@ -43,6 +68,7 @@ export function useTimelinePlayhead({
|
|||||||
const scrollLeft = scrollArea.scrollLeft;
|
const scrollLeft = scrollArea.scrollLeft;
|
||||||
const x = e.clientX - rect.left + scrollLeft;
|
const x = e.clientX - rect.left + scrollLeft;
|
||||||
|
|
||||||
|
// Calculate how many pixels represent one second, depending on zoom
|
||||||
const pixelsPerSecond = 50 * zoomLevel;
|
const pixelsPerSecond = 50 * zoomLevel;
|
||||||
const rawTime = Math.max(0, Math.min(duration, x / pixelsPerSecond));
|
const rawTime = Math.max(0, Math.min(duration, x / pixelsPerSecond));
|
||||||
const time = snapTimeToFrame(rawTime, activeProject?.fps || 30);
|
const time = snapTimeToFrame(rawTime, activeProject?.fps || 30);
|
||||||
@@ -51,6 +77,13 @@ export function useTimelinePlayhead({
|
|||||||
[rulerRef, rulerScrollRef, duration, zoomLevel, activeProject?.fps]
|
[rulerRef, rulerScrollRef, duration, zoomLevel, activeProject?.fps]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle mouse down event on the ruler:
|
||||||
|
* - Starts scrubbing and updates playhead immediately
|
||||||
|
* - Registers mousemove/mouseup events to allow scrubbing
|
||||||
|
* - Updates time as mouse moves and seeks to new time
|
||||||
|
* - Cleans up listeners and triggers re-render when scrubbing ends
|
||||||
|
*/
|
||||||
const handleRulerMouseDown = useCallback(
|
const handleRulerMouseDown = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -61,6 +94,7 @@ export function useTimelinePlayhead({
|
|||||||
scrubTimeRef.current = time;
|
scrubTimeRef.current = time;
|
||||||
seek(time);
|
seek(time);
|
||||||
|
|
||||||
|
// Mouse move handler: update playhead and seek to new time
|
||||||
const onMouseMove = (e: MouseEvent) => {
|
const onMouseMove = (e: MouseEvent) => {
|
||||||
const t = getTimeFromMouse(e);
|
const t = getTimeFromMouse(e);
|
||||||
if (t !== scrubTimeRef.current) {
|
if (t !== scrubTimeRef.current) {
|
||||||
@@ -69,6 +103,7 @@ export function useTimelinePlayhead({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Mouse up handler: stop scrubbing, cleanup, and force re-render
|
||||||
const onMouseUp = () => {
|
const onMouseUp = () => {
|
||||||
isScrubbingRef.current = false;
|
isScrubbingRef.current = false;
|
||||||
window.removeEventListener("mousemove", onMouseMove);
|
window.removeEventListener("mousemove", onMouseMove);
|
||||||
@@ -76,6 +111,7 @@ export function useTimelinePlayhead({
|
|||||||
forceRerender((v) => v + 1);
|
forceRerender((v) => v + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Attach listeners to window for drag outside the ruler area
|
||||||
window.addEventListener("mousemove", onMouseMove);
|
window.addEventListener("mousemove", onMouseMove);
|
||||||
window.addEventListener("mouseup", onMouseUp);
|
window.addEventListener("mouseup", onMouseUp);
|
||||||
},
|
},
|
||||||
@@ -83,7 +119,7 @@ export function useTimelinePlayhead({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
playheadPosition,
|
playheadPosition, // Current playhead position (real-time while scrubbing)
|
||||||
handleRulerMouseDown,
|
handleRulerMouseDown, // Attach to the ruler's onMouseDown
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,15 +15,30 @@ interface UseTimelineZoomReturn {
|
|||||||
handleWheel: (e: React.WheelEvent) => void;
|
handleWheel: (e: React.WheelEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* useTimelineZoom
|
||||||
|
*
|
||||||
|
* Custom hook to manage zoom logic for a timeline component.
|
||||||
|
* Handles zoom state, step calculation, level changes, and mouse wheel zooming (with ctrl/meta).
|
||||||
|
*/
|
||||||
export function useTimelineZoom(): UseTimelineZoomReturn {
|
export function useTimelineZoom(): UseTimelineZoomReturn {
|
||||||
|
// Current zoom level (1 = default)
|
||||||
const [zoomLevel, setZoomLevel] = useState(1);
|
const [zoomLevel, setZoomLevel] = useState(1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the current zoom step based on the zoom level and a base step constant.
|
||||||
|
* Ensures minimum step is 1 (prevents zero or negative step).
|
||||||
|
*/
|
||||||
const zoomStep = useMemo(
|
const zoomStep = useMemo(
|
||||||
() =>
|
() =>
|
||||||
Math.max(1, Math.round(zoomLevel / TIMELINE_CONSTANTS.ZOOM_STEP_BASE)),
|
Math.max(1, Math.round(zoomLevel / TIMELINE_CONSTANTS.ZOOM_STEP_BASE)),
|
||||||
[zoomLevel]
|
[zoomLevel]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the zoom level using a given step.
|
||||||
|
* The zoom level is clamped to a minimum value defined in constants.
|
||||||
|
*/
|
||||||
const handleChangeZoomStep = useCallback((newStep: number) => {
|
const handleChangeZoomStep = useCallback((newStep: number) => {
|
||||||
setZoomLevel(
|
setZoomLevel(
|
||||||
Math.max(
|
Math.max(
|
||||||
@@ -33,6 +48,10 @@ export function useTimelineZoom(): UseTimelineZoomReturn {
|
|||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the zoom level directly.
|
||||||
|
* The new value is clamped between the defined minimum and maximum levels.
|
||||||
|
*/
|
||||||
const handleChangeZoomLevel = useCallback((newLevel: number) => {
|
const handleChangeZoomLevel = useCallback((newLevel: number) => {
|
||||||
setZoomLevel(
|
setZoomLevel(
|
||||||
Math.max(
|
Math.max(
|
||||||
@@ -42,7 +61,10 @@ export function useTimelineZoom(): UseTimelineZoomReturn {
|
|||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Mouse wheel zoom, ctrl/meta gesture
|
/**
|
||||||
|
* Handle zooming using mouse wheel + ctrl/meta key (like browser zoom).
|
||||||
|
* Prevents default browser behavior and updates zoom level accordingly.
|
||||||
|
*/
|
||||||
const handleWheel = useCallback((e: React.WheelEvent) => {
|
const handleWheel = useCallback((e: React.WheelEvent) => {
|
||||||
if (e.ctrlKey || e.metaKey) {
|
if (e.ctrlKey || e.metaKey) {
|
||||||
e.preventDefault?.();
|
e.preventDefault?.();
|
||||||
@@ -59,12 +81,13 @@ export function useTimelineZoom(): UseTimelineZoomReturn {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Return all handlers and state values for use in timeline components
|
||||||
return {
|
return {
|
||||||
zoomLevel,
|
zoomLevel, // Current zoom level
|
||||||
zoomStep,
|
zoomStep, // Current zoom step (calculated from zoomLevel)
|
||||||
setZoomLevel,
|
setZoomLevel, // Directly set the zoom level (accepts value or updater function)
|
||||||
handleChangeZoomStep,
|
handleChangeZoomStep, // Change zoom by step value
|
||||||
handleChangeZoomLevel,
|
handleChangeZoomLevel, // Change zoom by level value
|
||||||
handleWheel,
|
handleWheel, // Handler for mouse wheel zooming
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user