diff --git a/src/App.jsx b/src/App.jsx index 3bd45a31..40d5c392 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -498,7 +498,7 @@ const App = memo(() => { // const getSafeCutTime = useCallback((cutTime, next) => ffmpeg.getSafeCutTime(neighbouringFrames, cutTime, next), [neighbouringFrames]); - const addCutSegment = useCallback(() => { + const addSegment = useCallback(() => { try { // Cannot add if prev seg is not finished if (currentCutSeg.start === undefined && currentCutSeg.end === undefined) return; @@ -521,7 +521,7 @@ const App = memo(() => { } catch (err) { console.error(err); } - }, [currentCutSeg.start, currentCutSeg.end, getCurrentTime, cutSegments, createIndexedSegment, setCutSegments]); + }, [currentCutSeg.start, currentCutSeg.end, getCurrentTime, duration, cutSegments, createIndexedSegment, setCutSegments]); const setCutStart = useCallback(() => { if (!filePath) return; @@ -530,8 +530,8 @@ const App = memo(() => { // https://github.com/mifi/lossless-cut/issues/168 // If current time is after the end of the current segment in the timeline, // add a new segment that starts at playerTime - if (currentCutSeg.end != null && currentTime > currentCutSeg.end) { - addCutSegment(); + if (currentCutSeg.end != null && currentTime >= currentCutSeg.end) { + addSegment(); } else { try { const startTime = currentTime; @@ -544,7 +544,7 @@ const App = memo(() => { handleError(err); } } - }, [filePath, getCurrentTime, currentCutSeg.end, addCutSegment, setCutTime]); + }, [filePath, getCurrentTime, currentCutSeg.end, addSegment, setCutTime]); const setCutEnd = useCallback(() => { if (!filePath) return; @@ -850,20 +850,22 @@ const App = memo(() => { }); }, [copyAnyAudioTrack, filePath, mainStreams, setCopyStreamIdsForPath]); - const removeCutSegment = useCallback((index) => { - if (cutSegments.length === 1 && cutSegments[0].start == null && cutSegments[0].end == null) return; // Initial segment - - if (cutSegments.length <= 1) { - clearSegments(); - return; - } - - const cutSegmentsNew = [...cutSegments]; - cutSegmentsNew.splice(index, 1); - - setCutSegments(cutSegmentsNew); + const removeSegments = useCallback((removeSegmentIds) => { + if (cutSegments.length === 1 && cutSegments[0].start == null && cutSegments[0].end == null) return; // We are at initial segment, nothing more we can do (it cannot be removed) + setCutSegments((existing) => { + const newSegments = existing.filter((seg) => !removeSegmentIds.includes(seg.segId)); + if (newSegments.length === 0) { + clearSegments(); // when removing the last segments, we start over + return existing; + } + return newSegments; + }); }, [clearSegments, cutSegments, setCutSegments]); + const removeCutSegment = useCallback((index) => { + removeSegments([cutSegments[index].segId]); + }, [cutSegments, removeSegments]); + const thumnailsRef = useRef([]); const thumnailsRenderingPromiseRef = useRef(); @@ -1165,6 +1167,8 @@ const App = memo(() => { const deselectAllSegments = useCallback(() => setDeselectedSegmentIds(Object.fromEntries(cutSegments.map((s) => [s.segId, true]))), [cutSegments]); const selectAllSegments = useCallback(() => setDeselectedSegmentIds({}), []); + const removeSelectedSegments = useCallback(() => removeSegments(selectedSegmentsRaw.map((seg) => seg.segId)), [removeSegments, selectedSegmentsRaw]); + const selectOnlyCurrentSegment = useCallback(() => selectOnlySegment(currentCutSeg), [currentCutSeg, selectOnlySegment]); const toggleCurrentSegmentSelected = useCallback(() => toggleSegmentSelected(currentCutSeg), [currentCutSeg, toggleSegmentSelected]); @@ -1841,7 +1845,7 @@ const App = memo(() => { undo: () => cutSegmentsHistory.back(), redo: () => cutSegmentsHistory.forward(), labelCurrentSegment: () => { onLabelSegmentPress(currentSegIndexSafe); return false; }, - addSegment: () => addCutSegment(), + addSegment, toggleHelp: () => { toggleHelp(); return false; }, export: onExportPress, reorderSegsByStartTime, @@ -1909,7 +1913,7 @@ const App = memo(() => { if (match) return bubble; return true; // bubble the event - }, [addCutSegment, askSetStartTimeOffset, batchFileJump, batchOpenSelectedFile, captureSnapshot, changePlaybackRate, cleanupFilesDialog, clearSegments, closeBatch, closeExportConfirm, concatCurrentBatch, concatDialogVisible, convertFormatBatch, createFixedDurationSegments, createNumSegments, currentSegIndexSafe, cutSegmentsHistory, deselectAllSegments, exportConfirmVisible, extractAllStreams, fillSegmentsGaps, goToTimecode, increaseRotation, invertAllCutSegments, jumpCutEnd, jumpCutStart, jumpSeg, jumpTimelineEnd, jumpTimelineStart, keyboardNormalSeekSpeed, keyboardSeekAccFactor, keyboardShortcutsVisible, onExportConfirm, onExportPress, onLabelSegmentPress, pause, play, removeCutSegment, reorderSegsByStartTime, seekClosestKeyframe, seekRel, seekRelPercent, selectAllSegments, selectOnlyCurrentSegment, setCutEnd, setCutStart, setPlaybackVolume, shortStep, shuffleSegments, splitCurrentSegment, timelineToggleComfortZoom, toggleCaptureFormat, toggleCurrentSegmentSelected, toggleHelp, toggleKeyboardShortcuts, toggleKeyframeCut, togglePlay, toggleSegmentsList, toggleStreamsSelector, toggleStripAudio, tryFixInvalidDuration, userHtml5ifyCurrentFile, zoomRel]); + }, [addSegment, askSetStartTimeOffset, batchFileJump, batchOpenSelectedFile, captureSnapshot, changePlaybackRate, cleanupFilesDialog, clearSegments, closeBatch, closeExportConfirm, concatCurrentBatch, concatDialogVisible, convertFormatBatch, createFixedDurationSegments, createNumSegments, currentSegIndexSafe, cutSegmentsHistory, deselectAllSegments, exportConfirmVisible, extractAllStreams, fillSegmentsGaps, goToTimecode, increaseRotation, invertAllCutSegments, jumpCutEnd, jumpCutStart, jumpSeg, jumpTimelineEnd, jumpTimelineStart, keyboardNormalSeekSpeed, keyboardSeekAccFactor, keyboardShortcutsVisible, onExportConfirm, onExportPress, onLabelSegmentPress, pause, play, removeCutSegment, reorderSegsByStartTime, seekClosestKeyframe, seekRel, seekRelPercent, selectAllSegments, selectOnlyCurrentSegment, setCutEnd, setCutStart, setPlaybackVolume, shortStep, shuffleSegments, splitCurrentSegment, timelineToggleComfortZoom, toggleCaptureFormat, toggleCurrentSegmentSelected, toggleHelp, toggleKeyboardShortcuts, toggleKeyframeCut, togglePlay, toggleSegmentsList, toggleStreamsSelector, toggleStripAudio, tryFixInvalidDuration, userHtml5ifyCurrentFile, zoomRel]); useKeyboard({ keyBindings, onKeyPress }); @@ -2327,8 +2331,9 @@ const App = memo(() => { onLabelSegmentPress={onLabelSegmentPress} currentCutSeg={currentCutSeg} segmentAtCursor={segmentAtCursor} - addCutSegment={addCutSegment} + addSegment={addSegment} removeCutSegment={removeCutSegment} + onRemoveSelectedPress={removeSelectedSegments} toggleSegmentsList={toggleSegmentsList} splitCurrentSegment={splitCurrentSegment} selectedSegmentsRaw={selectedSegmentsRaw} diff --git a/src/SegmentList.jsx b/src/SegmentList.jsx index c8919ee6..6eeb159c 100644 --- a/src/SegmentList.jsx +++ b/src/SegmentList.jsx @@ -21,7 +21,7 @@ const buttonBaseStyle = { const neutralButtonColor = 'rgba(255, 255, 255, 0.2)'; -const Segment = memo(({ seg, index, currentSegIndex, formatTimecode, getFrameCount, updateOrder, invertCutSegments, onClick, onRemovePress, onReorderPress, onLabelPress, enabled, onSelectSingleSegment, onToggleSegmentSelected, onDeselectAllSegments, onSelectSegmentsByLabel, onSelectAllSegments, jumpSegStart, jumpSegEnd, addCutSegment, onViewSegmentTagsPress }) => { +const Segment = memo(({ seg, index, currentSegIndex, formatTimecode, getFrameCount, updateOrder, invertCutSegments, onClick, onRemovePress, onRemoveSelectedPress, onReorderPress, onLabelPress, enabled, onSelectSingleSegment, onToggleSegmentSelected, onDeselectAllSegments, onSelectSegmentsByLabel, onSelectAllSegments, jumpSegStart, jumpSegEnd, addSegment, onViewSegmentTagsPress }) => { const { t } = useTranslation(); const ref = useRef(); @@ -34,9 +34,10 @@ const Segment = memo(({ seg, index, currentSegIndex, formatTimecode, getFrameCou { type: 'separator' }, - { label: t('Add segment'), click: addCutSegment }, + { label: t('Add segment'), click: addSegment }, { label: t('Label segment'), click: onLabelPress }, { label: t('Remove segment'), click: onRemovePress }, + { label: t('Remove selected segments'), click: onRemoveSelectedPress }, { type: 'separator' }, @@ -55,7 +56,7 @@ const Segment = memo(({ seg, index, currentSegIndex, formatTimecode, getFrameCou { label: t('Segment tags'), click: () => onViewSegmentTagsPress(index) }, ]; - }, [invertCutSegments, t, jumpSegStart, jumpSegEnd, addCutSegment, onLabelPress, onRemovePress, onReorderPress, updateOrder, onSelectSingleSegment, seg, onSelectAllSegments, onDeselectAllSegments, onSelectSegmentsByLabel, onViewSegmentTagsPress, index]); + }, [invertCutSegments, t, jumpSegStart, jumpSegEnd, addSegment, onLabelPress, onRemovePress, onRemoveSelectedPress, onReorderPress, updateOrder, onSelectSingleSegment, seg, onSelectAllSegments, onDeselectAllSegments, onSelectSegmentsByLabel, onViewSegmentTagsPress, index]); useContextMenu(ref, contextMenuTemplate); @@ -131,7 +132,7 @@ const Segment = memo(({ seg, index, currentSegIndex, formatTimecode, getFrameCou const SegmentList = memo(({ width, formatTimecode, apparentCutSegments, inverseCutSegments, getFrameCount, onSegClick, currentSegIndex, - updateSegOrder, updateSegOrders, addCutSegment, removeCutSegment, + updateSegOrder, updateSegOrders, addSegment, removeCutSegment, onRemoveSelectedPress, onLabelSegmentPress, currentCutSeg, segmentAtCursor, toggleSegmentsList, splitCurrentSegment, selectedSegments, selectedSegmentsRaw, onSelectSingleSegment, onToggleSegmentSelected, onDeselectAllSegments, onSelectAllSegments, onSelectSegmentsByLabel, jumpSegStart, jumpSegEnd, onViewSegmentTagsPress, @@ -189,7 +190,7 @@ const SegmentList = memo(({ style={{ ...buttonBaseStyle, background: neutralButtonColor }} role="button" title={t('Add segment')} - onClick={addCutSegment} + onClick={addSegment} /> removeCutSegment(index)} onReorderPress={() => onReorderSegsPress(index)} onLabelPress={() => onLabelSegmentPress(index)}