From 2502188367575891d50e75664ffe6aa860753f28 Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Tue, 18 Feb 2025 12:22:16 +0800 Subject: [PATCH] improvements - implement "fill segment gaps" and "invert segments" for markers too - new function: select all markers - import PBF as markers - closes #993 --- src/renderer/src/App.tsx | 18 +++--- src/renderer/src/SegmentList.tsx | 28 ++++++---- .../src/components/KeyboardShortcuts.tsx | 4 ++ src/renderer/src/edlFormats.test.ts | 2 +- src/renderer/src/edlFormats.ts | 27 ++------- src/renderer/src/hooks/useSegments.ts | 55 ++++++++++++------- types.ts | 2 +- 7 files changed, 74 insertions(+), 62 deletions(-) diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 15b99a95..b1f6a3a6 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -335,7 +335,7 @@ function App() { }, [isFileOpened]); const { - cutSegments, cutSegmentsHistory, createSegmentsFromKeyframes, shuffleSegments, detectBlackScenes, detectSilentScenes, detectSceneChanges, removeCutSegment, invertAllSegments, fillSegmentsGaps, combineOverlappingSegments, combineSelectedSegments, shiftAllSegmentTimes, alignSegmentTimesToKeyframes, updateSegOrder, updateSegOrders, reorderSegsByStartTime, addSegment, setCutStart, setCutEnd, onLabelSegment, splitCurrentSegment, focusSegmentAtCursor, createNumSegments, createFixedDurationSegments, createFixedByteSizedSegments, createRandomSegments, haveInvalidSegs, currentSegIndexSafe, currentCutSeg, inverseCutSegments, clearSegments, loadCutSegments, isSegmentSelected, setCutTime, setCurrentSegIndex, onLabelSelectedSegments, deselectAllSegments, selectAllSegments, selectOnlyCurrentSegment, toggleCurrentSegmentSelected, invertSelectedSegments, removeSelectedSegments, setDeselectedSegmentIds, onSelectSegmentsByLabel, onSelectSegmentsByExpr, onMutateSegmentsByExpr, toggleSegmentSelected, selectOnlySegment, selectedSegments, selectedSegmentsOrInverse, nonFilteredSegmentsOrInverse, segmentsToExport, duplicateCurrentSegment, duplicateSegment, updateSegAtIndex, findSegmentsAtCursor, + cutSegments, cutSegmentsHistory, createSegmentsFromKeyframes, shuffleSegments, detectBlackScenes, detectSilentScenes, detectSceneChanges, removeCutSegment, invertAllSegments, fillSegmentsGaps, combineOverlappingSegments, combineSelectedSegments, shiftAllSegmentTimes, alignSegmentTimesToKeyframes, updateSegOrder, updateSegOrders, reorderSegsByStartTime, addSegment, setCutStart, setCutEnd, labelSegment, splitCurrentSegment, focusSegmentAtCursor, createNumSegments, createFixedDurationSegments, createFixedByteSizedSegments, createRandomSegments, haveInvalidSegs, currentSegIndexSafe, currentCutSeg, inverseCutSegments, clearSegments, loadCutSegments, isSegmentSelected, setCutTime, setCurrentSegIndex, labelSelectedSegments, deselectAllSegments, selectAllSegments, selectOnlyCurrentSegment, toggleCurrentSegmentSelected, invertSelectedSegments, removeSelectedSegments, setDeselectedSegmentIds, selectSegmentsByLabel, selectSegmentsByExpr, selectAllMarkers, mutateSegmentsByExpr, toggleSegmentSelected, selectOnlySegment, selectedSegments, selectedSegmentsOrInverse, nonFilteredSegmentsOrInverse, segmentsToExport, duplicateCurrentSegment, duplicateSegment, updateSegAtIndex, findSegmentsAtCursor, } = useSegments({ filePath, workingRef, setWorking, setProgress, videoStream: activeVideoStream, duration, getRelevantTime, maxLabelLength, checkFileOpened, invertCutSegments, segmentsToChaptersOnly, timecodePlaceholder, parseTimecode, appendFfmpegCommandLog, durationSafe, mainFileMeta }); const { getEdlFilePath, projectFileSavePath, getProjectFileSavePath } = useSegmentsAutoSave({ autoSaveProjectFile, storeProjectInWorkingDir, filePath, customOutDir, cutSegments }); @@ -1979,7 +1979,7 @@ function App() { removeCurrentSegment: () => removeCutSegment(currentSegIndexSafe), undo: () => cutSegmentsHistory.back(), redo: () => cutSegmentsHistory.forward(), - labelCurrentSegment: () => { onLabelSegment(currentSegIndexSafe); return false; }, + labelCurrentSegment: () => { labelSegment(currentSegIndexSafe); return false; }, addSegment, duplicateCurrentSegment, toggleLastCommands: () => { toggleLastCommands(); return false; }, @@ -2047,10 +2047,11 @@ function App() { toggleShowKeyframes, showIncludeExternalStreamsDialog, toggleFullscreenVideo, + selectAllMarkers, }; return ret; - }, [toggleLoopSelectedSegments, pause, timelineToggleComfortZoom, captureSnapshot, captureSnapshotAsCoverArt, setCutStart, setCutEnd, cleanupFilesDialog, splitCurrentSegment, focusSegmentAtCursor, increaseRotation, goToTimecode, jumpCutStart, jumpCutEnd, jumpTimelineStart, jumpTimelineEnd, batchOpenSelectedFile, closeBatch, addSegment, duplicateCurrentSegment, onExportPress, extractCurrentSegmentFramesAsImages, extractSelectedSegmentsFramesAsImages, reorderSegsByStartTime, invertAllSegments, fillSegmentsGaps, combineOverlappingSegments, combineSelectedSegments, createFixedDurationSegments, createNumSegments, createFixedByteSizedSegments, createRandomSegments, alignSegmentTimesToKeyframes, shuffleSegments, clearSegments, toggleSegmentsList, toggleStreamsSelector, extractAllStreams, convertFormatBatch, concatBatch, toggleCaptureFormat, toggleStripAudio, toggleStripThumbnail, askStartTimeOffset, deselectAllSegments, selectAllSegments, selectOnlyCurrentSegment, editCurrentSegmentTags, toggleCurrentSegmentSelected, invertSelectedSegments, removeSelectedSegments, tryFixInvalidDuration, shiftAllSegmentTimes, toggleMuted, copySegmentsToClipboard, handleShowStreamsSelectorClick, openFilesDialog, openDirDialog, toggleSettings, createSegmentsFromKeyframes, toggleWaveformMode, toggleShowThumbnails, toggleShowKeyframes, showIncludeExternalStreamsDialog, toggleFullscreenVideo, checkFileOpened, cutSegments, seekRel, keyboardSeekAccFactor, togglePlay, play, userChangePlaybackRate, keyboardNormalSeekSpeed, keyboardSeekSpeed2, keyboardSeekSpeed3, seekRelPercent, seekClosestKeyframe, shortStep, jumpSeg, setCurrentSegIndex, zoomRel, batchFileJump, removeCutSegment, currentSegIndexSafe, cutSegmentsHistory, onLabelSegment, toggleLastCommands, userHtml5ifyCurrentFile, toggleKeyframeCut, setPlaybackVolume, closeFileWithConfirm, openSendReportDialogWithState, detectBlackScenes, detectSilentScenes, detectSceneChanges]); + }, [toggleLoopSelectedSegments, pause, timelineToggleComfortZoom, captureSnapshot, captureSnapshotAsCoverArt, setCutStart, setCutEnd, cleanupFilesDialog, splitCurrentSegment, focusSegmentAtCursor, increaseRotation, goToTimecode, jumpCutStart, jumpCutEnd, jumpTimelineStart, jumpTimelineEnd, batchOpenSelectedFile, closeBatch, addSegment, duplicateCurrentSegment, onExportPress, extractCurrentSegmentFramesAsImages, extractSelectedSegmentsFramesAsImages, reorderSegsByStartTime, invertAllSegments, fillSegmentsGaps, combineOverlappingSegments, combineSelectedSegments, createFixedDurationSegments, createNumSegments, createFixedByteSizedSegments, createRandomSegments, alignSegmentTimesToKeyframes, shuffleSegments, clearSegments, toggleSegmentsList, toggleStreamsSelector, extractAllStreams, convertFormatBatch, concatBatch, toggleCaptureFormat, toggleStripAudio, toggleStripThumbnail, askStartTimeOffset, deselectAllSegments, selectAllSegments, selectOnlyCurrentSegment, editCurrentSegmentTags, toggleCurrentSegmentSelected, invertSelectedSegments, removeSelectedSegments, tryFixInvalidDuration, shiftAllSegmentTimes, toggleMuted, copySegmentsToClipboard, handleShowStreamsSelectorClick, openFilesDialog, openDirDialog, toggleSettings, createSegmentsFromKeyframes, toggleWaveformMode, toggleShowThumbnails, toggleShowKeyframes, showIncludeExternalStreamsDialog, toggleFullscreenVideo, selectAllMarkers, checkFileOpened, cutSegments, seekRel, keyboardSeekAccFactor, togglePlay, play, userChangePlaybackRate, keyboardNormalSeekSpeed, keyboardSeekSpeed2, keyboardSeekSpeed3, seekRelPercent, seekClosestKeyframe, shortStep, jumpSeg, setCurrentSegIndex, zoomRel, batchFileJump, removeCutSegment, currentSegIndexSafe, cutSegmentsHistory, labelSegment, toggleLastCommands, userHtml5ifyCurrentFile, toggleKeyframeCut, setPlaybackVolume, closeFileWithConfirm, openSendReportDialogWithState, detectBlackScenes, detectSilentScenes, detectSceneChanges]); const getKeyboardAction = useCallback((action: MainKeyboardAction) => mainActions[action], [mainActions]); @@ -2497,7 +2498,7 @@ function App() { onSegClick={setCurrentSegIndex} updateSegOrder={updateSegOrder} updateSegOrders={updateSegOrders} - onLabelSegment={onLabelSegment} + onLabelSegment={labelSegment} currentCutSeg={currentCutSeg} segmentAtCursor={segmentAtCursor} addSegment={addSegment} @@ -2516,10 +2517,11 @@ function App() { onExtractSegmentFramesAsImages={extractSegmentFramesAsImages} jumpSegStart={jumpSegStart} jumpSegEnd={jumpSegEnd} - onSelectSegmentsByLabel={onSelectSegmentsByLabel} - onSelectSegmentsByExpr={onSelectSegmentsByExpr} - onMutateSegmentsByExpr={onMutateSegmentsByExpr} - onLabelSelectedSegments={onLabelSelectedSegments} + onSelectSegmentsByLabel={selectSegmentsByLabel} + onSelectSegmentsByExpr={selectSegmentsByExpr} + onSelectAllMarkers={selectAllMarkers} + onMutateSegmentsByExpr={mutateSegmentsByExpr} + onLabelSelectedSegments={labelSelectedSegments} updateSegAtIndex={updateSegAtIndex} editingSegmentTags={editingSegmentTags} editingSegmentTagsSegmentIndex={editingSegmentTagsSegmentIndex} diff --git a/src/renderer/src/SegmentList.tsx b/src/renderer/src/SegmentList.tsx index 1746854e..e7711800 100644 --- a/src/renderer/src/SegmentList.tsx +++ b/src/renderer/src/SegmentList.tsx @@ -47,6 +47,7 @@ const Segment = memo(({ onDeselectAllSegments, onSelectSegmentsByLabel, onSelectSegmentsByExpr, + onSelectAllMarkers, onSelectAllSegments, onMutateSegmentsByExpr, jumpSegStart, @@ -66,17 +67,18 @@ const Segment = memo(({ onClick: (i: number) => void, onRemovePress: UseSegments['removeCutSegment'], onRemoveSelected: UseSegments['removeSelectedSegments'], - onLabelSelectedSegments: UseSegments['onLabelSelectedSegments'], + onLabelSelectedSegments: UseSegments['labelSelectedSegments'], onReorderPress: (i: number) => Promise, - onLabelPress: UseSegments['onLabelSegment'], + onLabelPress: UseSegments['labelSegment'], selected: boolean, onSelectSingleSegment: UseSegments['selectOnlySegment'], onToggleSegmentSelected: UseSegments['toggleSegmentSelected'], onDeselectAllSegments: UseSegments['deselectAllSegments'], - onSelectSegmentsByLabel: UseSegments['onSelectSegmentsByLabel'], - onSelectSegmentsByExpr: UseSegments['onSelectSegmentsByExpr'], + onSelectSegmentsByLabel: UseSegments['selectSegmentsByLabel'], + onSelectSegmentsByExpr: UseSegments['selectSegmentsByExpr'], + onSelectAllMarkers: UseSegments['selectAllMarkers'], onSelectAllSegments: UseSegments['selectAllSegments'], - onMutateSegmentsByExpr: UseSegments['onMutateSegmentsByExpr'], + onMutateSegmentsByExpr: UseSegments['mutateSegmentsByExpr'], jumpSegStart: (i: number) => void, jumpSegEnd: (i: number) => void, addSegment: UseSegments['addSegment'], @@ -112,6 +114,7 @@ const Segment = memo(({ { label: t('Select only this segment'), click: () => onSelectSingleSegment(seg) }, { label: t('Select all segments'), click: () => onSelectAllSegments() }, { label: t('Deselect all segments'), click: () => onDeselectAllSegments() }, + { label: t('Select all markers'), click: () => onSelectAllMarkers() }, { label: t('Select segments by label'), click: () => onSelectSegmentsByLabel() }, { label: t('Select segments by expression'), click: () => onSelectSegmentsByExpr() }, { label: t('Invert selected segments'), click: () => onInvertSelectedSegments() }, @@ -133,7 +136,7 @@ const Segment = memo(({ { label: t('Segment tags'), click: () => onEditSegmentTags(index) }, ...(seg.end != null ? [{ label: t('Extract frames as image files'), click: () => onExtractSegmentFramesAsImages([seg as Pick]) }] : []), ]; - }, [invertCutSegments, t, addSegment, onLabelSelectedSegments, onRemoveSelected, updateSegOrder, index, jumpSegStart, jumpSegEnd, onLabelPress, onRemovePress, onDuplicateSegmentClick, seg, onSelectSingleSegment, onSelectAllSegments, onDeselectAllSegments, onSelectSegmentsByLabel, onSelectSegmentsByExpr, onInvertSelectedSegments, onMutateSegmentsByExpr, onReorderPress, onEditSegmentTags, onExtractSegmentFramesAsImages]); + }, [invertCutSegments, t, addSegment, onLabelSelectedSegments, onRemoveSelected, seg, updateSegOrder, index, jumpSegStart, jumpSegEnd, onLabelPress, onRemovePress, onDuplicateSegmentClick, onSelectSingleSegment, onSelectAllSegments, onDeselectAllSegments, onSelectAllMarkers, onSelectSegmentsByLabel, onSelectSegmentsByExpr, onInvertSelectedSegments, onMutateSegmentsByExpr, onReorderPress, onEditSegmentTags, onExtractSegmentFramesAsImages]); useContextMenu(ref, contextMenuTemplate); @@ -252,6 +255,7 @@ function SegmentList({ onSelectSegmentsByLabel, onSelectSegmentsByExpr, onMutateSegmentsByExpr, + onSelectAllMarkers, onExtractSegmentFramesAsImages, onLabelSelectedSegments, onInvertSelectedSegments, @@ -277,7 +281,7 @@ function SegmentList({ addSegment: UseSegments['addSegment'], removeCutSegment: UseSegments['removeCutSegment'], onRemoveSelected: UseSegments['removeSelectedSegments'], - onLabelSegment: UseSegments['onLabelSegment'], + onLabelSegment: UseSegments['labelSegment'], currentCutSeg: UseSegments['currentCutSeg'], segmentAtCursor: StateSegment | undefined, toggleSegmentsList: () => void, @@ -288,11 +292,12 @@ function SegmentList({ onToggleSegmentSelected: UseSegments['toggleSegmentSelected'], onDeselectAllSegments: UseSegments['deselectAllSegments'], onSelectAllSegments: UseSegments['selectAllSegments'], - onSelectSegmentsByLabel: UseSegments['onSelectSegmentsByLabel'], - onSelectSegmentsByExpr: UseSegments['onSelectSegmentsByExpr'], - onMutateSegmentsByExpr: UseSegments['onMutateSegmentsByExpr'], + onSelectSegmentsByLabel: UseSegments['selectSegmentsByLabel'], + onSelectSegmentsByExpr: UseSegments['selectSegmentsByExpr'], + onSelectAllMarkers: UseSegments['selectAllMarkers'], + onMutateSegmentsByExpr: UseSegments['mutateSegmentsByExpr'], onExtractSegmentFramesAsImages: (segments: Pick[]) => Promise, - onLabelSelectedSegments: UseSegments['onLabelSelectedSegments'], + onLabelSelectedSegments: UseSegments['labelSelectedSegments'], onInvertSelectedSegments: UseSegments['invertSelectedSegments'], onDuplicateSegmentClick: UseSegments['duplicateSegment'], jumpSegStart: (index: number) => void, @@ -510,6 +515,7 @@ function SegmentList({ onMutateSegmentsByExpr={onMutateSegmentsByExpr} onExtractSegmentFramesAsImages={onExtractSegmentFramesAsImages} onLabelSelectedSegments={onLabelSelectedSegments} + onSelectAllMarkers={onSelectAllMarkers} onInvertSelectedSegments={onInvertSelectedSegments} onDuplicateSegmentClick={onDuplicateSegmentClick} /> diff --git a/src/renderer/src/components/KeyboardShortcuts.tsx b/src/renderer/src/components/KeyboardShortcuts.tsx index 0bcae313..7345192a 100644 --- a/src/renderer/src/components/KeyboardShortcuts.tsx +++ b/src/renderer/src/components/KeyboardShortcuts.tsx @@ -446,6 +446,10 @@ const KeyboardShortcuts = memo(({ name: t('Select all segments'), category: segmentsAndCutpointsCategory, }, + selectAllMarkers: { + name: t('Select all markers'), + category: segmentsAndCutpointsCategory, + }, toggleCurrentSegmentSelected: { name: t('Toggle current segment selected'), category: segmentsAndCutpointsCategory, diff --git a/src/renderer/src/edlFormats.test.ts b/src/renderer/src/edlFormats.test.ts index 708cc059..a151d2b9 100644 --- a/src/renderer/src/edlFormats.test.ts +++ b/src/renderer/src/edlFormats.test.ts @@ -183,7 +183,7 @@ it('parses xmeml - with multiple tracks', async () => { // see https://github.com/mifi/lossless-cut/issues/1195 it('parses fcpxml 1.9', async () => { - expect(await parseFcpXml(await readFixture('FCPXML_1_9.fcpxml'))).toMatchSnapshot(); + expect(parseFcpXml(await readFixture('FCPXML_1_9.fcpxml'))).toMatchSnapshot(); }); // https://github.com/mifi/lossless-cut/issues/1024 diff --git a/src/renderer/src/edlFormats.ts b/src/renderer/src/edlFormats.ts index 98b21485..03b5ea1a 100644 --- a/src/renderer/src/edlFormats.ts +++ b/src/renderer/src/edlFormats.ts @@ -213,30 +213,15 @@ export function parseCuesheet(cuesheet: ICueSheet) { }); } -// See https://github.com/mifi/lossless-cut/issues/993#issuecomment-1037090403 +// See https://github.com/mifi/lossless-cut/issues/993 export function parsePbf(buf: Buffer) { const text = buf.toString('utf16le'); - const bookmarks = text.split('\n').map((line) => { - const match = line.match(/^\d+=(\d+)\*([^*]+)*([^*]+)?/); - if (match) return { time: parseInt(match[1]!, 10) / 1000, name: match[2] }; - return undefined; - }).filter(Boolean); - - const out: SegmentBase[] = []; - - for (let i = 0; i < bookmarks.length;) { - const bookmark = bookmarks[i]!; - const nextBookmark = bookmarks[i + 1]; - if (!nextBookmark) { - out.push({ start: bookmark.time, end: undefined, name: bookmark.name }); - i += 1; - } else { - out.push({ start: bookmark.time, end: nextBookmark && nextBookmark.time, name: bookmark.name }); - i += nextBookmark.name === ' ' ? 2 : 1; - } - } - return out; + return text.split('\n').flatMap((line) => { + const match = line.match(/^\d+=(\d+)\*([^*]+)*([^*]+)?/); + if (match) return [{ start: parseInt(match[1]!, 10) / 1000, name: match[2] }]; + return []; + }); } // https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/FinalCutPro_XML/VersionsoftheInterchangeFormat/VersionsoftheInterchangeFormat.html diff --git a/src/renderer/src/hooks/useSegments.ts b/src/renderer/src/hooks/useSegments.ts index bc946edf..e85f598a 100644 --- a/src/renderer/src/hooks/useSegments.ts +++ b/src/renderer/src/hooks/useSegments.ts @@ -238,24 +238,37 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea }); }, [cutSegments, duration, haveInvalidSegs]); + // 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 : cutSegments), [cutSegments, selectedSegmentsRaw]); + const invertAllSegments = useCallback(() => { - if (inverseCutSegments.length === 0) { + // treat markers as 0 length + const sortedSegments = sortSegments(selectedSegments.map(({ end, ...rest }) => ({ ...rest, end: end ?? rest.start }))); + + const inverseSegmentsAndMarkers = invertSegments(sortedSegments, true, true, duration); + + if (inverseSegmentsAndMarkers.length === 0) { errorToast(i18n.t('Make sure you have no overlapping segments.')); return; } - // don't reset segColorIndex (which represent colors) when inverting - const newInverseCutSegments = inverseCutSegments.map((inverseSegment, index) => addSegmentColorIndex(createSegment(inverseSegment), index)); + // preserve segColorIndex (which represent colors) when inverting + const newInverseCutSegments = inverseSegmentsAndMarkers.map((inverseSegment, index) => addSegmentColorIndex(createSegment(inverseSegment), index)); setCutSegments(newInverseCutSegments); - }, [inverseCutSegments, setCutSegments]); + }, [duration, selectedSegments, setCutSegments]); const fillSegmentsGaps = useCallback(() => { - if (inverseCutSegments.length === 0) { + // treat markers as 0 length + const sortedSegments = sortSegments(selectedSegments.map(({ end, ...rest }) => ({ ...rest, end: end ?? rest.start }))); + + const inverseSegmentsAndMarkers = invertSegments(sortedSegments, true, true, duration); + + if (inverseSegmentsAndMarkers.length === 0) { errorToast(i18n.t('Make sure you have no overlapping segments.')); return; } - const newInverseCutSegments = inverseCutSegments.map((inverseSegment) => createIndexedSegment({ segment: inverseSegment, incrementCount: true })); + const newInverseCutSegments = inverseSegmentsAndMarkers.map((segment) => createIndexedSegment({ segment, incrementCount: true })); setCutSegments((existing) => ([...existing, ...newInverseCutSegments])); - }, [createIndexedSegment, inverseCutSegments, setCutSegments]); + }, [createIndexedSegment, duration, selectedSegments, setCutSegments]); const combineOverlappingSegments = useCallback(() => { setCutSegments((existingSegments) => combineOverlappingSegments2(existingSegments)); @@ -472,7 +485,7 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea } }, [checkFileOpened, getRelevantTime, setCutTime]); - const onLabelSegment = useCallback(async (index: number) => { + const labelSegment = useCallback(async (index: number) => { const { name } = cutSegments[index]!; const value = await labelSegmentDialog({ currentName: name, maxLength: maxLabelLength }); if (value != null) updateSegAtIndex(index, { name: value }); @@ -558,7 +571,7 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea }); }, [cutSegments.length]); - const onSelectSegmentsByLabel = useCallback(async () => { + const selectSegmentsByLabel = useCallback(async () => { const { name } = currentCutSeg; const value = await selectSegmentsByLabelDialog(name); if (value == null) return; @@ -566,7 +579,11 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea enableSegments(segmentsToEnable); }, [currentCutSeg, cutSegments, enableSegments]); - const onSelectSegmentsByExpr = useCallback(async () => { + const selectAllMarkers = useCallback(() => { + enableSegments(cutSegments.filter((seg) => seg.end == null)); + }, [cutSegments, enableSegments]); + + const selectSegmentsByExpr = useCallback(async () => { const matchSegment = async (seg: StateSegment, index: number, expr: string) => ( (await safeishEval(expr, { segment: getScopeSegment(seg, index) })) === true ); @@ -595,7 +612,7 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea enableSegments(segmentsToEnable); }, [cutSegments, enableSegments, getScopeSegment]); - const onMutateSegmentsByExpr = useCallback(async () => { + const mutateSegmentsByExpr = useCallback(async () => { async function mutateSegment(seg: StateSegment, index: number, expr: string) { const response = (await safeishEval(expr, { segment: getScopeSegment(seg, index) })); invariant(typeof response === 'object' && response != null, i18n.t('The expression must return an object')); @@ -642,7 +659,7 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea safeSetCutSegments(await mutateSegments(value)); }, [cutSegments, getScopeSegment, isSegmentSelected, safeSetCutSegments]); - const onLabelSelectedSegments = useCallback(async () => { + const labelSelectedSegments = useCallback(async () => { const firstSelectedSegment = selectedSegmentsRaw[0]; if (firstSelectedSegment == null) return; const { name } = firstSelectedSegment; @@ -654,9 +671,6 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea })); }, [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 : cutSegments), [cutSegments, selectedSegmentsRaw]); - const selectedSegmentsOrInverse = useMemo<{ start: number, end: number }[]>(() => { // For invertCutSegments we do not support filtering (selecting) segments if (invertCutSegments) return inverseCutSegments; @@ -713,7 +727,7 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea duplicateSegment, setCutStart, setCutEnd, - onLabelSegment, + labelSegment, splitCurrentSegment, focusSegmentAtCursor, createNumSegments, @@ -735,16 +749,17 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea setCurrentSegIndex, setDeselectedSegmentIds, - onLabelSelectedSegments, + labelSelectedSegments, deselectAllSegments, selectAllSegments, selectOnlyCurrentSegment, toggleCurrentSegmentSelected, invertSelectedSegments, removeSelectedSegments, - onSelectSegmentsByLabel, - onSelectSegmentsByExpr, - onMutateSegmentsByExpr, + selectSegmentsByLabel, + selectSegmentsByExpr, + selectAllMarkers, + mutateSegmentsByExpr, toggleSegmentSelected, selectOnlySegment, setCutTime, diff --git a/types.ts b/types.ts index b2a58309..1ec28f17 100644 --- a/types.ts +++ b/types.ts @@ -1,4 +1,4 @@ -export type KeyboardAction = 'addSegment' | 'togglePlayResetSpeed' | 'togglePlayNoResetSpeed' | 'reducePlaybackRate' | 'reducePlaybackRateMore' | 'increasePlaybackRate' | 'increasePlaybackRateMore' | 'timelineToggleComfortZoom' | 'seekPreviousFrame' | 'seekNextFrame' | 'captureSnapshot' | 'setCutStart' | 'setCutEnd' | 'removeCurrentSegment' | 'cleanupFilesDialog' | 'splitCurrentSegment' | 'focusSegmentAtCursor' | 'increaseRotation' | 'goToTimecode' | 'seekBackwards' | 'seekBackwards2' | 'seekBackwards3' | 'seekBackwardsPercent' | 'seekBackwardsPercent' | 'seekBackwardsKeyframe' | 'jumpCutStart' | 'seekForwards' | 'seekForwards2' | 'seekForwards3' | 'seekForwardsPercent' | 'seekForwardsPercent' | 'seekForwardsKeyframe' | 'jumpCutEnd' | 'jumpTimelineStart' | 'jumpTimelineEnd' | 'jumpFirstSegment' | 'jumpPrevSegment' | 'timelineZoomIn' | 'timelineZoomIn' | 'batchPreviousFile' | 'jumpLastSegment' | 'jumpNextSegment' | 'timelineZoomOut' | 'timelineZoomOut' | 'batchNextFile' | 'batchOpenSelectedFile' | 'batchOpenPreviousFile' | 'batchOpenNextFile' | 'undo' | 'undo' | 'redo' | 'redo' | 'copySegmentsToClipboard' | 'copySegmentsToClipboard' | 'toggleFullscreenVideo' | 'labelCurrentSegment' | 'export' | 'toggleKeyboardShortcuts' | 'closeActiveScreen' | 'increaseVolume' | 'decreaseVolume' | 'toggleMuted' | 'detectBlackScenes' | 'detectSilentScenes' | 'detectSceneChanges' | 'toggleLastCommands' | 'play' | 'pause' | 'reloadFile' | 'html5ify' | 'togglePlayOnlyCurrentSegment' | 'toggleLoopOnlyCurrentSegment' | 'toggleLoopStartEndOnlyCurrentSegment' | 'toggleLoopSelectedSegments' | 'editCurrentSegmentTags' | 'duplicateCurrentSegment' | 'reorderSegsByStartTime' | 'invertAllSegments' | 'fillSegmentsGaps' | 'shiftAllSegmentTimes' | 'alignSegmentTimesToKeyframes' | 'createSegmentsFromKeyframes' | 'createFixedDurationSegments' | 'createNumSegments' | 'createFixedByteSizedSegments' | 'createRandomSegments' | 'shuffleSegments' | 'combineOverlappingSegments' | 'combineSelectedSegments' | 'clearSegments' | 'toggleSegmentsList' | 'selectOnlyCurrentSegment' | 'deselectAllSegments' | 'selectAllSegments' | 'toggleCurrentSegmentSelected' | 'invertSelectedSegments' | 'removeSelectedSegments' | 'toggleStreamsSelector' | 'extractAllStreams' | 'showStreamsSelector' | 'showIncludeExternalStreamsDialog' | 'captureSnapshotAsCoverArt' | 'extractCurrentSegmentFramesAsImages' | 'extractSelectedSegmentsFramesAsImages' | 'convertFormatBatch' | 'convertFormatCurrentFile' | 'fixInvalidDuration' | 'closeBatch' | 'concatBatch' | 'toggleKeyframeCutMode' | 'toggleCaptureFormat' | 'toggleStripAudio' | 'toggleStripThumbnail' | 'setStartTimeOffset' | 'toggleWaveformMode' | 'toggleShowThumbnails' | 'toggleShowKeyframes' | 'toggleSettings' | 'openSendReportDialog' | 'openFilesDialog' | 'openDirDialog' | 'exportYouTube' | 'closeCurrentFile' | 'quit'; +export type KeyboardAction = 'addSegment' | 'togglePlayResetSpeed' | 'togglePlayNoResetSpeed' | 'reducePlaybackRate' | 'reducePlaybackRateMore' | 'increasePlaybackRate' | 'increasePlaybackRateMore' | 'timelineToggleComfortZoom' | 'seekPreviousFrame' | 'seekNextFrame' | 'captureSnapshot' | 'setCutStart' | 'setCutEnd' | 'removeCurrentSegment' | 'cleanupFilesDialog' | 'splitCurrentSegment' | 'focusSegmentAtCursor' | 'increaseRotation' | 'goToTimecode' | 'seekBackwards' | 'seekBackwards2' | 'seekBackwards3' | 'seekBackwardsPercent' | 'seekBackwardsPercent' | 'seekBackwardsKeyframe' | 'jumpCutStart' | 'seekForwards' | 'seekForwards2' | 'seekForwards3' | 'seekForwardsPercent' | 'seekForwardsPercent' | 'seekForwardsKeyframe' | 'jumpCutEnd' | 'jumpTimelineStart' | 'jumpTimelineEnd' | 'jumpFirstSegment' | 'jumpPrevSegment' | 'timelineZoomIn' | 'timelineZoomIn' | 'batchPreviousFile' | 'jumpLastSegment' | 'jumpNextSegment' | 'timelineZoomOut' | 'timelineZoomOut' | 'batchNextFile' | 'batchOpenSelectedFile' | 'batchOpenPreviousFile' | 'batchOpenNextFile' | 'undo' | 'undo' | 'redo' | 'redo' | 'copySegmentsToClipboard' | 'copySegmentsToClipboard' | 'toggleFullscreenVideo' | 'labelCurrentSegment' | 'export' | 'toggleKeyboardShortcuts' | 'closeActiveScreen' | 'increaseVolume' | 'decreaseVolume' | 'toggleMuted' | 'detectBlackScenes' | 'detectSilentScenes' | 'detectSceneChanges' | 'toggleLastCommands' | 'play' | 'pause' | 'reloadFile' | 'html5ify' | 'togglePlayOnlyCurrentSegment' | 'toggleLoopOnlyCurrentSegment' | 'toggleLoopStartEndOnlyCurrentSegment' | 'toggleLoopSelectedSegments' | 'editCurrentSegmentTags' | 'duplicateCurrentSegment' | 'reorderSegsByStartTime' | 'invertAllSegments' | 'fillSegmentsGaps' | 'shiftAllSegmentTimes' | 'alignSegmentTimesToKeyframes' | 'createSegmentsFromKeyframes' | 'createFixedDurationSegments' | 'createNumSegments' | 'createFixedByteSizedSegments' | 'createRandomSegments' | 'shuffleSegments' | 'combineOverlappingSegments' | 'combineSelectedSegments' | 'clearSegments' | 'toggleSegmentsList' | 'selectOnlyCurrentSegment' | 'deselectAllSegments' | 'selectAllSegments' | 'toggleCurrentSegmentSelected' | 'invertSelectedSegments' | 'removeSelectedSegments' | 'toggleStreamsSelector' | 'extractAllStreams' | 'showStreamsSelector' | 'showIncludeExternalStreamsDialog' | 'captureSnapshotAsCoverArt' | 'extractCurrentSegmentFramesAsImages' | 'extractSelectedSegmentsFramesAsImages' | 'convertFormatBatch' | 'convertFormatCurrentFile' | 'fixInvalidDuration' | 'closeBatch' | 'concatBatch' | 'toggleKeyframeCutMode' | 'toggleCaptureFormat' | 'toggleStripAudio' | 'toggleStripThumbnail' | 'setStartTimeOffset' | 'toggleWaveformMode' | 'toggleShowThumbnails' | 'toggleShowKeyframes' | 'toggleSettings' | 'openSendReportDialog' | 'openFilesDialog' | 'openDirDialog' | 'exportYouTube' | 'closeCurrentFile' | 'quit' | 'selectAllMarkers'; export interface KeyBinding { keys: string,