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 &&
}
+
+
@@ -1406,6 +1477,15 @@ const App = memo(() => {
/>
{renderInvertCutButton()}
+
+