implement preview of all segments #253

pull/1482/head
Mikael Finstad 3 years ago
parent 9ea6326a16
commit ff3fb745a5
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26

@ -115,7 +115,7 @@ const App = memo(() => {
const [usingDummyVideo, setUsingDummyVideo] = useState(false);
const [playing, setPlaying] = useState(false);
const [canvasPlayerEventId, setCanvasPlayerEventId] = useState(0);
const playingOnlySegmentIdRef = useRef();
const playbackModeRef = useRef();
const [playerTime, setPlayerTime] = useState();
const [duration, setDuration] = useState();
const [rotation, setRotation] = useState(360);
@ -278,14 +278,19 @@ const App = memo(() => {
setCanvasPlayerEventId((id) => id + 1); // To make sure that we can seek even to the same commanded time that we are already add (e.g. loop current segment)
}, []);
const userSeekAbs = useCallback((val) => {
playbackModeRef.current = undefined; // If the user seeks, we clear any custom playback mode
return seekAbs(val);
}, [seekAbs]);
const commandedTimeRef = useRef(commandedTime);
useEffect(() => {
commandedTimeRef.current = commandedTime;
}, [commandedTime]);
const seekRel = useCallback((val) => {
seekAbs(videoRef.current.currentTime + val);
}, [seekAbs]);
userSeekAbs(videoRef.current.currentTime + val);
}, [userSeekAbs]);
const seekRelPercent = useCallback((val) => {
if (!isDurationValid(zoomedDuration)) return;
@ -299,8 +304,8 @@ const App = memo(() => {
// try to align with frame
const currentTimeNearestFrameNumber = getFrameCountRaw(fps, videoRef.current.currentTime);
const nextFrame = currentTimeNearestFrameNumber + direction;
seekAbs(nextFrame / fps);
}, [seekAbs, detectedFps]);
userSeekAbs(nextFrame / fps);
}, [detectedFps, userSeekAbs]);
// 360 means we don't modify rotation
const isRotationSet = rotation !== 360;
@ -343,15 +348,15 @@ const App = memo(() => {
}, [isFileOpened]);
const {
cutSegments, cutSegmentsHistory, createSegmentsFromKeyframes, shuffleSegments, detectBlackScenes, detectSilentScenes, detectSceneChanges, removeCutSegment, invertAllSegments, fillSegmentsGaps, combineOverlappingSegments, shiftAllSegmentTimes, alignSegmentTimesToKeyframes, onViewSegmentTags, updateSegOrder, updateSegOrders, reorderSegsByStartTime, addSegment, setCutStart, setCutEnd, onLabelSegment, splitCurrentSegment, createNumSegments, createFixedDurationSegments, createRandomSegments, apparentCutSegments, haveInvalidSegs, currentSegIndexSafe, currentCutSeg, currentApparentCutSeg, inverseCutSegments, clearSegments, loadCutSegments, selectedSegmentsRaw, setCutTime, getSegApparentEnd, setCurrentSegIndex, onLabelSelectedSegments, deselectAllSegments, selectAllSegments, selectOnlyCurrentSegment, toggleCurrentSegmentSelected, removeSelectedSegments, setDeselectedSegmentIds, onSelectSegmentsByLabel, toggleSegmentSelected, selectOnlySegment, getApparentCutSegmentById,
} = useSegments({ filePath, workingRef, setWorking, setCutProgress, mainVideoStream, duration, getRelevantTime, maxLabelLength, checkFileOpened });
cutSegments, cutSegmentsHistory, createSegmentsFromKeyframes, shuffleSegments, detectBlackScenes, detectSilentScenes, detectSceneChanges, removeCutSegment, invertAllSegments, fillSegmentsGaps, combineOverlappingSegments, shiftAllSegmentTimes, alignSegmentTimesToKeyframes, onViewSegmentTags, updateSegOrder, updateSegOrders, reorderSegsByStartTime, addSegment, setCutStart, setCutEnd, onLabelSegment, splitCurrentSegment, createNumSegments, createFixedDurationSegments, createRandomSegments, apparentCutSegments, haveInvalidSegs, currentSegIndexSafe, currentCutSeg, currentApparentCutSeg, inverseCutSegments, clearSegments, loadCutSegments, selectedSegmentsRaw, setCutTime, getSegApparentEnd, setCurrentSegIndex, onLabelSelectedSegments, deselectAllSegments, selectAllSegments, selectOnlyCurrentSegment, toggleCurrentSegmentSelected, removeSelectedSegments, setDeselectedSegmentIds, onSelectSegmentsByLabel, toggleSegmentSelected, selectOnlySegment, getApparentCutSegmentById, selectedSegments, selectedSegmentsOrInverse, nonFilteredSegmentsOrInverse,
} = useSegments({ filePath, workingRef, setWorking, setCutProgress, mainVideoStream, duration, getRelevantTime, maxLabelLength, checkFileOpened, invertCutSegments });
const jumpSegStart = useCallback((index) => seekAbs(apparentCutSegments[index].start), [apparentCutSegments, seekAbs]);
const jumpSegEnd = useCallback((index) => seekAbs(apparentCutSegments[index].end), [apparentCutSegments, seekAbs]);
const jumpSegStart = useCallback((index) => userSeekAbs(apparentCutSegments[index].start), [apparentCutSegments, userSeekAbs]);
const jumpSegEnd = useCallback((index) => userSeekAbs(apparentCutSegments[index].end), [apparentCutSegments, userSeekAbs]);
const jumpCutStart = useCallback(() => jumpSegStart(currentSegIndexSafe), [currentSegIndexSafe, jumpSegStart]);
const jumpCutEnd = useCallback(() => jumpSegEnd(currentSegIndexSafe), [currentSegIndexSafe, jumpSegEnd]);
const jumpTimelineStart = useCallback(() => seekAbs(0), [seekAbs]);
const jumpTimelineEnd = useCallback(() => seekAbs(durationSafe), [durationSafe, seekAbs]);
const jumpTimelineStart = useCallback(() => userSeekAbs(0), [userSeekAbs]);
const jumpTimelineEnd = useCallback(() => userSeekAbs(durationSafe), [durationSafe, userSeekAbs]);
const getFrameCount = useCallback((sec) => getFrameCountRaw(detectedFps, sec), [detectedFps]);
@ -435,7 +440,7 @@ const App = memo(() => {
const onStopPlaying = useCallback(() => {
onPlayingChange(false);
playingOnlySegmentIdRef.current = undefined;
playbackModeRef.current = undefined;
}, []);
const onSartPlaying = useCallback(() => onPlayingChange(true), []);
const onDurationChange = useCallback((e) => {
@ -629,7 +634,7 @@ const App = memo(() => {
setPreviewFilePath();
setUsingDummyVideo(false);
setPlaying(false);
playingOnlySegmentIdRef.current = undefined;
playbackModeRef.current = undefined;
setCanvasPlayerEventId(0);
setDuration();
cutSegmentsHistory.go(0);
@ -761,6 +766,9 @@ const App = memo(() => {
const showPlaybackFailedMessage = () => errorToast(i18n.t('Unable to playback this file. Try to convert to supported format from the menu'));
const getNewJumpIndex = (oldIndex, direction) => Math.max(oldIndex + direction, 0);
const jumpSeg = useCallback((direction) => setCurrentSegIndex((old) => Math.min(getNewJumpIndex(old, direction), cutSegments.length - 1)), [cutSegments, setCurrentSegIndex]);
const pause = useCallback(() => {
if (!filePath || !playing) return;
videoRef.current.pause();
@ -777,38 +785,56 @@ const App = memo(() => {
});
}, [filePath, playing]);
const togglePlay = useCallback(({ resetPlaybackRate, onlyCurrentSegment } = {}) => {
playingOnlySegmentIdRef.current = undefined;
const togglePlay = useCallback(({ resetPlaybackRate, playbackMode } = {}) => {
playbackModeRef.current = undefined;
if (playing) {
pause();
return;
}
if (onlyCurrentSegment != null) {
playingOnlySegmentIdRef.current = { segId: currentApparentCutSeg.segId, mode: onlyCurrentSegment };
seekAbs(currentApparentCutSeg.start);
if (playbackMode != null) {
if (playbackMode === 'loop-selected-segments') {
const firstSelectedSegment = selectedSegments[0];
playbackModeRef.current = { segId: firstSelectedSegment.segId, playbackMode };
const index = apparentCutSegments.indexOf(firstSelectedSegment);
if (index >= 0) setCurrentSegIndex(index);
seekAbs(firstSelectedSegment.start);
} else {
playbackModeRef.current = { segId: currentApparentCutSeg.segId, playbackMode };
seekAbs(currentApparentCutSeg.start);
}
}
play(resetPlaybackRate);
}, [playing, play, pause, currentApparentCutSeg.segId, currentApparentCutSeg.start, seekAbs]);
}, [playing, play, pause, selectedSegments, apparentCutSegments, setCurrentSegIndex, seekAbs, currentApparentCutSeg.segId, currentApparentCutSeg.start]);
const onTimeUpdate = useCallback((e) => {
const { currentTime } = e.target;
if (playerTime === currentTime) return;
setPlayerTime(currentTime);
if (playingOnlySegmentIdRef.current != null) {
const { segId, mode } = playingOnlySegmentIdRef.current;
const playingOnlySegment = getApparentCutSegmentById(segId);
if (playingOnlySegment != null) {
const { seek, stop } = playOnlyCurrentSegment({ mode, currentTime, playingOnlySegment });
if (seek) seekAbs(seek);
if (playbackModeRef.current != null) {
const { segId, playbackMode } = playbackModeRef.current;
const playingSegment = getApparentCutSegmentById(segId);
if (playingSegment != null) {
const { seek, stop, nextSegment } = playOnlyCurrentSegment({ playbackMode, currentTime, playingSegment });
// console.log({ seek, stop, nextSegment });
if (nextSegment != null) {
const index = selectedSegments.indexOf(playingSegment);
let newIndex = getNewJumpIndex(index >= 0 ? index : 0, 1);
if (newIndex > selectedSegments.length - 1) newIndex = 0; // have reached end of last segment, start over
const nextSelectedSegment = selectedSegments[newIndex];
if (nextSelectedSegment != null) seekAbs(nextSelectedSegment.start);
playbackModeRef.current.segId = nextSelectedSegment.segId;
}
if (seek != null) seekAbs(seek);
if (stop) {
playingOnlySegmentIdRef.current = undefined;
playbackModeRef.current = undefined;
pause();
}
}
}
}, [getApparentCutSegmentById, pause, playerTime, seekAbs]);
}, [getApparentCutSegmentById, pause, playerTime, seekAbs, selectedSegments]);
const closeFileWithConfirm = useCallback(() => {
if (!isFileOpened || workingRef.current) return;
@ -998,14 +1024,6 @@ const App = memo(() => {
}
}, [isFileOpened, cleanupChoices, askForCleanupChoices, cleanupFiles, setWorking]);
// For invertCutSegments we do not support filtering
const selectedSegmentsOrInverseRaw = useMemo(() => (invertCutSegments ? inverseCutSegments : selectedSegmentsRaw), [inverseCutSegments, invertCutSegments, selectedSegmentsRaw]);
const nonFilteredSegments = useMemo(() => (invertCutSegments ? inverseCutSegments : apparentCutSegments), [invertCutSegments, inverseCutSegments, apparentCutSegments]);
// If user has selected none to export, it makes no sense, so export all instead
const selectedSegmentsOrInverse = selectedSegmentsOrInverseRaw.length > 0 ? selectedSegmentsOrInverseRaw : nonFilteredSegments;
const segmentsToExport = useMemo(() => {
if (!segmentsToChaptersOnly) return selectedSegmentsOrInverse;
// segmentsToChaptersOnly is a special mode where all segments will be simply written out as chapters to one file: https://github.com/mifi/lossless-cut/issues/993#issuecomment-1037927595
@ -1430,13 +1448,11 @@ const App = memo(() => {
const toggleLastCommands = useCallback(() => setLastCommandsVisible(val => !val), []);
const toggleSettings = useCallback(() => setSettingsVisible(val => !val), []);
const jumpSeg = useCallback((val) => setCurrentSegIndex((old) => Math.max(Math.min(old + val, cutSegments.length - 1), 0)), [cutSegments.length, setCurrentSegIndex]);
const seekClosestKeyframe = useCallback((direction) => {
const time = findNearestKeyFrameTime({ time: getRelevantTime(), direction });
if (time == null) return;
seekAbs(time);
}, [findNearestKeyFrameTime, getRelevantTime, seekAbs]);
userSeekAbs(time);
}, [findNearestKeyFrameTime, getRelevantTime, userSeekAbs]);
const seekAccelerationRef = useRef(1);
@ -1520,8 +1536,8 @@ const App = memo(() => {
if (timeCode === undefined) return;
seekAbs(timeCode);
}, [filePath, seekAbs]);
userSeekAbs(timeCode);
}, [filePath, userSeekAbs]);
const toggleStreamsSelector = useCallback(() => setStreamsSelectorShown((v) => !v), []);
@ -1767,6 +1783,8 @@ const App = memo(() => {
setConcatDialogVisible(true);
}, [batchFiles.length, openFilesDialog]);
const toggleLoopSelectedSegments = useCallback(() => togglePlay({ resetPlaybackRate: true, playbackMode: 'loop-selected-segments' }), [togglePlay]);
const onKeyPress = useCallback(({ action, keyup }) => {
function seekReset() {
seekAccelerationRef.current = 1;
@ -1777,9 +1795,10 @@ const App = memo(() => {
const mainActions = {
togglePlayNoResetSpeed: () => togglePlay(),
togglePlayResetSpeed: () => togglePlay({ resetPlaybackRate: true }),
togglePlayOnlyCurrentSegment: () => togglePlay({ resetPlaybackRate: true, onlyCurrentSegment: 'play' }),
toggleLoopOnlyCurrentSegment: () => togglePlay({ resetPlaybackRate: true, onlyCurrentSegment: 'loop-full' }),
toggleLoopStartEndOnlyCurrentSegment: () => togglePlay({ resetPlaybackRate: true, onlyCurrentSegment: 'loop-start-end' }),
togglePlayOnlyCurrentSegment: () => togglePlay({ resetPlaybackRate: true, playbackMode: 'play-segment-once' }),
toggleLoopOnlyCurrentSegment: () => togglePlay({ resetPlaybackRate: true, playbackMode: 'loop-segment' }),
toggleLoopStartEndOnlyCurrentSegment: () => togglePlay({ resetPlaybackRate: true, playbackMode: 'loop-segment-start-end' }),
toggleLoopSelectedSegments,
play: () => play(),
pause,
reducePlaybackRate: () => changePlaybackRate(-1),
@ -1907,7 +1926,7 @@ const App = memo(() => {
if (match) return bubble;
return true; // bubble the event
}, [addSegment, alignSegmentTimesToKeyframes, askSetStartTimeOffset, batchFileJump, batchOpenSelectedFile, captureSnapshot, captureSnapshotAsCoverArt, changePlaybackRate, cleanupFilesDialog, clearSegments, closeBatch, closeExportConfirm, combineOverlappingSegments, concatCurrentBatch, concatDialogVisible, convertFormatBatch, createFixedDurationSegments, createNumSegments, createRandomSegments, currentSegIndexSafe, cutSegmentsHistory, deselectAllSegments, exportConfirmVisible, extractAllStreams, extractCurrentSegmentFramesAsImages, fillSegmentsGaps, goToTimecode, increaseRotation, invertAllSegments, jumpCutEnd, jumpCutStart, jumpSeg, jumpTimelineEnd, jumpTimelineStart, keyboardNormalSeekSpeed, keyboardSeekAccFactor, keyboardShortcutsVisible, onExportConfirm, onExportPress, onLabelSegment, pause, play, removeCutSegment, removeSelectedSegments, reorderSegsByStartTime, seekClosestKeyframe, seekRel, seekRelPercent, selectAllSegments, selectOnlyCurrentSegment, setCutEnd, setCutStart, setPlaybackVolume, shiftAllSegmentTimes, shortStep, shuffleSegments, splitCurrentSegment, timelineToggleComfortZoom, toggleCaptureFormat, toggleCurrentSegmentSelected, toggleKeyboardShortcuts, toggleKeyframeCut, toggleLastCommands, togglePlay, toggleSegmentsList, toggleStreamsSelector, toggleStripAudio, tryFixInvalidDuration, userHtml5ifyCurrentFile, zoomRel]);
}, [addSegment, alignSegmentTimesToKeyframes, askSetStartTimeOffset, batchFileJump, batchOpenSelectedFile, captureSnapshot, captureSnapshotAsCoverArt, changePlaybackRate, cleanupFilesDialog, clearSegments, closeBatch, closeExportConfirm, combineOverlappingSegments, concatCurrentBatch, concatDialogVisible, convertFormatBatch, createFixedDurationSegments, createNumSegments, createRandomSegments, currentSegIndexSafe, cutSegmentsHistory, deselectAllSegments, exportConfirmVisible, extractAllStreams, extractCurrentSegmentFramesAsImages, fillSegmentsGaps, goToTimecode, increaseRotation, invertAllSegments, jumpCutEnd, jumpCutStart, jumpSeg, jumpTimelineEnd, jumpTimelineStart, keyboardNormalSeekSpeed, keyboardSeekAccFactor, keyboardShortcutsVisible, onExportConfirm, onExportPress, onLabelSegment, pause, play, removeCutSegment, removeSelectedSegments, reorderSegsByStartTime, seekClosestKeyframe, seekRel, seekRelPercent, selectAllSegments, selectOnlyCurrentSegment, setCutEnd, setCutStart, setPlaybackVolume, shiftAllSegmentTimes, shortStep, shuffleSegments, splitCurrentSegment, timelineToggleComfortZoom, toggleCaptureFormat, toggleCurrentSegmentSelected, toggleKeyboardShortcuts, toggleKeyframeCut, toggleLastCommands, toggleLoopSelectedSegments, togglePlay, toggleSegmentsList, toggleStreamsSelector, toggleStripAudio, tryFixInvalidDuration, userHtml5ifyCurrentFile, zoomRel]);
useKeyboard({ keyBindings, onKeyPress });
@ -2277,7 +2296,7 @@ const App = memo(() => {
commandedTimeRef={commandedTimeRef}
startTimeOffset={startTimeOffset}
zoom={zoom}
seekAbs={seekAbs}
seekAbs={userSeekAbs}
durationSafe={durationSafe}
apparentCutSegments={apparentCutSegments}
setCurrentSegIndex={setCurrentSegIndex}
@ -2304,10 +2323,11 @@ const App = memo(() => {
captureSnapshot={captureSnapshot}
onExportPress={onExportPress}
segmentsToExport={segmentsToExport}
seekAbs={seekAbs}
seekAbs={userSeekAbs}
currentSegIndexSafe={currentSegIndexSafe}
cutSegments={cutSegments}
currentCutSeg={currentCutSeg}
selectedSegments={selectedSegments}
setCutStart={setCutStart}
setCutEnd={setCutEnd}
setCurrentSegIndex={setCurrentSegIndex}
@ -2332,6 +2352,8 @@ const App = memo(() => {
keyframesEnabled={keyframesEnabled}
toggleKeyframesEnabled={toggleKeyframesEnabled}
detectedFps={detectedFps}
toggleLoopSelectedSegments={toggleLoopSelectedSegments}
isFileOpened={isFileOpened}
/>
</div>
@ -2371,7 +2393,7 @@ const App = memo(() => {
)}
</SideSheet>
<ExportConfirm filePath={filePath} areWeCutting={areWeCutting} nonFilteredSegments={nonFilteredSegments} selectedSegments={selectedSegmentsOrInverse} segmentsToExport={segmentsToExport} willMerge={willMerge} visible={exportConfirmVisible} onClosePress={closeExportConfirm} onExportConfirm={onExportConfirm} renderOutFmt={renderOutFmt} outputDir={outputDir} numStreamsTotal={numStreamsTotal} numStreamsToCopy={numStreamsToCopy} setStreamsSelectorShown={setStreamsSelectorShown} outFormat={fileFormat} setOutSegTemplate={setOutSegTemplate} outSegTemplate={outSegTemplateOrDefault} generateOutSegFileNames={generateOutSegFileNames} currentSegIndexSafe={currentSegIndexSafe} getOutSegError={getOutSegError} mainCopiedThumbnailStreams={mainCopiedThumbnailStreams} />
<ExportConfirm filePath={filePath} areWeCutting={areWeCutting} nonFilteredSegmentsOrInverse={nonFilteredSegmentsOrInverse} selectedSegments={selectedSegmentsOrInverse} segmentsToExport={segmentsToExport} willMerge={willMerge} visible={exportConfirmVisible} onClosePress={closeExportConfirm} onExportConfirm={onExportConfirm} renderOutFmt={renderOutFmt} outputDir={outputDir} numStreamsTotal={numStreamsTotal} numStreamsToCopy={numStreamsToCopy} setStreamsSelectorShown={setStreamsSelectorShown} outFormat={fileFormat} setOutSegTemplate={setOutSegTemplate} outSegTemplate={outSegTemplateOrDefault} generateOutSegFileNames={generateOutSegFileNames} currentSegIndexSafe={currentSegIndexSafe} getOutSegError={getOutSegError} mainCopiedThumbnailStreams={mainCopiedThumbnailStreams} />
<LastCommandsSheet
visible={lastCommandsVisible}

@ -1,4 +1,4 @@
import React, { memo, useCallback, useEffect } from 'react';
import React, { memo, useCallback, useEffect, useMemo } from 'react';
import { Select } from 'evergreen-ui';
import { motion } from 'framer-motion';
import { MdRotate90DegreesCcw } from 'react-icons/md';
@ -33,11 +33,37 @@ const BottomBar = memo(({
seekAbs, currentSegIndexSafe, cutSegments, currentCutSeg, setCutStart, setCutEnd,
setCurrentSegIndex, cutStartTimeManual, setCutStartTimeManual, cutEndTimeManual, setCutEndTimeManual,
jumpTimelineStart, jumpTimelineEnd, jumpCutEnd, jumpCutStart, startTimeOffset, setCutTime, currentApparentCutSeg,
playing, shortStep, togglePlay, toggleTimelineMode, hasAudio, timelineMode,
keyframesEnabled, toggleKeyframesEnabled, seekClosestKeyframe, detectedFps,
playing, shortStep, togglePlay, toggleLoopSelectedSegments, toggleTimelineMode, hasAudio, timelineMode,
keyframesEnabled, toggleKeyframesEnabled, seekClosestKeyframe, detectedFps, isFileOpened, selectedSegments,
}) => {
const { t } = useTranslation();
// ok this is a bit over-engineered but what the hell!
const loopSelectedSegmentsButtonStyle = useMemo(() => {
// cannot have less than 1 gradient element:
const selectedSegmentsSafe = (selectedSegments.length > 1 ? selectedSegments : [selectedSegments[0], selectedSegments[0]]).slice(0, 10);
const gradientColors = selectedSegmentsSafe.map((seg, i) => {
const segColor = getSegColor(seg);
// make colors stronger, the more segments
return `${segColor.alpha(Math.max(0.4, Math.min(0.8, selectedSegmentsSafe.length / 3))).string()} ${((i / (selectedSegmentsSafe.length - 1)) * 100).toFixed(1)}%`;
}).join(', ');
return {
paddingLeft: 2,
backgroundOffset: 30,
background: `linear-gradient(90deg, ${gradientColors})`,
border: '1px solid rgb(200,200,200)',
margin: '2px 4px 0 0px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: 20,
height: 24,
borderRadius: 4,
};
}, [selectedSegments]);
const { invertCutSegments, setInvertCutSegments, simpleMode, toggleSimpleMode, exportConfirmEnabled } = useUserSettings();
const onYinYangClick = useCallback(() => {
@ -199,6 +225,7 @@ const BottomBar = memo(({
<>
<FaStepBackward
size={16}
style={{ flexShrink: 0 }}
title={t('Jump to start of video')}
role="button"
onClick={jumpTimelineStart}
@ -234,11 +261,17 @@ const BottomBar = memo(({
<div role="button" onClick={() => togglePlay()} style={{ background: primaryColor, margin: '2px 5px 0 5px', display: 'flex', alignItems: 'center', justifyContent: 'center', width: 34, height: 34, borderRadius: 17 }}>
<PlayPause
style={{ marginLeft: playing ? 0 : 2 }}
style={{ paddingLeft: playing ? 0 : 2 }}
size={16}
/>
</div>
<div role="button" onClick={toggleLoopSelectedSegments} title={t('Play selected segments in order')} style={loopSelectedSegmentsButtonStyle}>
<FaPlay
size={14}
/>
</div>
{!simpleMode && (
<FaCaretRight
style={{ flexShrink: 0, marginRight: -6, marginLeft: -4 }}
@ -269,6 +302,7 @@ const BottomBar = memo(({
<FaStepForward
size={16}
style={{ flexShrink: 0 }}
title={t('Jump to end of video')}
role="button"
onClick={jumpTimelineEnd}
@ -335,7 +369,7 @@ const BottomBar = memo(({
</>
)}
{!simpleMode && (
{!simpleMode && isFileOpened && (
<FaTrashAlt
title={t('Close file and clean up')}
style={{ padding: '5px 10px' }}

@ -44,7 +44,7 @@ const HelpIcon = ({ onClick, style }) => <IoIosHelpCircle size={20} role="button
const ExportConfirm = memo(({
areWeCutting, selectedSegments, segmentsToExport, willMerge, visible, onClosePress, onExportConfirm,
outFormat, renderOutFmt, outputDir, numStreamsTotal, numStreamsToCopy, setStreamsSelectorShown, outSegTemplate,
setOutSegTemplate, generateOutSegFileNames, filePath, currentSegIndexSafe, getOutSegError, nonFilteredSegments,
setOutSegTemplate, generateOutSegFileNames, filePath, currentSegIndexSafe, getOutSegError, nonFilteredSegmentsOrInverse,
mainCopiedThumbnailStreams,
}) => {
const { t } = useTranslation();
@ -137,7 +137,7 @@ const ExportConfirm = memo(({
<h2 style={{ marginTop: 0, marginBottom: '.5em' }}>{t('Export options')}</h2>
<ul style={{ margin: 0 }}>
{selectedSegments.length !== nonFilteredSegments.length && <li><FaRegCheckCircle size={12} style={{ marginRight: 3 }} />{t('{{selectedSegments}} of {{nonFilteredSegments}} segments selected', { selectedSegments: selectedSegments.length, nonFilteredSegments: nonFilteredSegments.length })}</li>}
{selectedSegments.length !== nonFilteredSegmentsOrInverse.length && <li><FaRegCheckCircle size={12} style={{ marginRight: 3 }} />{t('{{selectedSegments}} of {{nonFilteredSegments}} segments selected', { selectedSegments: selectedSegments.length, nonFilteredSegments: nonFilteredSegmentsOrInverse.length })}</li>}
<li>
{t('Merge {{segments}} cut segments to one file?', { segments: selectedSegments.length })} <ExportModeButton selectedSegments={selectedSegments} />
<HelpIcon onClick={onExportModeHelpPress} />

@ -76,7 +76,7 @@ const Segment = memo(({ seg, index, currentSegIndex, formatTimecode, getFrameCou
}, 300, [isActive]);
function renderNumber() {
if (invertCutSegments) return <FaSave style={{ cursor: 'grab', color: saveColor, marginRight: 5, verticalAlign: 'middle' }} size={14} />;
if (invertCutSegments) return <FaSave style={{ color: saveColor, marginRight: 5, verticalAlign: 'middle' }} size={14} />;
const segColor = getSegColor(seg);
@ -104,6 +104,8 @@ const Segment = memo(({ seg, index, currentSegIndex, formatTimecode, getFrameCou
onToggleSegmentSelected(seg);
}, [onToggleSegmentSelected, seg]);
const cursor = invertCutSegments ? undefined : 'grab';
return (
<motion.div
ref={ref}
@ -117,9 +119,9 @@ const Segment = memo(({ seg, index, currentSegIndex, formatTimecode, getFrameCou
exit={{ scaleY: 0 }}
className="segment-list-entry"
>
<div className="segment-handle" style={{ cursor: 'grab', color: 'white', marginBottom: 3, display: 'flex', alignItems: 'center', height: 16 }}>
<div className="segment-handle" style={{ cursor, color: 'white', marginBottom: 3, display: 'flex', alignItems: 'center', height: 16 }}>
{renderNumber()}
<span style={{ cursor: 'grab', fontSize: Math.min(310 / timeStr.length, 14), whiteSpace: 'nowrap' }}>{timeStr}</span>
<span style={{ cursor, fontSize: Math.min(310 / timeStr.length, 14), whiteSpace: 'nowrap' }}>{timeStr}</span>
</div>
<div style={{ fontSize: 12, color: 'white' }}>{seg.name}</div>

@ -113,6 +113,7 @@ const KeyboardShortcuts = memo(({
const { actionsMap, extraLinesPerCategory } = useMemo(() => {
const playbackCategory = t('Playback');
const selectivePlaybackCategory = t('Playback/preview segments only');
const seekingCategory = t('Seeking');
const segmentsAndCutpointsCategory = t('Segments and cut points');
const zoomOperationsCategory = t('Timeline/zoom operations');
@ -156,18 +157,6 @@ const KeyboardShortcuts = memo(({
name: t('Play/pause (no reset speed)'),
category: playbackCategory,
},
togglePlayOnlyCurrentSegment: {
name: t('Play/pause (only current segment)'),
category: playbackCategory,
},
toggleLoopOnlyCurrentSegment: {
name: t('Loop/pause (only current segment)'),
category: playbackCategory,
},
toggleLoopStartEndOnlyCurrentSegment: {
name: t('Loop/pause (only beginning and end of current segment)'),
category: playbackCategory,
},
play: {
name: t('Play'),
category: playbackCategory,
@ -201,6 +190,24 @@ const KeyboardShortcuts = memo(({
category: playbackCategory,
},
// selectivePlaybackCategory
togglePlayOnlyCurrentSegment: {
name: t('Play current segment once'),
category: selectivePlaybackCategory,
},
toggleLoopOnlyCurrentSegment: {
name: t('Loop current segment'),
category: selectivePlaybackCategory,
},
toggleLoopStartEndOnlyCurrentSegment: {
name: t('Loop beginning and end of current segment'),
category: selectivePlaybackCategory,
},
toggleLoopSelectedSegments: {
name: t('Play selected segments in order'),
category: selectivePlaybackCategory,
},
// seekingCategory
seekPreviousFrame: {
name: t('Step backward 1 frame'),

@ -14,7 +14,7 @@ const SegmentCutpointButton = ({ currentCutSeg, side, Icon, onClick, title, styl
size={13}
title={title}
role="button"
style={{ color: 'white', padding: start ? '4px 4px 4px 2px' : '4px 2px 4px 4px', borderLeft: start && border, borderRight: !start && border, background: backgroundColor, borderRadius: 6, ...style }}
style={{ flexShrink: 0, color: 'white', padding: start ? '4px 4px 4px 2px' : '4px 2px 4px 4px', borderLeft: start && border, borderRight: !start && border, background: backgroundColor, borderRadius: 6, ...style }}
onClick={onClick}
/>
);

@ -18,7 +18,7 @@ import { maxSegmentsAllowed } from '../util/constants';
export default ({
filePath, workingRef, setWorking, setCutProgress, mainVideoStream,
duration, getRelevantTime, maxLabelLength, checkFileOpened,
duration, getRelevantTime, maxLabelLength, checkFileOpened, invertCutSegments,
}) => {
// Segment related state
const segCounterRef = useRef(0);
@ -430,6 +430,13 @@ export default ({
}));
}, [maxLabelLength, selectedSegmentsRaw, setCutSegments]);
// Guaranteed to have at least one segment (if user has selected none to export (selectedSegments empty), it makes no sense so select all instead.)
const selectedSegments = useMemo(() => (selectedSegmentsRaw.length > 0 ? selectedSegmentsRaw : apparentCutSegments), [apparentCutSegments, selectedSegmentsRaw]);
// For invertCutSegments we do not support filtering (selecting) segments
const selectedSegmentsOrInverse = useMemo(() => (invertCutSegments ? inverseCutSegments : selectedSegments), [inverseCutSegments, invertCutSegments, selectedSegments]);
const nonFilteredSegmentsOrInverse = useMemo(() => (invertCutSegments ? inverseCutSegments : apparentCutSegments), [invertCutSegments, inverseCutSegments, apparentCutSegments]);
const removeSelectedSegments = useCallback(() => removeSegments(selectedSegmentsRaw.map((seg) => seg.segId)), [removeSegments, selectedSegmentsRaw]);
const selectOnlySegment = useCallback((seg) => setDeselectedSegmentIds(Object.fromEntries(cutSegments.filter((s) => s.segId !== seg.segId).map((s) => [s.segId, true]))), [cutSegments]);
@ -476,7 +483,9 @@ export default ({
clearSegments,
loadCutSegments,
selectedSegmentsRaw,
setCutTime,
selectedSegments,
selectedSegmentsOrInverse,
nonFilteredSegmentsOrInverse,
getSegApparentEnd,
setCurrentSegIndex,

@ -165,34 +165,49 @@ export function convertSegmentsToChapters(sortedSegments) {
return sortSegments([...sortedSegments, ...invertedSegments]);
}
export function playOnlyCurrentSegment({ mode, currentTime, playingOnlySegment }) {
if (mode === 'loop-start-end') {
const maxSec = 3; // max time each side (start/end)
const sec = Math.min(maxSec, (playingOnlySegment.end - playingOnlySegment.start) / 3) * 2;
const startWindowEnd = playingOnlySegment.start + sec / 2;
const endWindowStart = playingOnlySegment.end - sec / 2;
export function playOnlyCurrentSegment({ playbackMode, currentTime, playingSegment }) {
switch (playbackMode) {
case 'loop-segment-start-end': {
const maxSec = 3; // max time each side (start/end)
const sec = Math.min(maxSec, (playingSegment.end - playingSegment.start) / 3) * 2;
const startWindowEnd = playingSegment.start + sec / 2;
const endWindowStart = playingSegment.end - sec / 2;
if (currentTime >= playingSegment.end) {
return { seek: playingSegment.start };
}
if (currentTime < endWindowStart && currentTime >= startWindowEnd) {
return { seek: endWindowStart };
}
break;
}
if (currentTime >= playingOnlySegment.end) {
return { seek: playingOnlySegment.start };
case 'loop-segment': {
if (currentTime >= playingSegment.end) {
return { seek: playingSegment.start };
}
break;
}
if (currentTime < endWindowStart && currentTime >= startWindowEnd) {
return { seek: endWindowStart };
case 'play-segment-once': {
if (currentTime >= playingSegment.end) {
return {
seek: playingSegment.end,
stop: true,
};
}
break;
}
}
if (mode === 'loop-full') {
if (currentTime >= playingOnlySegment.end) {
return { seek: playingOnlySegment.start };
case 'loop-selected-segments': {
if (currentTime >= playingSegment.end) {
return { nextSegment: true };
}
break;
}
}
// mode === 'play'
if (currentTime >= playingOnlySegment.end) {
return {
seek: playingOnlySegment.end,
stop: true,
};
default:
}
return {};

Loading…
Cancel
Save