2020-02-27 10:17:35 +01:00
|
|
|
import React, { memo, useRef, useMemo, useCallback, useEffect, useState } from 'react';
|
2021-01-26 23:24:59 +01:00
|
|
|
import { motion, useMotionValue, useSpring } from 'framer-motion';
|
2020-02-29 15:27:01 +01:00
|
|
|
import debounce from 'lodash/debounce';
|
2020-03-19 16:37:38 +01:00
|
|
|
import { useTranslation } from 'react-i18next';
|
2022-01-14 16:27:56 +01:00
|
|
|
import { FaCaretDown, FaCaretUp } from 'react-icons/fa';
|
2020-02-26 04:11:28 +01:00
|
|
|
|
|
|
|
import TimelineSeg from './TimelineSeg';
|
2021-03-31 10:11:29 +02:00
|
|
|
import BetweenSegments from './BetweenSegments';
|
2022-01-14 16:54:33 +01:00
|
|
|
import useContextMenu from './hooks/useContextMenu';
|
2022-03-01 06:53:44 +01:00
|
|
|
import useUserSettings from './hooks/useUserSettings';
|
2020-02-26 04:11:28 +01:00
|
|
|
|
2020-02-29 15:27:01 +01:00
|
|
|
|
2023-03-10 05:12:48 +01:00
|
|
|
import { timelineBackground, darkModeTransition } from './colors';
|
2020-02-26 04:11:28 +01:00
|
|
|
|
2022-01-14 16:27:56 +01:00
|
|
|
const currentTimeWidth = 1;
|
2020-02-26 04:11:28 +01:00
|
|
|
|
2022-01-15 17:02:36 +01:00
|
|
|
const Waveform = memo(({ waveform, calculateTimelinePercent, durationSafe }) => {
|
2020-02-27 10:17:35 +01:00
|
|
|
const [style, setStyle] = useState({ display: 'none' });
|
|
|
|
|
2020-03-05 11:32:02 +01:00
|
|
|
const leftPos = calculateTimelinePercent(waveform.from);
|
2020-02-27 10:17:35 +01:00
|
|
|
|
|
|
|
const toTruncated = Math.min(waveform.to, durationSafe);
|
|
|
|
|
|
|
|
// Prevents flash
|
|
|
|
function onLoad() {
|
|
|
|
setStyle({
|
|
|
|
position: 'absolute', height: '100%', left: leftPos, width: `${((toTruncated - waveform.from) / durationSafe) * 100}%`,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return (
|
2022-01-15 17:02:36 +01:00
|
|
|
<img src={waveform.url} draggable={false} style={style} alt="" onLoad={onLoad} />
|
2020-02-27 10:17:35 +01:00
|
|
|
);
|
|
|
|
});
|
|
|
|
|
2023-03-12 09:51:15 +01:00
|
|
|
const Waveforms = memo(({ calculateTimelinePercent, durationSafe, waveforms, zoom, height }) => (
|
|
|
|
<div style={{ height, width: `${zoom * 100}%`, position: 'relative' }}>
|
2022-01-15 17:02:36 +01:00
|
|
|
{waveforms.map((waveform) => (
|
|
|
|
<Waveform key={`${waveform.from}-${waveform.to}`} waveform={waveform} calculateTimelinePercent={calculateTimelinePercent} durationSafe={durationSafe} />
|
|
|
|
))}
|
|
|
|
</div>
|
|
|
|
));
|
|
|
|
|
2022-01-14 16:27:56 +01:00
|
|
|
const CommandedTime = memo(({ commandedTimePercent }) => {
|
2023-03-10 05:12:48 +01:00
|
|
|
const color = 'var(--gray12)';
|
2022-01-14 16:54:33 +01:00
|
|
|
const commonStyle = { left: commandedTimePercent, position: 'absolute', zIndex: 4, pointerEvents: 'none' };
|
2022-01-14 16:27:56 +01:00
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<FaCaretDown style={{ ...commonStyle, top: 0, color, fontSize: 14, marginLeft: -7, marginTop: -6 }} />
|
|
|
|
<div style={{ ...commonStyle, bottom: 0, top: 0, backgroundColor: color, width: currentTimeWidth }} />
|
|
|
|
<FaCaretUp style={{ ...commonStyle, bottom: 0, color, fontSize: 14, marginLeft: -7, marginBottom: -5 }} />
|
|
|
|
</>
|
2022-01-14 16:54:33 +01:00
|
|
|
);
|
2022-01-14 16:27:56 +01:00
|
|
|
});
|
|
|
|
|
2020-02-26 04:11:28 +01:00
|
|
|
const Timeline = memo(({
|
2023-02-16 04:49:09 +01:00
|
|
|
durationSafe, startTimeOffset, playerTime, commandedTime, relevantTime,
|
2022-01-15 17:02:36 +01:00
|
|
|
zoom, neighbouringKeyFrames, seekAbs, apparentCutSegments,
|
2023-08-21 00:15:49 +02:00
|
|
|
setCurrentSegIndex, currentSegIndexSafe, inverseCutSegments, formatTimecode, formatTimeAndFrames,
|
2023-03-12 09:51:15 +01:00
|
|
|
waveforms, shouldShowWaveform, shouldShowKeyframes, thumbnails,
|
|
|
|
onZoomWindowStartTimeChange, waveformEnabled, showThumbnails,
|
2023-02-18 04:07:38 +01:00
|
|
|
playing, isFileOpened, onWheel, commandedTimeRef, goToTimecode, isSegmentSelected,
|
2020-02-26 04:11:28 +01:00
|
|
|
}) => {
|
2020-03-19 16:37:38 +01:00
|
|
|
const { t } = useTranslation();
|
|
|
|
|
2023-03-12 09:51:15 +01:00
|
|
|
const timelineHeight = 36;
|
|
|
|
|
2023-04-02 17:03:33 +02:00
|
|
|
const { invertCutSegments } = useUserSettings();
|
2022-03-01 06:53:44 +01:00
|
|
|
|
2020-02-26 04:11:28 +01:00
|
|
|
const timelineScrollerRef = useRef();
|
|
|
|
const timelineScrollerSkipEventRef = useRef();
|
2020-02-29 15:27:01 +01:00
|
|
|
const timelineScrollerSkipEventDebounce = useRef();
|
2020-02-26 04:11:28 +01:00
|
|
|
const timelineWrapperRef = useRef();
|
|
|
|
|
2020-11-26 20:40:02 +01:00
|
|
|
const [hoveringTime, setHoveringTime] = useState();
|
|
|
|
|
2023-02-16 04:49:09 +01:00
|
|
|
const displayTime = (hoveringTime != null && isFileOpened && !playing ? hoveringTime : relevantTime) + startTimeOffset;
|
2022-01-15 11:12:53 +01:00
|
|
|
const displayTimePercent = useMemo(() => `${Math.round((displayTime / durationSafe) * 100)}%`, [displayTime, durationSafe]);
|
|
|
|
|
|
|
|
const isZoomed = zoom > 1;
|
2020-02-26 04:11:28 +01:00
|
|
|
|
2020-04-18 09:45:41 +02:00
|
|
|
// Don't show keyframes if too packed together (at current zoom)
|
|
|
|
// See https://github.com/mifi/lossless-cut/issues/259
|
2022-01-15 17:02:36 +01:00
|
|
|
// todo
|
|
|
|
// const areKeyframesTooClose = keyframes.length > zoom * 200;
|
|
|
|
const areKeyframesTooClose = false;
|
2020-02-27 10:17:35 +01:00
|
|
|
|
2022-01-14 16:27:56 +01:00
|
|
|
const calculateTimelinePos = useCallback((time) => (time !== undefined ? Math.min(time / durationSafe, 1) : undefined), [durationSafe]);
|
2020-03-05 11:32:02 +01:00
|
|
|
const calculateTimelinePercent = useCallback((time) => {
|
|
|
|
const pos = calculateTimelinePos(time);
|
|
|
|
return pos !== undefined ? `${pos * 100}%` : undefined;
|
|
|
|
}, [calculateTimelinePos]);
|
2020-02-26 04:11:28 +01:00
|
|
|
|
2020-03-05 11:32:02 +01:00
|
|
|
const currentTimePercent = useMemo(() => calculateTimelinePercent(playerTime), [calculateTimelinePercent, playerTime]);
|
|
|
|
const commandedTimePercent = useMemo(() => calculateTimelinePercent(commandedTime), [calculateTimelinePercent, commandedTime]);
|
2020-02-26 04:11:28 +01:00
|
|
|
|
2021-04-05 19:04:56 +02:00
|
|
|
const timeOfInterestPosPixels = useMemo(() => {
|
|
|
|
// https://github.com/mifi/lossless-cut/issues/676
|
2023-02-16 04:49:09 +01:00
|
|
|
const pos = calculateTimelinePos(relevantTime);
|
2021-01-26 23:24:59 +01:00
|
|
|
if (pos != null && timelineScrollerRef.current) return pos * zoom * timelineScrollerRef.current.offsetWidth;
|
2020-03-05 11:32:02 +01:00
|
|
|
return undefined;
|
2023-02-16 04:49:09 +01:00
|
|
|
}, [calculateTimelinePos, relevantTime, zoom]);
|
2020-02-26 04:11:28 +01:00
|
|
|
|
2020-03-05 12:00:23 +01:00
|
|
|
const calcZoomWindowStartTime = useCallback(() => (timelineScrollerRef.current
|
2020-11-22 23:39:39 +01:00
|
|
|
? (timelineScrollerRef.current.scrollLeft / (timelineScrollerRef.current.offsetWidth * zoom)) * durationSafe
|
|
|
|
: 0), [durationSafe, zoom]);
|
2020-03-05 12:00:23 +01:00
|
|
|
|
|
|
|
// const zoomWindowStartTime = calcZoomWindowStartTime(duration, zoom);
|
2020-02-26 04:11:28 +01:00
|
|
|
|
2020-02-29 15:27:01 +01:00
|
|
|
useEffect(() => {
|
|
|
|
timelineScrollerSkipEventDebounce.current = debounce(() => {
|
|
|
|
timelineScrollerSkipEventRef.current = false;
|
|
|
|
}, 1000);
|
|
|
|
}, []);
|
|
|
|
|
2020-03-05 11:32:02 +01:00
|
|
|
function suppressScrollerEvents() {
|
2020-02-26 04:11:28 +01:00
|
|
|
timelineScrollerSkipEventRef.current = true;
|
2020-02-29 15:27:01 +01:00
|
|
|
timelineScrollerSkipEventDebounce.current();
|
2020-03-05 11:32:02 +01:00
|
|
|
}
|
|
|
|
|
2021-01-26 23:24:59 +01:00
|
|
|
const scrollLeftMotion = useMotionValue(0);
|
|
|
|
|
|
|
|
const spring = useSpring(scrollLeftMotion, { damping: 100, stiffness: 1000 });
|
|
|
|
|
|
|
|
useEffect(() => {
|
2023-02-16 04:32:17 +01:00
|
|
|
spring.on('change', (value) => {
|
2021-01-26 23:24:59 +01:00
|
|
|
if (timelineScrollerSkipEventRef.current) return; // Don't animate while zooming
|
|
|
|
timelineScrollerRef.current.scrollLeft = value;
|
|
|
|
});
|
|
|
|
}, [spring]);
|
|
|
|
|
2020-03-05 11:32:02 +01:00
|
|
|
// Pan timeline when cursor moves out of timeline window
|
|
|
|
useEffect(() => {
|
2021-04-05 19:04:56 +02:00
|
|
|
if (timeOfInterestPosPixels == null || timelineScrollerSkipEventRef.current) return;
|
2021-01-26 23:24:59 +01:00
|
|
|
|
2021-04-05 19:04:56 +02:00
|
|
|
if (timeOfInterestPosPixels > timelineScrollerRef.current.scrollLeft + timelineScrollerRef.current.offsetWidth) {
|
2021-01-26 23:24:59 +01:00
|
|
|
const timelineWidth = timelineWrapperRef.current.offsetWidth;
|
2021-04-05 19:04:56 +02:00
|
|
|
const scrollLeft = timeOfInterestPosPixels - (timelineScrollerRef.current.offsetWidth * 0.1);
|
2021-01-26 23:24:59 +01:00
|
|
|
scrollLeftMotion.set(Math.min(scrollLeft, timelineWidth - timelineScrollerRef.current.offsetWidth));
|
2021-04-05 19:04:56 +02:00
|
|
|
} else if (timeOfInterestPosPixels < timelineScrollerRef.current.scrollLeft) {
|
|
|
|
const scrollLeft = timeOfInterestPosPixels - (timelineScrollerRef.current.offsetWidth * 0.9);
|
2021-01-26 23:24:59 +01:00
|
|
|
scrollLeftMotion.set(Math.max(scrollLeft, 0));
|
2020-03-05 11:32:02 +01:00
|
|
|
}
|
2021-04-05 19:04:56 +02:00
|
|
|
}, [timeOfInterestPosPixels, scrollLeftMotion]);
|
2020-03-05 11:32:02 +01:00
|
|
|
|
|
|
|
// Keep cursor in middle while zooming
|
|
|
|
useEffect(() => {
|
|
|
|
suppressScrollerEvents();
|
|
|
|
|
2022-01-15 11:12:53 +01:00
|
|
|
if (isZoomed) {
|
2020-02-29 15:27:01 +01:00
|
|
|
const zoomedTargetWidth = timelineScrollerRef.current.offsetWidth * zoom;
|
2020-02-27 10:17:35 +01:00
|
|
|
|
2021-01-26 23:24:59 +01:00
|
|
|
const scrollLeft = Math.max((commandedTimeRef.current / durationSafe) * zoomedTargetWidth - timelineScrollerRef.current.offsetWidth / 2, 0);
|
|
|
|
scrollLeftMotion.set(scrollLeft);
|
|
|
|
timelineScrollerRef.current.scrollLeft = scrollLeft;
|
2020-02-26 04:11:28 +01:00
|
|
|
}
|
2022-01-15 11:12:53 +01:00
|
|
|
}, [zoom, durationSafe, commandedTimeRef, scrollLeftMotion, isZoomed]);
|
2020-02-26 04:11:28 +01:00
|
|
|
|
2020-02-27 15:59:37 +01:00
|
|
|
|
2020-03-05 11:32:02 +01:00
|
|
|
useEffect(() => {
|
|
|
|
const cancelWheel = (event) => event.preventDefault();
|
|
|
|
|
|
|
|
const scroller = timelineScrollerRef.current;
|
|
|
|
scroller.addEventListener('wheel', cancelWheel, { passive: false });
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
scroller.removeEventListener('wheel', cancelWheel);
|
|
|
|
};
|
|
|
|
}, []);
|
2020-02-27 15:59:37 +01:00
|
|
|
|
2020-03-05 11:32:02 +01:00
|
|
|
const onTimelineScroll = useCallback(() => {
|
2020-03-05 12:00:23 +01:00
|
|
|
onZoomWindowStartTimeChange(calcZoomWindowStartTime());
|
|
|
|
}, [calcZoomWindowStartTime, onZoomWindowStartTimeChange]);
|
2020-02-27 15:59:37 +01:00
|
|
|
|
2020-03-05 11:32:02 +01:00
|
|
|
// Keep cursor in middle while scrolling
|
|
|
|
/* const onTimelineScroll = useCallback((e) => {
|
|
|
|
onZoomWindowStartTimeChange(zoomWindowStartTime);
|
|
|
|
|
|
|
|
if (!zoomed || timelineScrollerSkipEventRef.current) return;
|
2020-02-27 15:59:37 +01:00
|
|
|
|
2020-02-27 10:17:35 +01:00
|
|
|
seekAbs((((e.target.scrollLeft + (timelineScrollerRef.current.offsetWidth * 0.5))
|
|
|
|
/ (timelineScrollerRef.current.offsetWidth * zoom)) * duration));
|
2020-03-05 11:32:02 +01:00
|
|
|
}, [duration, seekAbs, zoomed, zoom, zoomWindowStartTime, onZoomWindowStartTimeChange]); */
|
|
|
|
|
2020-11-26 20:40:02 +01:00
|
|
|
const getMouseTimelinePos = useCallback((e) => {
|
2020-02-26 04:11:28 +01:00
|
|
|
const target = timelineWrapperRef.current;
|
|
|
|
const rect = target.getBoundingClientRect();
|
2020-11-26 20:40:02 +01:00
|
|
|
const relX = e.pageX - (rect.left + document.body.scrollLeft);
|
|
|
|
return (relX / target.offsetWidth) * durationSafe;
|
|
|
|
}, [durationSafe]);
|
|
|
|
|
2023-01-27 07:02:49 +01:00
|
|
|
const mouseDownRef = useRef();
|
|
|
|
|
|
|
|
const handleScrub = useCallback((e) => seekAbs((getMouseTimelinePos(e))), [seekAbs, getMouseTimelinePos]);
|
2020-02-26 04:11:28 +01:00
|
|
|
|
2020-11-26 20:40:02 +01:00
|
|
|
useEffect(() => {
|
|
|
|
setHoveringTime();
|
2023-02-16 04:49:09 +01:00
|
|
|
}, [relevantTime]);
|
2020-11-26 20:40:02 +01:00
|
|
|
|
2023-01-27 07:02:49 +01:00
|
|
|
const onMouseDown = useCallback((e) => {
|
|
|
|
if (e.nativeEvent.buttons !== 1) return; // not primary button
|
|
|
|
|
|
|
|
handleScrub(e.nativeEvent);
|
|
|
|
|
|
|
|
mouseDownRef.current = e.target;
|
|
|
|
|
|
|
|
function onMouseMove(e2) {
|
|
|
|
if (mouseDownRef.current == null) return;
|
|
|
|
seekAbs(getMouseTimelinePos(e2));
|
|
|
|
}
|
|
|
|
|
|
|
|
function onMouseUp() {
|
|
|
|
mouseDownRef.current = undefined;
|
|
|
|
window.removeEventListener('mouseup', onMouseUp);
|
|
|
|
window.removeEventListener('mousemove', onMouseMove);
|
|
|
|
}
|
|
|
|
|
|
|
|
// https://github.com/mifi/lossless-cut/issues/1432
|
|
|
|
// https://stackoverflow.com/questions/11533098/how-to-catch-mouse-up-event-outside-of-element
|
|
|
|
// https://stackoverflow.com/questions/6073505/what-is-the-difference-between-screenx-y-clientx-y-and-pagex-y
|
|
|
|
window.addEventListener('mouseup', onMouseUp, { once: true });
|
|
|
|
window.addEventListener('mousemove', onMouseMove);
|
|
|
|
}, [getMouseTimelinePos, handleScrub, seekAbs]);
|
|
|
|
|
2022-11-23 06:11:30 +01:00
|
|
|
const onMouseMove = useCallback((e) => {
|
2023-01-27 07:02:49 +01:00
|
|
|
if (!mouseDownRef.current) { // no button pressed
|
2022-11-23 06:11:30 +01:00
|
|
|
setHoveringTime(getMouseTimelinePos(e.nativeEvent));
|
|
|
|
}
|
2023-01-27 07:02:49 +01:00
|
|
|
e.preventDefault();
|
|
|
|
}, [getMouseTimelinePos]);
|
|
|
|
|
2020-11-26 20:40:02 +01:00
|
|
|
const onMouseOut = useCallback(() => setHoveringTime(), []);
|
|
|
|
|
2022-01-14 18:48:41 +01:00
|
|
|
const contextMenuTemplate = useMemo(() => [
|
2022-01-14 16:54:33 +01:00
|
|
|
{ label: t('Seek to timecode'), click: goToTimecode },
|
2022-01-14 18:48:41 +01:00
|
|
|
], [goToTimecode, t]);
|
|
|
|
|
|
|
|
useContextMenu(timelineScrollerRef, contextMenuTemplate);
|
2022-01-14 16:54:33 +01:00
|
|
|
|
2020-02-26 04:11:28 +01:00
|
|
|
return (
|
2022-11-23 06:11:30 +01:00
|
|
|
// eslint-disable-next-line jsx-a11y/no-static-element-interactions,jsx-a11y/mouse-events-have-key-events
|
|
|
|
<div
|
2023-04-02 17:03:33 +02:00
|
|
|
style={{ position: 'relative', borderTop: '1px solid var(--gray7)', borderBottom: '1px solid var(--gray7)' }}
|
2023-01-27 07:02:49 +01:00
|
|
|
onMouseDown={onMouseDown}
|
2020-11-26 20:40:02 +01:00
|
|
|
onMouseMove={onMouseMove}
|
|
|
|
onMouseOut={onMouseOut}
|
2020-02-26 04:11:28 +01:00
|
|
|
>
|
2023-03-12 09:51:15 +01:00
|
|
|
{(waveformEnabled && !shouldShowWaveform) && (
|
|
|
|
<div style={{ pointerEvents: 'none', display: 'flex', alignItems: 'center', justifyContent: 'center', height: timelineHeight, bottom: timelineHeight, left: 0, right: 0, color: 'var(--gray11)' }}>
|
|
|
|
{t('Zoom in more to view waveform')}
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
|
2022-11-23 06:11:30 +01:00
|
|
|
<div
|
|
|
|
style={{ overflowX: 'scroll' }}
|
|
|
|
className="hide-scrollbar"
|
|
|
|
onWheel={onWheel}
|
|
|
|
onScroll={onTimelineScroll}
|
|
|
|
ref={timelineScrollerRef}
|
|
|
|
>
|
|
|
|
{waveformEnabled && shouldShowWaveform && waveforms.length > 0 && (
|
|
|
|
<Waveforms
|
|
|
|
calculateTimelinePercent={calculateTimelinePercent}
|
|
|
|
durationSafe={durationSafe}
|
|
|
|
waveforms={waveforms}
|
|
|
|
zoom={zoom}
|
2023-03-12 09:51:15 +01:00
|
|
|
height={40}
|
2022-11-23 06:11:30 +01:00
|
|
|
/>
|
|
|
|
)}
|
|
|
|
|
2023-03-12 09:51:15 +01:00
|
|
|
{showThumbnails && (
|
|
|
|
<div style={{ height: 60, width: `${zoom * 100}%`, position: 'relative', marginBottom: 3 }}>
|
2022-11-23 06:11:30 +01:00
|
|
|
{thumbnails.map((thumbnail, i) => {
|
|
|
|
const leftPercent = (thumbnail.time / durationSafe) * 100;
|
|
|
|
const nextThumbnail = thumbnails[i + 1];
|
|
|
|
const nextThumbTime = nextThumbnail ? nextThumbnail.time : durationSafe;
|
|
|
|
const maxWidthPercent = ((nextThumbTime - thumbnail.time) / durationSafe) * 100 * 0.9;
|
|
|
|
return (
|
2023-03-12 09:51:15 +01:00
|
|
|
<img key={thumbnail.url} src={thumbnail.url} alt="" style={{ position: 'absolute', left: `${leftPercent}%`, height: '100%', boxSizing: 'border-box', zIndex: 1, maxWidth: `${maxWidthPercent}%`, objectFit: 'cover', border: '1px solid rgba(255, 255, 255, 0.5)', borderBottomRightRadius: 15, borderTopLeftRadius: 15, borderTopRightRadius: 15, pointerEvents: 'none' }} />
|
2022-11-23 06:11:30 +01:00
|
|
|
);
|
|
|
|
})}
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
|
2020-02-26 04:11:28 +01:00
|
|
|
<div
|
2023-03-10 05:12:48 +01:00
|
|
|
style={{ height: timelineHeight, width: `${zoom * 100}%`, position: 'relative', backgroundColor: timelineBackground, transition: darkModeTransition }}
|
2022-11-23 06:11:30 +01:00
|
|
|
ref={timelineWrapperRef}
|
2020-02-26 04:11:28 +01:00
|
|
|
>
|
2022-11-23 06:11:30 +01:00
|
|
|
{currentTimePercent !== undefined && (
|
2023-03-10 05:12:48 +01:00
|
|
|
<motion.div transition={{ type: 'spring', damping: 70, stiffness: 800 }} animate={{ left: currentTimePercent }} style={{ position: 'absolute', bottom: 0, top: 0, zIndex: 3, backgroundColor: 'var(--gray12)', width: currentTimeWidth, pointerEvents: 'none' }} />
|
2020-02-27 10:17:35 +01:00
|
|
|
)}
|
2022-11-23 06:11:30 +01:00
|
|
|
{commandedTimePercent !== undefined && (
|
|
|
|
<CommandedTime commandedTimePercent={commandedTimePercent} />
|
2020-02-27 15:59:37 +01:00
|
|
|
)}
|
|
|
|
|
2022-11-23 06:11:30 +01:00
|
|
|
{apparentCutSegments.map((seg, i) => {
|
|
|
|
if (seg.start === 0 && seg.end === 0) return null; // No video loaded
|
2020-12-12 00:25:02 +01:00
|
|
|
|
2023-02-18 04:07:38 +01:00
|
|
|
const selected = invertCutSegments || isSegmentSelected({ segId: seg.segId });
|
|
|
|
|
2022-11-23 06:11:30 +01:00
|
|
|
return (
|
|
|
|
<TimelineSeg
|
|
|
|
key={seg.segId}
|
2023-04-03 02:27:30 +02:00
|
|
|
seg={seg}
|
2022-11-23 06:11:30 +01:00
|
|
|
segNum={i}
|
|
|
|
onSegClick={setCurrentSegIndex}
|
|
|
|
isActive={i === currentSegIndexSafe}
|
2020-02-26 04:11:28 +01:00
|
|
|
duration={durationSafe}
|
|
|
|
invertCutSegments={invertCutSegments}
|
2022-11-23 06:11:30 +01:00
|
|
|
formatTimecode={formatTimecode}
|
2023-02-18 04:07:38 +01:00
|
|
|
selected={selected}
|
2020-02-26 04:11:28 +01:00
|
|
|
/>
|
2022-11-23 06:11:30 +01:00
|
|
|
);
|
|
|
|
})}
|
|
|
|
|
|
|
|
{inverseCutSegments.map((seg) => (
|
|
|
|
<BetweenSegments
|
2023-02-16 05:32:48 +01:00
|
|
|
key={seg.segId}
|
2022-11-23 06:11:30 +01:00
|
|
|
start={seg.start}
|
|
|
|
end={seg.end}
|
|
|
|
duration={durationSafe}
|
|
|
|
invertCutSegments={invertCutSegments}
|
|
|
|
/>
|
|
|
|
))}
|
2020-02-26 04:11:28 +01:00
|
|
|
|
2022-11-23 06:11:30 +01:00
|
|
|
{shouldShowKeyframes && !areKeyframesTooClose && neighbouringKeyFrames.map((f) => (
|
2023-03-10 05:53:22 +01:00
|
|
|
<div key={f.time} style={{ position: 'absolute', top: 0, bottom: 0, left: `${(f.time / durationSafe) * 100}%`, marginLeft: -1, width: 1, background: 'var(--gray11)', pointerEvents: 'none' }} />
|
2022-11-23 06:11:30 +01:00
|
|
|
))}
|
2020-02-26 04:11:28 +01:00
|
|
|
</div>
|
2022-11-23 06:11:30 +01:00
|
|
|
</div>
|
2020-02-26 04:11:28 +01:00
|
|
|
|
2022-11-23 06:11:30 +01:00
|
|
|
<div style={{ position: 'absolute', height: timelineHeight, left: 0, right: 0, bottom: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', pointerEvents: 'none', zIndex: 2 }}>
|
|
|
|
<div style={{ background: 'rgba(0,0,0,0.4)', borderRadius: 3, padding: '2px 4px', color: 'rgba(255, 255, 255, 0.8)' }}>
|
2023-08-21 00:15:49 +02:00
|
|
|
{formatTimeAndFrames(displayTime)}{isZoomed ? ` ${displayTimePercent}` : ''}
|
2020-02-26 04:11:28 +01:00
|
|
|
</div>
|
|
|
|
</div>
|
2022-11-23 06:11:30 +01:00
|
|
|
</div>
|
2020-02-26 04:11:28 +01:00
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
export default Timeline;
|