From 75f5d3d1bad774c9370b29048ea6570dbc74c8f6 Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Tue, 5 Sep 2023 23:19:16 +0200 Subject: [PATCH] Allow customising merged file name closes #938 see also #916 #96 #1691 --- src/App.jsx | 27 +++++++++++++++++-------- src/components/ConcatDialog.jsx | 2 +- src/components/ExportConfirm.jsx | 17 +++++++++++++++- src/components/MergedOutFileName.jsx | 12 +++++++++++ src/components/OutSegTemplateEditor.jsx | 5 ++--- src/components/TextInput.jsx | 10 +++++++++ src/hooks/useFfmpegOperations.js | 8 ++------ 7 files changed, 62 insertions(+), 19 deletions(-) create mode 100644 src/components/MergedOutFileName.jsx create mode 100644 src/components/TextInput.jsx diff --git a/src/App.jsx b/src/App.jsx index bc74c106..8f85f33e 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -67,7 +67,7 @@ import { getOutPath, getSuffixedOutPath, handleError, getOutDir, isMasBuild, isStoreBuild, dragPreventer, havePermissionToReadFile, resolvePathIfNeeded, getPathReadAccessError, html5ifiedPrefix, html5dummySuffix, findExistingHtml5FriendlyFile, - deleteFiles, isOutOfSpaceError, isExecaFailure, readFileSize, readFileSizes, checkFileSizes, setDocumentTitle, + deleteFiles, isOutOfSpaceError, isExecaFailure, readFileSize, readFileSizes, checkFileSizes, setDocumentTitle, getOutFileExtension, getSuffixedFileName, } from './util'; import { toast, errorToast } from './swal'; import { formatDuration } from './util/duration'; @@ -148,6 +148,7 @@ const App = memo(() => { const [hideCanvasPreview, setHideCanvasPreview] = useState(false); const [exportConfirmVisible, setExportConfirmVisible] = useState(false); const [cacheBuster, setCacheBuster] = useState(0); + const [customMergedOutFileName, setMergedOutFileName] = useState(); const { fileFormat, setFileFormat, detectedFileFormat, setDetectedFileFormat, isCustomFormatSelected } = useFileFormatState(); @@ -731,6 +732,7 @@ const App = memo(() => { setActiveSubtitleStreamIndex(); setHideCanvasPreview(false); setExportConfirmVisible(false); + setMergedOutFileName(); cancelRenderThumbnails(); }, [cutSegmentsHistory, clearSegments, setFileFormat, setDetectedFileFormat, setDeselectedSegmentIds, cancelRenderThumbnails]); @@ -983,7 +985,7 @@ const App = memo(() => { if (sendErrorReport) openSendConcatReportDialogWithState(err, reportState); }, [fileFormat, openSendConcatReportDialogWithState]); - const userConcatFiles = useCallback(async ({ paths, includeAllStreams, streams, fileFormat: outFormat, fileName: outFileName, clearBatchFilesAfterConcat }) => { + const userConcatFiles = useCallback(async ({ paths, includeAllStreams, streams, fileFormat: outFormat, outFileName, clearBatchFilesAfterConcat }) => { if (workingRef.current) return; try { setConcatDialogVisible(false); @@ -1109,6 +1111,16 @@ const App = memo(() => { const willMerge = segmentsToExport.length > 1 && autoMerge; + const mergedOutFileName = useMemo(() => { + if (customMergedOutFileName != null) return customMergedOutFileName; + const ext = getOutFileExtension({ isCustomFormatSelected, outFormat: fileFormat, filePath }); + return getSuffixedFileName(filePath, `cut-merged-${new Date().getTime()}${ext}`); + }, [customMergedOutFileName, fileFormat, filePath, isCustomFormatSelected]); + + const mergedOutFilePath = useMemo(() => ( + getOutPath({ customOutDir, filePath, fileName: mergedOutFileName }) + ), [customOutDir, filePath, mergedOutFileName]); + const onExportConfirm = useCallback(async () => { if (numStreamsToCopy === 0) { errorToast(i18n.t('No tracks selected for export')); @@ -1167,17 +1179,15 @@ const App = memo(() => { detectedFps, }); - let concatOutPath; if (willMerge) { setCutProgress(0); setWorking(i18n.t('Merging')); const chapterNames = segmentsToChapters && !invertCutSegments ? segmentsToExport.map((s) => s.name) : undefined; - concatOutPath = await autoConcatCutSegments({ + await autoConcatCutSegments({ customOutDir, outFormat: fileFormat, - isCustomFormatSelected, segmentPaths: outFiles, ffmpegExperimental, preserveMovData, @@ -1187,6 +1197,7 @@ const App = memo(() => { autoDeleteMergedSegments, preserveMetadataOnMerge, appendFfmpegCommandLog, + mergedOutFilePath, }); } @@ -1217,7 +1228,7 @@ const App = memo(() => { if (areWeCutting) notices.push(i18n.t('Cutpoints may be inaccurate.')); - const revealPath = concatOutPath || outFiles[0]; + const revealPath = willMerge ? mergedOutFilePath : outFiles[0]; if (!hideAllNotifications) openExportFinishedToast({ filePath: revealPath, warnings, notices }); if (cleanupChoices.cleanupAfterExport) await cleanupFiles(cleanupChoices); @@ -1244,7 +1255,7 @@ const App = memo(() => { setWorking(); setCutProgress(); } - }, [numStreamsToCopy, setWorking, segmentsToChaptersOnly, outSegTemplateOrDefault, generateOutSegFileNames, segmentsToExport, getOutSegError, cutMultiple, outputDir, customOutDir, fileFormat, duration, isRotationSet, effectiveRotation, copyFileStreams, allFilesMeta, keyframeCut, shortestFlag, ffmpegExperimental, preserveMovData, preserveMetadataOnMerge, movFastStart, avoidNegativeTs, customTagsByFile, paramsByStreamId, detectedFps, willMerge, enableOverwriteOutput, exportConfirmEnabled, mainFileFormatData, mainStreams, exportExtraStreams, areWeCutting, hideAllNotifications, cleanupChoices, cleanupFiles, selectedSegmentsOrInverse, segmentsToChapters, invertCutSegments, autoConcatCutSegments, isCustomFormatSelected, autoDeleteMergedSegments, nonCopiedExtraStreams, filePath, handleExportFailed]); + }, [numStreamsToCopy, setWorking, segmentsToChaptersOnly, outSegTemplateOrDefault, generateOutSegFileNames, segmentsToExport, getOutSegError, cutMultiple, outputDir, customOutDir, fileFormat, duration, isRotationSet, effectiveRotation, copyFileStreams, allFilesMeta, keyframeCut, shortestFlag, ffmpegExperimental, preserveMovData, preserveMetadataOnMerge, movFastStart, avoidNegativeTs, customTagsByFile, paramsByStreamId, detectedFps, willMerge, enableOverwriteOutput, exportConfirmEnabled, mainFileFormatData, mainStreams, exportExtraStreams, areWeCutting, mergedOutFilePath, hideAllNotifications, cleanupChoices, cleanupFiles, selectedSegmentsOrInverse, segmentsToChapters, invertCutSegments, autoConcatCutSegments, autoDeleteMergedSegments, nonCopiedExtraStreams, filePath, handleExportFailed]); const onExportPress = useCallback(async () => { if (!filePath || workingRef.current || segmentsToExport.length < 1) return; @@ -2483,7 +2494,7 @@ const App = memo(() => { )} - + setFileFormat(newFormat), [setFileFormat]); - const onConcatClick = useCallback(() => onConcat({ paths, includeAllStreams, streams: fileMeta.streams, fileName: outFileName, fileFormat, clearBatchFilesAfterConcat }), [clearBatchFilesAfterConcat, fileFormat, fileMeta, includeAllStreams, onConcat, outFileName, paths]); + const onConcatClick = useCallback(() => onConcat({ paths, includeAllStreams, streams: fileMeta.streams, outFileName, fileFormat, clearBatchFilesAfterConcat }), [clearBatchFilesAfterConcat, fileFormat, fileMeta, includeAllStreams, onConcat, outFileName, paths]); return ( <> diff --git a/src/components/ExportConfirm.jsx b/src/components/ExportConfirm.jsx index 25671a8f..f95cc7d2 100644 --- a/src/components/ExportConfirm.jsx +++ b/src/components/ExportConfirm.jsx @@ -15,6 +15,7 @@ import OutSegTemplateEditor from './OutSegTemplateEditor'; import HighlightedText, { highlightedTextStyle } from './HighlightedText'; import Select from './Select'; import Switch from './Switch'; +import MergedOutFileName from './MergedOutFileName'; import { primaryTextColor } from '../colors'; import { withBlur } from '../util'; @@ -36,7 +37,7 @@ const ExportConfirm = memo(({ areWeCutting, selectedSegments, segmentsToExport, willMerge, visible, onClosePress, onExportConfirm, outFormat, renderOutFmt, outputDir, numStreamsTotal, numStreamsToCopy, onShowStreamsSelectorClick, outSegTemplate, setOutSegTemplate, generateOutSegFileNames, filePath, currentSegIndexSafe, getOutSegError, nonFilteredSegmentsOrInverse, - mainCopiedThumbnailStreams, needSmartCut, + mainCopiedThumbnailStreams, needSmartCut, mergedOutFileName, setMergedOutFileName, }) => { const { t } = useTranslation(); @@ -198,6 +199,20 @@ const ExportConfirm = memo(({ )} + {willMerge && ( + + + {t('Merged output file name:')} + + + + + + showHelpText({ text: t('Name of the merged/concatenated output file when concatenating multiple segments.') })} /> + + + )} + {t('Overwrite existing files')} diff --git a/src/components/MergedOutFileName.jsx b/src/components/MergedOutFileName.jsx new file mode 100644 index 00000000..d2d4ef3b --- /dev/null +++ b/src/components/MergedOutFileName.jsx @@ -0,0 +1,12 @@ +import React, { memo } from 'react'; + +import TextInput from './TextInput'; + + +const MergedOutFileName = memo(({ mergedOutFileName, setMergedOutFileName }) => ( +
+ setMergedOutFileName(e.target.value)} style={{ textAlign: 'right' }} /> +
+)); + +export default MergedOutFileName; diff --git a/src/components/OutSegTemplateEditor.jsx b/src/components/OutSegTemplateEditor.jsx index 2d78c3d6..013fc97e 100644 --- a/src/components/OutSegTemplateEditor.jsx +++ b/src/components/OutSegTemplateEditor.jsx @@ -13,6 +13,7 @@ import { defaultOutSegTemplate, segNumVariable, segSuffixVariable } from '../uti import useUserSettings from '../hooks/useUserSettings'; import Switch from './Switch'; import Select from './Select'; +import TextInput from './TextInput'; const ReactSwal = withReactContent(Swal); @@ -22,8 +23,6 @@ const formatVariable = (variable) => `\${${variable}}`; const extVar = formatVariable('EXT'); -const inputStyle = { flexGrow: 1, fontFamily: 'inherit', fontSize: '.8em', backgroundColor: 'var(--gray3)', color: 'var(--gray12)', border: '1px solid var(--gray7)', appearance: 'none' }; - const OutSegTemplateEditor = memo(({ outSegTemplate, setOutSegTemplate, generateOutSegFileNames, currentSegIndexSafe, getOutSegError }) => { const { safeOutputFileName, toggleSafeOutputFileName, outputFileNameMinZeroPadding, setOutputFileNameMinZeroPadding } = useUserSettings(); @@ -117,7 +116,7 @@ const OutSegTemplateEditor = memo(({ outSegTemplate, setOutSegTemplate, generate exit={{ opacity: 0, height: 0, marginTop: 0 }} >
- + {outSegFileNames != null && } diff --git a/src/components/TextInput.jsx b/src/components/TextInput.jsx new file mode 100644 index 00000000..0b59b04b --- /dev/null +++ b/src/components/TextInput.jsx @@ -0,0 +1,10 @@ +import React, { forwardRef } from 'react'; + +const inputStyle = { borderRadius: '.4em', flexGrow: 1, fontFamily: 'inherit', fontSize: '.8em', backgroundColor: 'var(--gray3)', color: 'var(--gray12)', border: '1px solid var(--gray7)', appearance: 'none' }; + +const TextInput = forwardRef(({ style, ...props }, forwardedRef) => ( + // eslint-disable-next-line react/jsx-props-no-spreading + +)); + +export default TextInput; diff --git a/src/hooks/useFfmpegOperations.js b/src/hooks/useFfmpegOperations.js index 66e07d6f..a1514e8c 100644 --- a/src/hooks/useFfmpegOperations.js +++ b/src/hooks/useFfmpegOperations.js @@ -437,9 +437,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea } }, [concatFiles, cutSingle, filePath, needSmartCut, shouldSkipExistingFile]); - const autoConcatCutSegments = useCallback(async ({ customOutDir, isCustomFormatSelected, outFormat, segmentPaths, ffmpegExperimental, onProgress, preserveMovData, movFastStart, autoDeleteMergedSegments, chapterNames, preserveMetadataOnMerge, appendFfmpegCommandLog }) => { - const ext = getOutFileExtension({ isCustomFormatSelected, outFormat, filePath }); - const outPath = getSuffixedOutPath({ customOutDir, filePath, nameSuffix: `cut-merged-${new Date().getTime()}${ext}` }); + const autoConcatCutSegments = useCallback(async ({ customOutDir, outFormat, segmentPaths, ffmpegExperimental, onProgress, preserveMovData, movFastStart, autoDeleteMergedSegments, chapterNames, preserveMetadataOnMerge, appendFfmpegCommandLog, mergedOutFilePath }) => { const outDir = getOutDir(customOutDir, filePath); const chapters = await createChaptersFromSegments({ segmentPaths, chapterNames }); @@ -447,10 +445,8 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea const metadataFromPath = segmentPaths[0]; // need to re-read streams because may have changed const { streams } = await readFileMeta(metadataFromPath); - await concatFiles({ paths: segmentPaths, outDir, outPath, metadataFromPath, outFormat, includeAllStreams: true, streams, ffmpegExperimental, onProgress, preserveMovData, movFastStart, chapters, preserveMetadataOnMerge, appendFfmpegCommandLog }); + await concatFiles({ paths: segmentPaths, outDir, outPath: mergedOutFilePath, metadataFromPath, outFormat, includeAllStreams: true, streams, ffmpegExperimental, onProgress, preserveMovData, movFastStart, chapters, preserveMetadataOnMerge, appendFfmpegCommandLog }); if (autoDeleteMergedSegments) await tryDeleteFiles(segmentPaths); - - return outPath; }, [concatFiles, filePath]); const html5ify = useCallback(async ({ customOutDir, filePath: filePathArg, speed, hasAudio, hasVideo, onProgress }) => {