diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 3b4c73d4..7d23ee7d 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -182,6 +182,7 @@ function App() { const durationSafe = isDurationValid(duration) ? duration : 1; const zoom = Math.floor(zoomUnrounded); const zoomedDuration = isDurationValid(duration) ? duration / zoom : undefined; + const zoomWindowEndTime = useMemo(() => (zoomedDuration != null ? zoomWindowStartTime + zoomedDuration : undefined), [zoomedDuration, zoomWindowStartTime]); useEffect(() => setDocumentTitle({ filePath, working: working?.text, progress }), [progress, filePath, working?.text]); @@ -2526,6 +2527,8 @@ function App() { inverseCutSegments={inverseCutSegments} formatTimecode={formatTimecode} formatTimeAndFrames={formatTimeAndFrames} + zoomWindowStartTime={zoomWindowStartTime} + zoomWindowEndTime={zoomWindowEndTime} onZoomWindowStartTimeChange={setZoomWindowStartTime} playing={playing} isFileOpened={isFileOpened} diff --git a/src/renderer/src/Timeline.tsx b/src/renderer/src/Timeline.tsx index 0bf76c33..2e541611 100644 --- a/src/renderer/src/Timeline.tsx +++ b/src/renderer/src/Timeline.tsx @@ -1,4 +1,4 @@ -import { memo, useRef, useMemo, useCallback, useEffect, useState, MutableRefObject, CSSProperties, WheelEventHandler } from 'react'; +import { memo, useRef, useMemo, useCallback, useEffect, useState, MutableRefObject, CSSProperties, WheelEventHandler, MouseEventHandler } from 'react'; import { motion, useMotionValue, useSpring } from 'framer-motion'; import debounce from 'lodash/debounce'; import { useTranslation } from 'react-i18next'; @@ -48,7 +48,11 @@ const Waveform = memo(({ waveform, calculateTimelinePercent, durationSafe }: { // eslint-disable-next-line react/display-name const Waveforms = memo(({ calculateTimelinePercent, durationSafe, waveforms, zoom, height }: { - calculateTimelinePercent: CalculateTimelinePercent, durationSafe: number, waveforms: RenderableWaveform[], zoom: number, height: number, + calculateTimelinePercent: CalculateTimelinePercent, + durationSafe: number, + waveforms: RenderableWaveform[], + zoom: number, + height: number, }) => (
{waveforms.map((waveform) => ( @@ -93,6 +97,8 @@ function Timeline({ shouldShowWaveform, shouldShowKeyframes, thumbnails, + zoomWindowStartTime, + zoomWindowEndTime, onZoomWindowStartTimeChange, waveformEnabled, showThumbnails, @@ -121,6 +127,8 @@ function Timeline({ shouldShowWaveform: boolean, shouldShowKeyframes: boolean, thumbnails: Thumbnail[], + zoomWindowStartTime: number, + zoomWindowEndTime: number | undefined, onZoomWindowStartTimeChange: (a: number) => void, waveformEnabled: boolean, showThumbnails: boolean, @@ -147,14 +155,14 @@ function Timeline({ const isZoomed = zoom > 1; + const keyFramesInZoomWindow = useMemo(() => (zoomWindowEndTime == null ? [] : neighbouringKeyFrames.filter((f) => f.time >= zoomWindowStartTime && f.time <= zoomWindowEndTime)), [neighbouringKeyFrames, zoomWindowEndTime, zoomWindowStartTime]); + // Don't show keyframes if too packed together (at current zoom) // See https://github.com/mifi/lossless-cut/issues/259 - // todo - // const areKeyframesTooClose = keyframes.length > zoom * 200; - const areKeyframesTooClose = false; + const areKeyframesTooClose = keyFramesInZoomWindow.length > zoom * 200; - const calculateTimelinePos = useCallback((time) => (time !== undefined ? Math.min(time / durationSafe, 1) : undefined), [durationSafe]); - const calculateTimelinePercent = useCallback((time) => { + const calculateTimelinePos = useCallback((time: number | undefined) => (time !== undefined ? Math.min(time / durationSafe, 1) : undefined), [durationSafe]); + const calculateTimelinePercent = useCallback((time: number | undefined) => { const pos = calculateTimelinePos(time); return pos !== undefined ? `${pos * 100}%` : undefined; }, [calculateTimelinePos]); @@ -228,7 +236,7 @@ function Timeline({ useEffect(() => { - const cancelWheel = (event) => event.preventDefault(); + const cancelWheel = (event: WheelEvent) => event.preventDefault(); const scroller = timelineScrollerRef.current; invariant(scroller != null); @@ -253,7 +261,7 @@ function Timeline({ / (timelineScrollerRef.current.offsetWidth * zoom)) * duration)); }, [duration, seekAbs, zoomed, zoom, zoomWindowStartTime, onZoomWindowStartTimeChange]); */ - const getMouseTimelinePos = useCallback((e) => { + const getMouseTimelinePos = useCallback((e: MouseEvent) => { const target = timelineWrapperRef.current; invariant(target != null); const rect = target.getBoundingClientRect(); @@ -261,22 +269,22 @@ function Timeline({ return (relX / target.offsetWidth) * durationSafe; }, [durationSafe]); - const mouseDownRef = useRef(); + const mouseDownRef = useRef(); - const handleScrub = useCallback((e) => seekAbs((getMouseTimelinePos(e))), [seekAbs, getMouseTimelinePos]); + const handleScrub = useCallback((e: MouseEvent) => seekAbs((getMouseTimelinePos(e))), [seekAbs, getMouseTimelinePos]); useEffect(() => { setHoveringTime(undefined); }, [relevantTime]); - const onMouseDown = useCallback((e) => { + const onMouseDown = useCallback>((e) => { if (e.nativeEvent.buttons !== 1) return; // not primary button handleScrub(e.nativeEvent); mouseDownRef.current = e.target; - function onMouseMove(e2) { + function onMouseMove(e2: MouseEvent) { if (mouseDownRef.current == null) return; seekAbs(getMouseTimelinePos(e2)); } @@ -294,7 +302,7 @@ function Timeline({ window.addEventListener('mousemove', onMouseMove); }, [getMouseTimelinePos, handleScrub, seekAbs]); - const onMouseMove = useCallback((e) => { + const onMouseMove = useCallback>((e) => { if (!mouseDownRef.current) { // no button pressed setHoveringTime(getMouseTimelinePos(e.nativeEvent)); } @@ -389,7 +397,7 @@ function Timeline({ /> ))} - {shouldShowKeyframes && !areKeyframesTooClose && neighbouringKeyFrames.map((f) => ( + {shouldShowKeyframes && !areKeyframesTooClose && keyFramesInZoomWindow.map((f) => (
))} diff --git a/src/renderer/src/TimelineSeg.tsx b/src/renderer/src/TimelineSeg.tsx index 5cb9d0f5..43c9370a 100644 --- a/src/renderer/src/TimelineSeg.tsx +++ b/src/renderer/src/TimelineSeg.tsx @@ -11,7 +11,14 @@ import { ApparentCutSegment, FormatTimecode } from './types'; function TimelineSeg({ seg, duration, isActive, segNum, onSegClick, invertCutSegments, formatTimecode, selected, } : { - seg: ApparentCutSegment, duration: number, isActive: boolean, segNum: number, onSegClick: (a: number) => void, invertCutSegments: boolean, formatTimecode: FormatTimecode, selected: boolean, + seg: ApparentCutSegment, + duration: number, + isActive: boolean, + segNum: number, + onSegClick: (a: number) => void, + invertCutSegments: boolean, + formatTimecode: FormatTimecode, + selected: boolean, }) { const { darkMode } = useUserSettings(); const { getSegColor } = useSegColors(); diff --git a/src/renderer/src/components/Switch.tsx b/src/renderer/src/components/Switch.tsx index f256be13..1d05690f 100644 --- a/src/renderer/src/components/Switch.tsx +++ b/src/renderer/src/components/Switch.tsx @@ -1,12 +1,11 @@ -import { CSSProperties } from 'react'; +import { RefAttributes } from 'react'; import * as RadixSwitch from '@radix-ui/react-switch'; import classes from './Switch.module.css'; -const Switch = ({ checked, disabled, onCheckedChange, title, style }: { - checked: boolean, disabled?: boolean, onCheckedChange: (v: boolean) => void, title?: string, style?: CSSProperties, -}) => ( - +const Switch = (props: RadixSwitch.SwitchProps & RefAttributes) => ( + // eslint-disable-next-line react/jsx-props-no-spreading + ); diff --git a/src/renderer/src/hooks/useKeyframes.ts b/src/renderer/src/hooks/useKeyframes.ts index 62369339..8e0819bb 100644 --- a/src/renderer/src/hooks/useKeyframes.ts +++ b/src/renderer/src/hooks/useKeyframes.ts @@ -8,9 +8,14 @@ import { FFprobeStream } from '../../../../ffprobe'; const maxKeyframes = 1000; // const maxKeyframes = 100; -export default ({ keyframesEnabled, filePath, commandedTime, videoStream, detectedFps, ffmpegExtractWindow }: { - keyframesEnabled: boolean, filePath: string | undefined, commandedTime: number, videoStream: FFprobeStream | undefined, detectedFps: number | undefined, ffmpegExtractWindow: number, -}) => { +function useKeyframes({ keyframesEnabled, filePath, commandedTime, videoStream, detectedFps, ffmpegExtractWindow }: { + keyframesEnabled: boolean, + filePath: string | undefined, + commandedTime: number, + videoStream: FFprobeStream | undefined, + detectedFps: number | undefined, + ffmpegExtractWindow: number, +}) { const readingKeyframesPromise = useRef>(); const [neighbouringKeyFramesMap, setNeighbouringKeyFrames] = useState>({}); const neighbouringKeyFrames = useMemo(() => Object.values(neighbouringKeyFramesMap), [neighbouringKeyFramesMap]); @@ -61,4 +66,6 @@ export default ({ keyframesEnabled, filePath, commandedTime, videoStream, detect return { neighbouringKeyFrames, findNearestKeyFrameTime, }; -}; +} + +export default useKeyframes;