diff --git a/src/App.jsx b/src/App.jsx index 81e3fde896f..a5ab0ffcef5 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -123,6 +123,7 @@ const App = memo(() => { const [thumbnails, setThumbnails] = useState([]); const [shortestFlag, setShortestFlag] = useState(false); const [zoomWindowStartTime, setZoomWindowStartTime] = useState(0); + const [enabledSegmentUuids, setSegmentsExportEnabled] = useState(); const [keyframesEnabled, setKeyframesEnabled] = useState(true); const [waveformEnabled, setWaveformEnabled] = useState(false); @@ -312,16 +313,20 @@ const App = memo(() => { || isCuttingStart(currentApparentCutSeg.start) || isCuttingEnd(currentApparentCutSeg.end, duration); - const jumpCutStart = useCallback(() => seekAbs(currentApparentCutSeg.start), [currentApparentCutSeg.start, seekAbs]); - const jumpCutEnd = useCallback(() => seekAbs(currentApparentCutSeg.end), [currentApparentCutSeg.end, seekAbs]); + const jumpSegStart = useCallback((index) => seekAbs(apparentCutSegments[index].start), [apparentCutSegments, seekAbs]); + const jumpSegEnd = useCallback((index) => seekAbs(apparentCutSegments[index].end), [apparentCutSegments, seekAbs]); + const jumpCutStart = useCallback(() => jumpSegStart(currentSegIndexSafe), [currentSegIndexSafe, jumpSegStart]); + const jumpCutEnd = useCallback(() => jumpSegEnd(currentSegIndexSafe), [currentSegIndexSafe, jumpSegEnd]); const sortedCutSegments = useMemo(() => sortSegments(apparentCutSegments), [apparentCutSegments]); const inverseCutSegments = useMemo(() => { - if (haveInvalidSegs) return undefined; - if (!isDurationValid(duration)) return undefined; - - return invertSegments(sortedCutSegments, duration); + function invertSegmentsSafe() { + if (haveInvalidSegs || !isDurationValid(duration)) return undefined; + if (!isDurationValid(duration)) return undefined; + return invertSegments(sortedCutSegments, duration); + } + return invertSegmentsSafe() || []; }, [duration, haveInvalidSegs, sortedCutSegments]); const updateSegAtIndex = useCallback((index, newProps) => { @@ -343,23 +348,20 @@ const App = memo(() => { updateSegAtIndex(currentSegIndexSafe, { [type]: Math.min(Math.max(time, 0), duration) }); }, [currentSegIndexSafe, getSegApparentEnd, currentCutSeg, duration, updateSegAtIndex]); - const setCurrentSegmentName = useCallback((name) => { - updateSegAtIndex(currentSegIndexSafe, { name }); - }, [currentSegIndexSafe, updateSegAtIndex]); + const onLabelSegmentPress = useCallback(async (index) => { + const { name } = cutSegments[index]; + const value = await labelSegmentDialog(name); + if (value != null) updateSegAtIndex(index, { name: value }); + }, [cutSegments, updateSegAtIndex]); - const onLabelSegmentPress = useCallback(async () => { - const value = await labelSegmentDialog(currentCutSeg.name); - if (value != null) setCurrentSegmentName(value); - }, [currentCutSeg, setCurrentSegmentName]); - - const updateCurrentSegOrder = useCallback((newOrder) => { + const updateSegOrder = useCallback((index, newOrder) => { if (newOrder > cutSegments.length - 1 || newOrder < 0) return; const newSegments = [...cutSegments]; - const removedSeg = newSegments.splice(currentSegIndexSafe, 1)[0]; + const removedSeg = newSegments.splice(index, 1)[0]; newSegments.splice(newOrder, 0, removedSeg); setCutSegments(newSegments); setCurrentSegIndex(newOrder); - }, [currentSegIndexSafe, cutSegments, setCutSegments]); + }, [cutSegments, setCutSegments]); const reorderSegsByStartTime = useCallback(() => { setCutSegments(sortBy(cutSegments, getSegApparentStart)); @@ -636,7 +638,7 @@ const App = memo(() => { }); }, [copyAnyAudioTrack, filePath, mainStreams]); - const removeCutSegment = useCallback(() => { + const removeCutSegment = useCallback((index) => { if (cutSegments.length === 1 && cutSegments[0].start == null && cutSegments[0].end == null) return; // Initial segment if (cutSegments.length <= 1) { @@ -645,10 +647,10 @@ const App = memo(() => { } const cutSegmentsNew = [...cutSegments]; - cutSegmentsNew.splice(currentSegIndexSafe, 1); + cutSegmentsNew.splice(index, 1); setCutSegments(cutSegmentsNew); - }, [currentSegIndexSafe, cutSegments, setCutSegments]); + }, [cutSegments, setCutSegments]); const clearSegments = useCallback(() => { setCutSegments(createInitialCutSegments()); @@ -768,6 +770,7 @@ const App = memo(() => { setZoom(1); setShortestFlag(false); setZoomWindowStartTime(0); + setSegmentsExportEnabled(); setHideCanvasPreview(false); setExportConfirmVisible(false); @@ -898,7 +901,19 @@ const App = memo(() => { const outSegments = useMemo(() => (invertCutSegments ? inverseCutSegments : apparentCutSegments), [invertCutSegments, inverseCutSegments, apparentCutSegments]); - const generateOutSegFileNames = useCallback(({ segments = outSegments, template }) => ( + // enabledSegmentUuids undefined means all are enabled + const enabledSegmentUuidsEffective = enabledSegmentUuids || Object.fromEntries(cutSegments.map((s) => [s.uuid, true])); + // For invertCutSegments we do not support filtering + const enabledOutSegmentsRaw = useMemo(() => (invertCutSegments ? outSegments : outSegments.filter((s) => enabledSegmentUuidsEffective[s.uuid])), [outSegments, invertCutSegments, enabledSegmentUuidsEffective]); + // If user has selected none to export, it makes no sense, so export all instead + const enabledOutSegments = enabledOutSegmentsRaw.length > 0 ? enabledOutSegmentsRaw : outSegments; + + const onExportSingleSegmentClick = useCallback((seg) => setSegmentsExportEnabled({ [seg.uuid]: true }), []); + const onExportSegmentEnabledToggle = useCallback((seg) => setSegmentsExportEnabled({ ...enabledSegmentUuidsEffective, [seg.uuid]: !enabledSegmentUuidsEffective[seg.uuid] }), [enabledSegmentUuidsEffective]); + const onExportSegmentDisableAll = useCallback(() => setSegmentsExportEnabled({}), []); + const onExportSegmentEnableAll = useCallback(() => setSegmentsExportEnabled(), []); + + const generateOutSegFileNames = useCallback(({ segments = enabledOutSegments, template }) => ( segments.map(({ start, end, name = '' }, i) => { const cutFromStr = formatDuration({ seconds: start, fileNameFriendly: true }); const cutToStr = formatDuration({ seconds: end, fileNameFriendly: true }); @@ -916,7 +931,7 @@ const App = memo(() => { const generated = generateSegFileName({ template, segSuffix, inputFileNameWithoutExt: fileNameWithoutExt, ext, segNum, segLabel: filenamify(name), cutFrom: cutFromStr, cutTo: cutToStr }); return generated.substr(0, 200); // Just to be sure }) - ), [fileFormat, filePath, isCustomFormatSelected, outSegments]); + ), [fileFormat, filePath, isCustomFormatSelected, enabledOutSegments]); // TODO improve user feedback const isOutSegFileNamesValid = useCallback((fileNames) => fileNames.every((fileName) => { @@ -953,7 +968,7 @@ const App = memo(() => { const closeExportConfirm = useCallback(() => setExportConfirmVisible(false), []); - const onExportConfirm = useCallback(async ({ exportSingle } = {}) => { + const onExportConfirm = useCallback(async () => { if (working) return; if (numStreamsToCopy === 0) { @@ -964,17 +979,15 @@ const App = memo(() => { setStreamsSelectorShown(false); setExportConfirmVisible(false); - const filteredOutSegments = exportSingle ? [outSegments[currentSegIndexSafe]] : outSegments; - try { setWorking(i18n.t('Exporting')); console.log('outSegTemplateOrDefault', outSegTemplateOrDefault); - let outSegFileNames = generateOutSegFileNames({ segments: filteredOutSegments, template: outSegTemplateOrDefault }); + let outSegFileNames = generateOutSegFileNames({ segments: enabledOutSegments, template: outSegTemplateOrDefault }); if (!isOutSegFileNamesValid(outSegFileNames) || hasDuplicates(outSegFileNames)) { console.error('Output segments file name invalid, using default instead', outSegFileNames); - outSegFileNames = generateOutSegFileNames({ segments: filteredOutSegments, template: defaultOutSegTemplate }); + outSegFileNames = generateOutSegFileNames({ segments: enabledOutSegments, template: defaultOutSegTemplate }); } // throw (() => { const err = new Error('test'); err.code = 'ENOENT'; return err; })(); @@ -985,7 +998,7 @@ const App = memo(() => { rotation: isRotationSet ? effectiveRotation : undefined, copyFileStreams, keyframeCut, - segments: filteredOutSegments, + segments: enabledOutSegments, segmentsFileNames: outSegFileNames, onProgress: setCutProgress, appendFfmpegCommandLog, @@ -1002,7 +1015,7 @@ const App = memo(() => { setCutProgress(0); setWorking(i18n.t('Merging')); - const chapterNames = segmentsToChapters && !invertCutSegments && outSegments ? outSegments.map((s) => s.name) : undefined; + const chapterNames = segmentsToChapters && !invertCutSegments ? enabledOutSegments.map((s) => s.name) : undefined; await autoMergeSegments({ customOutDir, @@ -1019,7 +1032,7 @@ const App = memo(() => { }); } - if (exportExtraStreams && !exportSingle) { + if (exportExtraStreams && enabledOutSegments.length > 1) { try { await extractStreams({ filePath, customOutDir, streams: nonCopiedExtraStreams }); } catch (err) { @@ -1051,7 +1064,7 @@ const App = memo(() => { setWorking(); setCutProgress(); } - }, [working, numStreamsToCopy, outSegments, currentSegIndexSafe, outSegTemplateOrDefault, generateOutSegFileNames, customOutDir, filePath, fileFormat, duration, isRotationSet, effectiveRotation, copyFileStreams, keyframeCut, shortestFlag, ffmpegExperimental, preserveMovData, movFastStart, avoidNegativeTs, customTagsByFile, customTagsByStreamId, autoMerge, exportExtraStreams, fileFormatData, mainStreams, hideAllNotifications, outputDir, segmentsToChapters, invertCutSegments, isCustomFormatSelected, autoDeleteMergedSegments, preserveMetadataOnMerge, nonCopiedExtraStreams, handleCutFailed, isOutSegFileNamesValid, cutMultiple, autoMergeSegments]); + }, [working, numStreamsToCopy, enabledOutSegments, outSegTemplateOrDefault, generateOutSegFileNames, customOutDir, filePath, fileFormat, duration, isRotationSet, effectiveRotation, copyFileStreams, keyframeCut, shortestFlag, ffmpegExperimental, preserveMovData, movFastStart, avoidNegativeTs, customTagsByFile, customTagsByStreamId, autoMerge, exportExtraStreams, fileFormatData, mainStreams, hideAllNotifications, outputDir, segmentsToChapters, invertCutSegments, isCustomFormatSelected, autoDeleteMergedSegments, preserveMetadataOnMerge, nonCopiedExtraStreams, handleCutFailed, isOutSegFileNamesValid, cutMultiple, autoMergeSegments]); const onExportPress = useCallback(async () => { if (working || !filePath) return; @@ -1061,14 +1074,14 @@ const App = memo(() => { return; } - if (!outSegments || outSegments.length < 1) { + if (enabledOutSegments.length < 1) { errorToast(i18n.t('No segments to export')); return; } if (exportConfirmEnabled) setExportConfirmVisible(true); else await onExportConfirm(); - }, [working, filePath, haveInvalidSegs, outSegments, exportConfirmEnabled, onExportConfirm]); + }, [working, filePath, haveInvalidSegs, enabledOutSegments, exportConfirmEnabled, onExportConfirm]); const capture = useCallback(async () => { if (!filePath) return; @@ -1352,7 +1365,7 @@ const App = memo(() => { mousetrap.bind('c', () => capture()); mousetrap.bind('i', () => setCutStart()); mousetrap.bind('o', () => setCutEnd()); - mousetrap.bind('backspace', () => removeCutSegment()); + mousetrap.bind('backspace', () => removeCutSegment(currentSegIndexSafe)); mousetrap.bind('d', () => cleanupFiles()); mousetrap.bind('b', () => splitCurrentSegment()); mousetrap.bind('r', () => increaseRotation()); @@ -1386,7 +1399,7 @@ const App = memo(() => { }); mousetrap.bind(['enter'], () => { - onLabelSegmentPress(); + onLabelSegmentPress(currentSegIndexSafe); return false; }); @@ -1396,7 +1409,7 @@ const App = memo(() => { setCutEnd, setCutStart, seekRel, seekRelPercent, shortStep, cleanupFiles, jumpSeg, seekClosestKeyframe, zoomRel, toggleComfortZoom, splitCurrentSegment, exportConfirmVisible, increaseRotation, jumpCutStart, jumpCutEnd, cutSegmentsHistory, keyboardSeekAccFactor, - keyboardNormalSeekSpeed, onLabelSegmentPress, + keyboardNormalSeekSpeed, onLabelSegmentPress, currentSegIndexSafe, ]); useEffect(() => { @@ -2060,7 +2073,7 @@ const App = memo(() => { numStreamsToCopy={numStreamsToCopy} numStreamsTotal={numStreamsTotal} setStreamsSelectorShown={setStreamsSelectorShown} - outSegments={outSegments} + enabledOutSegments={enabledOutSegments} autoMerge={autoMerge} setAutoMerge={setAutoMerge} autoDeleteMergedSegments={autoDeleteMergedSegments} @@ -2169,6 +2182,7 @@ const App = memo(() => { exit={{ x: sideBarWidth }} > { formatTimecode={formatTimecode} invertCutSegments={invertCutSegments} onSegClick={setCurrentSegIndex} - updateCurrentSegOrder={updateCurrentSegOrder} + updateSegOrder={updateSegOrder} onLabelSegmentPress={onLabelSegmentPress} currentCutSeg={currentCutSeg} segmentAtCursor={segmentAtCursor} @@ -2184,6 +2198,14 @@ const App = memo(() => { removeCutSegment={removeCutSegment} toggleSideBar={toggleSideBar} splitCurrentSegment={splitCurrentSegment} + enabledOutSegmentsRaw={enabledOutSegmentsRaw} + enabledOutSegments={enabledOutSegments} + onExportSingleSegmentClick={onExportSingleSegmentClick} + onExportSegmentEnabledToggle={onExportSegmentEnabledToggle} + onExportSegmentDisableAll={onExportSegmentDisableAll} + onExportSegmentEnableAll={onExportSegmentEnableAll} + jumpSegStart={jumpSegStart} + jumpSegEnd={jumpSegEnd} /> )} @@ -2278,7 +2300,7 @@ const App = memo(() => { renderCaptureFormatButton={renderCaptureFormatButton} capture={capture} onExportPress={onExportPress} - outSegments={outSegments} + enabledOutSegments={enabledOutSegments} exportConfirmEnabled={exportConfirmEnabled} toggleExportConfirmEnabled={toggleExportConfirmEnabled} simpleMode={simpleMode} @@ -2286,7 +2308,7 @@ const App = memo(() => { - + ; const ExportConfirm = memo(({ - autoMerge, areWeCutting, outSegments, visible, onClosePress, onExportConfirm, keyframeCut, toggleKeyframeCut, + autoMerge, areWeCutting, enabledOutSegments, visible, onClosePress, onExportConfirm, keyframeCut, toggleKeyframeCut, setAutoMerge, renderOutFmt, preserveMovData, togglePreserveMovData, movFastStart, toggleMovFastStart, avoidNegativeTs, setAvoidNegativeTs, - changeOutDir, outputDir, numStreamsTotal, numStreamsToCopy, setStreamsSelectorShown, currentSegIndex, invertCutSegments, + changeOutDir, outputDir, numStreamsTotal, numStreamsToCopy, setStreamsSelectorShown, exportConfirmEnabled, toggleExportConfirmEnabled, segmentsToChapters, toggleSegmentsToChapters, outFormat, preserveMetadataOnMerge, togglePreserveMetadataOnMerge, outSegTemplate, setOutSegTemplate, generateOutSegFileNames, filePath, currentSegIndexSafe, isOutSegFileNamesValid, autoDeleteMergedSegments, setAutoDeleteMergedSegments, @@ -96,11 +94,6 @@ const ExportConfirm = memo(({ toast.fire({ icon: 'info', timer: 10000, text: `${avoidNegativeTs}: ${texts[avoidNegativeTs]}` }); } - function getCurrentSegColor() { - const { segBgColor } = getSegColors(outSegments[currentSegIndex]); - return segBgColor; - } - const outSegTemplateHelpIcon = ; // https://stackoverflow.com/questions/33454533/cant-scroll-to-top-of-flex-item-that-is-overflowing-container @@ -119,7 +112,7 @@ const ExportConfirm = memo(({

{t('Export options')}

    - {outSegments.length >= 2 &&
  • {t('Merge {{segments}} cut segments to one file?', { segments: outSegments.length })}
  • } + {enabledOutSegments.length >= 2 &&
  • {t('Merge {{segments}} cut segments to one file?', { segments: enabledOutSegments.length })}
  • }
  • {t('Output container format:')} {renderOutFmt({ height: 20, maxWidth: 150 })} @@ -131,7 +124,7 @@ const ExportConfirm = memo(({
  • {t('Save output to path:')} {outputDir}
  • - {(outSegments.length === 1 || !autoMerge) && ( + {(enabledOutSegments.length === 1 || !autoMerge) && (
  • @@ -140,7 +133,7 @@ const ExportConfirm = memo(({

    {t('Advanced options')}

    - {autoMerge && outSegments.length >= 2 && ( + {autoMerge && enabledOutSegments.length >= 2 && (
    • {t('Create chapters from merged segments? (slow)')} @@ -196,19 +189,12 @@ const ExportConfirm = memo(({ transition={{ duration: 0.4, easings: ['easeOut'] }} style={{ display: 'flex', alignItems: 'flex-end' }} > -
      {t('Show this page before exporting?')}
      - - {outSegments.length > 1 && !invertCutSegments && ( -
      onExportConfirm({ exportSingle: true })} style={{ cursor: 'pointer', background: getCurrentSegColor(), borderRadius: 5, padding: '3px 10px', fontSize: 13, marginRight: 10 }}> - - {t('Export seg {{segNum}}', { segNum: currentSegIndex + 1 })} -
      - )} - onExportConfirm()} size={1.7} /> + onExportConfirm()} size={1.7} />
diff --git a/src/RightMenu.jsx b/src/RightMenu.jsx index 1841a504c22..6c79387b757 100644 --- a/src/RightMenu.jsx +++ b/src/RightMenu.jsx @@ -12,7 +12,7 @@ import ToggleExportConfirm from './components/ToggleExportConfirm'; const RightMenu = memo(({ isRotationSet, rotation, areWeCutting, increaseRotation, cleanupFiles, renderCaptureFormatButton, - capture, onExportPress, outSegments, hasVideo, autoMerge, exportConfirmEnabled, toggleExportConfirmEnabled, + capture, onExportPress, enabledOutSegments, hasVideo, autoMerge, exportConfirmEnabled, toggleExportConfirmEnabled, simpleMode, }) => { const rotationStr = `${rotation}°`; @@ -59,7 +59,7 @@ const RightMenu = memo(({ {!simpleMode && } - + ); }); diff --git a/src/SegmentList.jsx b/src/SegmentList.jsx index ebee1b5ec88..15de414843b 100644 --- a/src/SegmentList.jsx +++ b/src/SegmentList.jsx @@ -1,10 +1,11 @@ -import React, { memo } from 'react'; +import React, { memo, useMemo, useRef } from 'react'; import prettyMs from 'pretty-ms'; -import { FaSave, FaPlus, FaMinus, FaTag, FaSortNumericDown, FaAngleRight, FaArrowCircleUp, FaArrowCircleDown } from 'react-icons/fa'; +import { FaSave, FaPlus, FaMinus, FaTag, FaSortNumericDown, FaAngleRight, FaArrowCircleUp, FaArrowCircleDown, FaCheck, FaTimes } from 'react-icons/fa'; import { AiOutlineSplitCells } from 'react-icons/ai'; import { motion } from 'framer-motion'; import Swal from 'sweetalert2'; import { useTranslation } from 'react-i18next'; +import useContextMenu from './hooks/useContextMenu'; import { saveColor } from './colors'; import { getSegColors } from './util/colors'; @@ -16,9 +17,35 @@ const buttonBaseStyle = { const neutralButtonColor = 'rgba(255, 255, 255, 0.2)'; -const Segment = memo(({ seg, index, currentSegIndex, formatTimecode, getFrameCount, segOrderDecrease, segOrderIncrease, invertCutSegments, onClick }) => { +const Segment = memo(({ seg, index, currentSegIndex, formatTimecode, getFrameCount, updateOrder, invertCutSegments, onClick, onRemovePress, onReorderPress, onLabelPress, enabled, onExportSingleSegmentClick, onExportSegmentEnabledToggle, onExportSegmentDisableAll, onExportSegmentEnableAll, jumpSegStart, jumpSegEnd, addCutSegment }) => { const { t } = useTranslation(); + const ref = useRef(); + + useContextMenu(ref, invertCutSegments ? [] : [ + { label: t('Jump to cut start'), click: jumpSegStart }, + { label: t('Jump to cut end'), click: jumpSegEnd }, + + { type: 'separator' }, + + { label: t('Add segment'), click: addCutSegment }, + { label: t('Label segment'), click: onLabelPress }, + { label: t('Remove segment'), click: onRemovePress }, + + { type: 'separator' }, + + { label: t('Change segment order'), click: onReorderPress }, + { label: t('Increase segment order'), click: () => updateOrder(1) }, + { label: t('Decrease segment order'), click: () => updateOrder(-1) }, + + { type: 'separator' }, + + { label: t('Include ONLY this segment in export'), click: () => onExportSingleSegmentClick(seg) }, + { label: enabled ? t('Exclude this segment from export') : t('Include this segment in export'), click: () => onExportSegmentEnabledToggle(seg) }, + { label: t('Include all segments in export'), click: () => onExportSegmentEnableAll(seg) }, + { label: t('Exclude all segments from export'), click: () => onExportSegmentDisableAll(seg) }, + ]); + const duration = seg.end - seg.start; const durationMs = duration * 1000; @@ -27,28 +54,46 @@ const Segment = memo(({ seg, index, currentSegIndex, formatTimecode, getFrameCou function renderNumber() { if (invertCutSegments) return ; - const { - segBgColor, segBorderColor, - } = getSegColors(seg); + const { segBgColor, segBorderColor } = getSegColors(seg); - return {index + 1}; + return {index + 1}; } - const timeStr = `${formatTimecode(seg.start)} - ${formatTimecode(seg.end)}`; + const timeStr = useMemo(() => `${formatTimecode(seg.start)} - ${formatTimecode(seg.end)}`, [seg.start, seg.end, formatTimecode]); + + function onSegOrderDecreasePress(e) { + updateOrder(-1); + e.stopPropagation(); + } + function onSegOrderIncreasePress(e) { + updateOrder(1); + e.stopPropagation(); + } + + function onDoubleClick() { + if (invertCutSegments) return; + if (!enabled) { + onExportSegmentEnabledToggle(seg); + return; + } + jumpSegStart(); + } return ( !invertCutSegments && onClick(index)} + onDoubleClick={onDoubleClick} positionTransition - style={{ originY: 0, margin: '5px 0', border: `1px solid rgba(255,255,255,${isActive ? 1 : 0.3})`, padding: 5, borderRadius: 5, position: 'relative' }} + style={{ originY: 0, margin: '5px 0', border: `1px solid rgba(255,255,255,${isActive ? 1 : 0.3})`, padding: 5, borderRadius: 5, position: 'relative', opacity: !enabled && !invertCutSegments ? 0.5 : undefined }} initial={{ scaleY: 0 }} animate={{ scaleY: 1 }} exit={{ scaleY: 0 }} > -
+
{renderNumber()} - {timeStr} + {timeStr}
{seg.name}
@@ -60,10 +105,15 @@ const Segment = memo(({ seg, index, currentSegIndex, formatTimecode, getFrameCou {isActive && ( - - + + )} + {!enabled && !invertCutSegments && ( +
+ +
+ )} ); }); @@ -71,23 +121,26 @@ const Segment = memo(({ seg, index, currentSegIndex, formatTimecode, getFrameCou const SegmentList = memo(({ formatTimecode, cutSegments, outSegments, getFrameCount, onSegClick, currentSegIndex, invertCutSegments, - updateCurrentSegOrder, addCutSegment, removeCutSegment, + updateSegOrder, addCutSegment, removeCutSegment, onLabelSegmentPress, currentCutSeg, segmentAtCursor, toggleSideBar, splitCurrentSegment, + enabledOutSegments, enabledOutSegmentsRaw, onExportSingleSegmentClick, onExportSegmentEnabledToggle, onExportSegmentDisableAll, onExportSegmentEnableAll, + jumpSegStart, jumpSegEnd, simpleMode, }) => { const { t } = useTranslation(); let headerText = t('Segments to export:'); + if (outSegments.length === 0) { + if (invertCutSegments) headerText = t('Make sure you have no overlapping segments.'); + else headerText = t('No segments to export.'); + } - if (!outSegments && invertCutSegments) headerText = t('Make sure you have no overlapping segments.'); - else if (!outSegments || outSegments.length === 0) headerText = t('No segments to export.'); - - async function onReorderSegsPress() { + async function onReorderSegsPress(index) { if (cutSegments.length < 2) return; const { value } = await Swal.fire({ - title: `${t('Change order of segment')} ${currentSegIndex + 1}`, + title: `${t('Change order of segment')} ${index + 1}`, text: `Please enter a number from 1 to ${cutSegments.length} to be the new order for the current segment`, input: 'text', - inputValue: currentSegIndex + 1, + inputValue: index + 1, showCancelButton: true, inputValidator: (v) => { const parsed = parseInt(v, 10); @@ -97,28 +150,26 @@ const SegmentList = memo(({ if (value) { const newOrder = parseInt(value, 10); - updateCurrentSegOrder(newOrder - 1); + updateSegOrder(index, newOrder - 1); } } - function segOrderDecrease(e) { - updateCurrentSegOrder(currentSegIndex - 1); - e.stopPropagation(); - } - function segOrderIncrease(e) { - updateCurrentSegOrder(currentSegIndex + 1); - e.stopPropagation(); - } - const renderFooter = () => { const { segActiveBgColor: currentSegActiveBgColor } = getSegColors(currentCutSeg); const { segActiveBgColor: segmentAtCursorActiveBgColor } = getSegColors(segmentAtCursor); + function renderExportEnabledCheckBox() { + const segmentExportEnabled = currentCutSeg && enabledOutSegmentsRaw.some((s) => s.uuid === currentCutSeg.uuid); + const Icon = segmentExportEnabled ? FaCheck : FaTimes; + + return onExportSegmentEnabledToggle(currentCutSeg)} />; + } + return ( <>
= 2 ? currentSegActiveBgColor : neutralButtonColor }} role="button" - title={`${t('Delete current segment')} ${currentSegIndex + 1}`} - onClick={removeCutSegment} + title={`${t('Remove segment')} ${currentSegIndex + 1}`} + onClick={() => removeCutSegment(currentSegIndex)} /> - + {!invertCutSegments && !simpleMode && ( + <> + onReorderSegsPress(currentSegIndex)} + /> - + onLabelSegmentPress(currentSegIndex)} + /> + + {renderExportEnabledCheckBox()} + + )}
-
+
{t('Segments total:')}
-
{formatTimecode(outSegments.reduce((acc, { start, end }) => (end - start) + acc, 0))}
+
{formatTimecode(enabledOutSegments.reduce((acc, { start, end }) => (end - start) + acc, 0))}
); @@ -181,13 +238,37 @@ const SegmentList = memo(({ {headerText}
- {outSegments && outSegments.map((seg, index) => { + {outSegments.map((seg, index) => { const id = seg.uuid || `${seg.start}`; - return ; + const enabled = !invertCutSegments && enabledOutSegmentsRaw.includes(seg); + return ( + removeCutSegment(index)} + onReorderPress={() => onReorderSegsPress(index)} + onLabelPress={() => onLabelSegmentPress(index)} + jumpSegStart={() => jumpSegStart(index)} + jumpSegEnd={() => jumpSegEnd(index)} + updateOrder={(dir) => updateSegOrder(index, index + dir)} + getFrameCount={getFrameCount} + formatTimecode={formatTimecode} + currentSegIndex={currentSegIndex} + invertCutSegments={invertCutSegments} + onExportSingleSegmentClick={onExportSingleSegmentClick} + onExportSegmentEnabledToggle={onExportSegmentEnabledToggle} + onExportSegmentDisableAll={onExportSegmentDisableAll} + onExportSegmentEnableAll={onExportSegmentEnableAll} + /> + ); })}
- {outSegments && renderFooter()} + {outSegments.length > 0 && renderFooter()} ); }); diff --git a/src/Timeline.jsx b/src/Timeline.jsx index c055a7010be..a4f11287842 100644 --- a/src/Timeline.jsx +++ b/src/Timeline.jsx @@ -249,7 +249,7 @@ const Timeline = memo(({ ); })} - {inverseCutSegments && inverseCutSegments.map((seg) => ( + {inverseCutSegments.map((seg) => ( { const { t } = useTranslation(); @@ -47,7 +47,7 @@ const TopMenu = memo(({ {filePath && renderOutFmt({ height: 20, maxWidth: 100 })} - {filePath && } + {filePath && } diff --git a/src/components/ExportButton.jsx b/src/components/ExportButton.jsx index 58f42c198a8..5a1bb6d2023 100644 --- a/src/components/ExportButton.jsx +++ b/src/components/ExportButton.jsx @@ -6,21 +6,19 @@ import { useTranslation } from 'react-i18next'; import { primaryColor } from '../colors'; -const ExportButton = memo(({ outSegments, areWeCutting, autoMerge, onClick, size = 1 }) => { +const ExportButton = memo(({ enabledOutSegments, areWeCutting, autoMerge, onClick, size = 1 }) => { const CutIcon = areWeCutting ? FiScissors : FaFileExport; const { t } = useTranslation(); let exportButtonTitle = t('Export'); - if (outSegments) { - if (outSegments.length === 1) { - exportButtonTitle = t('Export selection'); - } else if (outSegments.length > 1) { - exportButtonTitle = t('Export {{ num }} segments', { num: outSegments.length }); - } + if (enabledOutSegments.length === 1) { + exportButtonTitle = t('Export selection'); + } else if (enabledOutSegments.length > 1) { + exportButtonTitle = t('Export {{ num }} segments', { num: enabledOutSegments.length }); } - const exportButtonText = autoMerge && outSegments && outSegments.length > 1 ? t('Export+merge') : t('Export'); + const exportButtonText = autoMerge && enabledOutSegments && enabledOutSegments.length > 1 ? t('Export+merge') : t('Export'); return (
{ +const MergeExportButton = memo(({ autoMerge, enabledOutSegments, setAutoMerge, autoDeleteMergedSegments, setAutoDeleteMergedSegments }) => { const { t } = useTranslation(); let AutoMergeIcon; @@ -52,7 +52,7 @@ const MergeExportButton = memo(({ autoMerge, outSegments, setAutoMerge, autoDele return (