From 765caff34f275da836b3dfb2fe8c1bcadf180161 Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Wed, 19 Feb 2020 12:55:51 +0800 Subject: [PATCH] implement zoom #113 --- src/InverseCutSegment.jsx | 2 +- src/TimelineSeg.jsx | 26 ++----- src/main.css | 4 ++ src/renderer.jsx | 146 +++++++++++++++++++++++++++++--------- 4 files changed, 124 insertions(+), 54 deletions(-) diff --git a/src/InverseCutSegment.jsx b/src/InverseCutSegment.jsx index 9abcddc0..025a81bf 100644 --- a/src/InverseCutSegment.jsx +++ b/src/InverseCutSegment.jsx @@ -11,7 +11,7 @@ const InverseCutSegment = ({ seg, duration, invertCutSegments }) => ( top: 0, bottom: 0, left: `${(seg.start / duration) * 100}%`, - width: `${Math.max(((seg.end - seg.start) / duration) * 100, 1)}%`, + width: `${((seg.end - seg.start) / duration) * 100}%`, display: 'flex', alignItems: 'center', pointerEvents: 'none', diff --git a/src/TimelineSeg.jsx b/src/TimelineSeg.jsx index 2ce8db4f..f823fc31 100644 --- a/src/TimelineSeg.jsx +++ b/src/TimelineSeg.jsx @@ -9,15 +9,14 @@ const { formatDuration } = require('./util'); const TimelineSeg = ({ duration, cutStart, cutEnd, isActive, segNum, - onSegClick, color, invertCutSegments, + onSegClick, color, invertCutSegments, zoomed, }) => { - const markerWidth = 4; - const cutSectionWidth = `${Math.max(((cutEnd - cutStart) / duration) * 100, 1)}%`; + const cutSectionWidth = `${((cutEnd - cutStart) / duration) * 100}%`; const strongColor = color.lighten(0.5).string(); const strongBgColor = color.lighten(0.5).alpha(0.5).string(); const startTimePos = `${(cutStart / duration) * 100}%`; - const markerBorder = isActive ? `2px solid ${strongColor}` : undefined; + const markerBorder = `2px solid ${isActive ? strongColor : 'transparent'}`; const backgroundColor = isActive ? strongBgColor : color.alpha(0.5).string(); const markerBorderRadius = 5; @@ -32,19 +31,12 @@ const TimelineSeg = ({ justifyContent: 'space-between', background: backgroundColor, originX: 0, - borderRadius: markerBorderRadius, - }; + boxSizing: 'border-box', - const startMarkerStyle = { - height: '100%', - width: markerWidth, borderLeft: markerBorder, borderTopLeftRadius: markerBorderRadius, borderBottomLeftRadius: markerBorderRadius, - }; - const endMarkerStyle = { - height: '100%', - width: markerWidth, + borderRight: markerBorder, borderTopRightRadius: markerBorderRadius, borderBottomRightRadius: markerBorderRadius, @@ -63,9 +55,7 @@ const TimelineSeg = ({ onClick={onThisSegClick} title={cutEnd > cutStart ? formatDuration({ seconds: cutEnd - cutStart }) : undefined} > -
- -
{segNum + 1}
+
{segNum + 1}
{invertCutSegments && ( @@ -84,10 +74,6 @@ const TimelineSeg = ({
- - {cutEnd > cutStart && ( -
- )} ); }; diff --git a/src/main.css b/src/main.css index 15a340cb..69100ba4 100644 --- a/src/main.css +++ b/src/main.css @@ -96,3 +96,7 @@ input, button, textarea, :focus { .dragging-helper-class { color: rgba(0,0,0,0.5); } + +#timeline-scroller::-webkit-scrollbar { + display: none; +} \ No newline at end of file diff --git a/src/renderer.jsx b/src/renderer.jsx index 4d2ff772..50e5e523 100644 --- a/src/renderer.jsx +++ b/src/renderer.jsx @@ -105,6 +105,7 @@ const App = memo(() => { const [copyStreamIdsByFile, setCopyStreamIdsByFile] = useState({}); const [muted, setMuted] = useState(false); const [streamsSelectorShown, setStreamsSelectorShown] = useState(false); + const [zoom, setZoom] = useState(1); // Global state const [captureFormat, setCaptureFormat] = useState('jpeg'); @@ -118,7 +119,8 @@ const App = memo(() => { const videoRef = useRef(); const timelineWrapperRef = useRef(); - + const timelineScrollerRef = useRef(); + const timelineScrollerSkipEventRef = useRef(); function setCopyStreamIdsForPath(path, cb) { setCopyStreamIdsByFile((old) => { @@ -187,6 +189,7 @@ const App = memo(() => { setMuted(false); setInvertCutSegments(false); setStreamsSelectorShown(false); + setZoom(1); }, []); useEffect(() => () => { @@ -197,11 +200,13 @@ const App = memo(() => { // Because segments could have undefined start / end // (meaning extend to start of timeline or end duration) - function getSegApparentStart(time) { + function getSegApparentStart(seg) { + const time = seg.start; return time !== undefined ? time : 0; } - const getSegApparentEnd = useCallback((time) => { + const getSegApparentEnd = useCallback((seg) => { + const time = seg.end; if (time !== undefined) return time; if (duration !== undefined) return duration; return 0; // Haven't gotten duration yet @@ -209,8 +214,8 @@ const App = memo(() => { const apparentCutSegments = cutSegments.map(cutSegment => ({ ...cutSegment, - start: getSegApparentStart(cutSegment.start), - end: getSegApparentEnd(cutSegment.end), + start: getSegApparentStart(cutSegment), + end: getSegApparentEnd(cutSegment), })); const invalidSegUuids = apparentCutSegments @@ -269,9 +274,16 @@ const App = memo(() => { const setCutTime = useCallback((type, time) => { const cloned = clone(cutSegments); + const currentSeg = cloned[currentSegIndex]; + if (type === 'start' && time >= getSegApparentEnd(currentSeg)) { + throw new Error('Start time must precede end time'); + } + if (type === 'end' && time <= getSegApparentStart(currentSeg)) { + throw new Error('Start time must precede end time'); + } cloned[currentSegIndex][type] = time; setCutSegments(cloned); - }, [currentSegIndex, cutSegments]); + }, [currentSegIndex, getSegApparentEnd, cutSegments]); function formatTimecode(sec) { return formatDuration({ seconds: sec, fps: timecodeShowFrames ? detectedFps : undefined }); @@ -305,15 +317,25 @@ const App = memo(() => { // https://github.com/mifi/lossless-cut/issues/168 // If we are after the end of the last segment in the timeline, // add a new segment that starts at currentTime - if (currentCutSeg.start != null && currentCutSeg.end != null + if (currentCutSeg.end != null && currentTime > currentCutSeg.end) { addCutSegment(); } else { - setCutTime('start', currentTime); + try { + setCutTime('start', currentTime); + } catch (err) { + errorToast(err.message); + } } }, [setCutTime, currentTime, currentCutSeg, addCutSegment]); - const setCutEnd = useCallback(() => setCutTime('end', currentTime), [setCutTime, currentTime]); + const setCutEnd = useCallback(() => { + try { + setCutTime('end', currentTime); + } catch (err) { + errorToast(err.message); + } + }, [setCutTime, currentTime]); async function setOutputDir() { const { filePaths } = await dialog.showOpenDialog({ properties: ['openDirectory'] }); @@ -452,8 +474,34 @@ const App = memo(() => { if (duration) seekAbs((relX / target.offsetWidth) * duration); } + const durationSafe = duration || 1; + const currentTimeWidth = 1; + // Prevent it from overflowing (and causing scroll) when end of timeline + const currentTimePos = currentTime !== undefined && currentTime < durationSafe ? `${(currentTime / durationSafe) * 100}%` : undefined; + + const zoomed = zoom > 1; + + useEffect(() => { + const { currentTime: ct } = videoRef.current; + timelineScrollerSkipEventRef.current = true; + if (zoom > 1) { + timelineScrollerRef.current.scrollLeft = (ct / durationSafe) + * (timelineWrapperRef.current.offsetWidth - timelineScrollerRef.current.offsetWidth); + } + }, [zoom, durationSafe]); + + function onTimelineScroll(e) { + if (timelineScrollerSkipEventRef.current) { + timelineScrollerSkipEventRef.current = false; + return; + } + if (!zoomed) return; + seekAbs((((e.target.scrollLeft + (timelineScrollerRef.current.offsetWidth / 2)) + / timelineWrapperRef.current.offsetWidth) * duration)); + } + function onWheel(e) { - seekRel(e.deltaX / 10); + if (!zoomed) seekRel(e.deltaX / 10); } const playCommand = useCallback(() => { @@ -899,7 +947,11 @@ const App = memo(() => { set(); const rel = time - startTimeOffset; - setCutTime(type, rel); + try { + setCutTime(type, rel); + } catch (err) { + console.error('Cannot set cut time', err); + } seekAbs(rel); }; @@ -923,9 +975,6 @@ const App = memo(() => { const otherFormatsMap = fromPairs(Object.entries(allOutFormats) .filter(([f]) => ![...commonFormats, detectedFileFormat].includes(f))); - const durationSafe = duration || 1; - const currentTimePos = currentTime !== undefined && `${(currentTime / durationSafe) * 100}%`; - const segColor = (currentCutSeg || {}).color; const segBgColor = segColor.alpha(0.5).string(); @@ -1104,6 +1153,18 @@ const App = memo(() => { ); } + useEffect(() => { + const keyScrollPreventer = (e) => { + // https://stackoverflow.com/questions/8916620/disable-arrow-key-scrolling-in-users-browser + if (e.target === document.body && [32, 37, 38, 39, 40].indexOf(e.keyCode) > -1) { + e.preventDefault(); + } + }; + + window.addEventListener('keydown', keyScrollPreventer); + return () => window.removeEventListener('keydown', keyScrollPreventer); + }, []); + const primaryColor = 'hsl(194, 78%, 47%)'; return ( @@ -1271,11 +1332,20 @@ const App = memo(() => { onPan={handleTap} options={{ recognizers: {} }} > -
-
- {currentTimePos !== undefined &&
} +
+
+
+ {currentTimePos !== undefined &&
} - {apparentCutSegments.map((seg, i) => ( { cutStart={seg.start} cutEnd={seg.end} invertCutSegments={invertCutSegments} + zoomed={zoomed} /> ))} - - - {inverseCutSegments && inverseCutSegments.map((seg, i) => ( - - ))} -
-
- {formatTimecode(offsetCurrentTime)} -
+ {inverseCutSegments && inverseCutSegments.map((seg, i) => ( + + ))} +
+
+ +
+
+ {formatTimecode(offsetCurrentTime)}
@@ -1406,6 +1477,15 @@ const App = memo(() => { /> {renderInvertCutButton()} + +