1
0
mirror of https://github.com/mifi/lossless-cut.git synced 2024-11-25 11:43:17 +01:00

Allow customising merged file name

closes #938

see also #916 #96 #1691
This commit is contained in:
Mikael Finstad 2023-09-05 23:19:16 +02:00
parent b20596e53a
commit 75f5d3d1ba
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26
7 changed files with 62 additions and 19 deletions

View File

@ -67,7 +67,7 @@ import {
getOutPath, getSuffixedOutPath, handleError, getOutDir, getOutPath, getSuffixedOutPath, handleError, getOutDir,
isMasBuild, isStoreBuild, dragPreventer, isMasBuild, isStoreBuild, dragPreventer,
havePermissionToReadFile, resolvePathIfNeeded, getPathReadAccessError, html5ifiedPrefix, html5dummySuffix, findExistingHtml5FriendlyFile, havePermissionToReadFile, resolvePathIfNeeded, getPathReadAccessError, html5ifiedPrefix, html5dummySuffix, findExistingHtml5FriendlyFile,
deleteFiles, isOutOfSpaceError, isExecaFailure, readFileSize, readFileSizes, checkFileSizes, setDocumentTitle, deleteFiles, isOutOfSpaceError, isExecaFailure, readFileSize, readFileSizes, checkFileSizes, setDocumentTitle, getOutFileExtension, getSuffixedFileName,
} from './util'; } from './util';
import { toast, errorToast } from './swal'; import { toast, errorToast } from './swal';
import { formatDuration } from './util/duration'; import { formatDuration } from './util/duration';
@ -148,6 +148,7 @@ const App = memo(() => {
const [hideCanvasPreview, setHideCanvasPreview] = useState(false); const [hideCanvasPreview, setHideCanvasPreview] = useState(false);
const [exportConfirmVisible, setExportConfirmVisible] = useState(false); const [exportConfirmVisible, setExportConfirmVisible] = useState(false);
const [cacheBuster, setCacheBuster] = useState(0); const [cacheBuster, setCacheBuster] = useState(0);
const [customMergedOutFileName, setMergedOutFileName] = useState();
const { fileFormat, setFileFormat, detectedFileFormat, setDetectedFileFormat, isCustomFormatSelected } = useFileFormatState(); const { fileFormat, setFileFormat, detectedFileFormat, setDetectedFileFormat, isCustomFormatSelected } = useFileFormatState();
@ -731,6 +732,7 @@ const App = memo(() => {
setActiveSubtitleStreamIndex(); setActiveSubtitleStreamIndex();
setHideCanvasPreview(false); setHideCanvasPreview(false);
setExportConfirmVisible(false); setExportConfirmVisible(false);
setMergedOutFileName();
cancelRenderThumbnails(); cancelRenderThumbnails();
}, [cutSegmentsHistory, clearSegments, setFileFormat, setDetectedFileFormat, setDeselectedSegmentIds, cancelRenderThumbnails]); }, [cutSegmentsHistory, clearSegments, setFileFormat, setDetectedFileFormat, setDeselectedSegmentIds, cancelRenderThumbnails]);
@ -983,7 +985,7 @@ const App = memo(() => {
if (sendErrorReport) openSendConcatReportDialogWithState(err, reportState); if (sendErrorReport) openSendConcatReportDialogWithState(err, reportState);
}, [fileFormat, openSendConcatReportDialogWithState]); }, [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; if (workingRef.current) return;
try { try {
setConcatDialogVisible(false); setConcatDialogVisible(false);
@ -1109,6 +1111,16 @@ const App = memo(() => {
const willMerge = segmentsToExport.length > 1 && autoMerge; 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 () => { const onExportConfirm = useCallback(async () => {
if (numStreamsToCopy === 0) { if (numStreamsToCopy === 0) {
errorToast(i18n.t('No tracks selected for export')); errorToast(i18n.t('No tracks selected for export'));
@ -1167,17 +1179,15 @@ const App = memo(() => {
detectedFps, detectedFps,
}); });
let concatOutPath;
if (willMerge) { if (willMerge) {
setCutProgress(0); setCutProgress(0);
setWorking(i18n.t('Merging')); setWorking(i18n.t('Merging'));
const chapterNames = segmentsToChapters && !invertCutSegments ? segmentsToExport.map((s) => s.name) : undefined; const chapterNames = segmentsToChapters && !invertCutSegments ? segmentsToExport.map((s) => s.name) : undefined;
concatOutPath = await autoConcatCutSegments({ await autoConcatCutSegments({
customOutDir, customOutDir,
outFormat: fileFormat, outFormat: fileFormat,
isCustomFormatSelected,
segmentPaths: outFiles, segmentPaths: outFiles,
ffmpegExperimental, ffmpegExperimental,
preserveMovData, preserveMovData,
@ -1187,6 +1197,7 @@ const App = memo(() => {
autoDeleteMergedSegments, autoDeleteMergedSegments,
preserveMetadataOnMerge, preserveMetadataOnMerge,
appendFfmpegCommandLog, appendFfmpegCommandLog,
mergedOutFilePath,
}); });
} }
@ -1217,7 +1228,7 @@ const App = memo(() => {
if (areWeCutting) notices.push(i18n.t('Cutpoints may be inaccurate.')); 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 (!hideAllNotifications) openExportFinishedToast({ filePath: revealPath, warnings, notices });
if (cleanupChoices.cleanupAfterExport) await cleanupFiles(cleanupChoices); if (cleanupChoices.cleanupAfterExport) await cleanupFiles(cleanupChoices);
@ -1244,7 +1255,7 @@ const App = memo(() => {
setWorking(); setWorking();
setCutProgress(); 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 () => { const onExportPress = useCallback(async () => {
if (!filePath || workingRef.current || segmentsToExport.length < 1) return; if (!filePath || workingRef.current || segmentsToExport.length < 1) return;
@ -2483,7 +2494,7 @@ const App = memo(() => {
)} )}
</Sheet> </Sheet>
<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} onShowStreamsSelectorClick={handleShowStreamsSelectorClick} outFormat={fileFormat} setOutSegTemplate={setOutSegTemplate} outSegTemplate={outSegTemplateOrDefault} generateOutSegFileNames={generateOutSegFileNames} currentSegIndexSafe={currentSegIndexSafe} getOutSegError={getOutSegError} mainCopiedThumbnailStreams={mainCopiedThumbnailStreams} needSmartCut={needSmartCut} /> <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} onShowStreamsSelectorClick={handleShowStreamsSelectorClick} outFormat={fileFormat} setOutSegTemplate={setOutSegTemplate} outSegTemplate={outSegTemplateOrDefault} generateOutSegFileNames={generateOutSegFileNames} currentSegIndexSafe={currentSegIndexSafe} getOutSegError={getOutSegError} mainCopiedThumbnailStreams={mainCopiedThumbnailStreams} needSmartCut={needSmartCut} mergedOutFileName={mergedOutFileName} setMergedOutFileName={setMergedOutFileName} />
<LastCommandsSheet <LastCommandsSheet
visible={lastCommandsVisible} visible={lastCommandsVisible}

View File

@ -155,7 +155,7 @@ const ConcatDialog = memo(({
const onOutputFormatUserChange = useCallback((newFormat) => setFileFormat(newFormat), [setFileFormat]); const onOutputFormatUserChange = useCallback((newFormat) => 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 ( return (
<> <>

View File

@ -15,6 +15,7 @@ import OutSegTemplateEditor from './OutSegTemplateEditor';
import HighlightedText, { highlightedTextStyle } from './HighlightedText'; import HighlightedText, { highlightedTextStyle } from './HighlightedText';
import Select from './Select'; import Select from './Select';
import Switch from './Switch'; import Switch from './Switch';
import MergedOutFileName from './MergedOutFileName';
import { primaryTextColor } from '../colors'; import { primaryTextColor } from '../colors';
import { withBlur } from '../util'; import { withBlur } from '../util';
@ -36,7 +37,7 @@ const ExportConfirm = memo(({
areWeCutting, selectedSegments, segmentsToExport, willMerge, visible, onClosePress, onExportConfirm, areWeCutting, selectedSegments, segmentsToExport, willMerge, visible, onClosePress, onExportConfirm,
outFormat, renderOutFmt, outputDir, numStreamsTotal, numStreamsToCopy, onShowStreamsSelectorClick, outSegTemplate, outFormat, renderOutFmt, outputDir, numStreamsTotal, numStreamsToCopy, onShowStreamsSelectorClick, outSegTemplate,
setOutSegTemplate, generateOutSegFileNames, filePath, currentSegIndexSafe, getOutSegError, nonFilteredSegmentsOrInverse, setOutSegTemplate, generateOutSegFileNames, filePath, currentSegIndexSafe, getOutSegError, nonFilteredSegmentsOrInverse,
mainCopiedThumbnailStreams, needSmartCut, mainCopiedThumbnailStreams, needSmartCut, mergedOutFileName, setMergedOutFileName,
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -198,6 +199,20 @@ const ExportConfirm = memo(({
</tr> </tr>
)} )}
{willMerge && (
<tr>
<td>
{t('Merged output file name:')}
</td>
<td>
<MergedOutFileName mergedOutFileName={mergedOutFileName} setMergedOutFileName={setMergedOutFileName} />
</td>
<td>
<HelpIcon onClick={() => showHelpText({ text: t('Name of the merged/concatenated output file when concatenating multiple segments.') })} />
</td>
</tr>
)}
<tr> <tr>
<td> <td>
{t('Overwrite existing files')} {t('Overwrite existing files')}

View File

@ -0,0 +1,12 @@
import React, { memo } from 'react';
import TextInput from './TextInput';
const MergedOutFileName = memo(({ mergedOutFileName, setMergedOutFileName }) => (
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap', justifyContent: 'flex-end' }}>
<TextInput value={mergedOutFileName} onChange={(e) => setMergedOutFileName(e.target.value)} style={{ textAlign: 'right' }} />
</div>
));
export default MergedOutFileName;

View File

@ -13,6 +13,7 @@ import { defaultOutSegTemplate, segNumVariable, segSuffixVariable } from '../uti
import useUserSettings from '../hooks/useUserSettings'; import useUserSettings from '../hooks/useUserSettings';
import Switch from './Switch'; import Switch from './Switch';
import Select from './Select'; import Select from './Select';
import TextInput from './TextInput';
const ReactSwal = withReactContent(Swal); const ReactSwal = withReactContent(Swal);
@ -22,8 +23,6 @@ const formatVariable = (variable) => `\${${variable}}`;
const extVar = formatVariable('EXT'); 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 OutSegTemplateEditor = memo(({ outSegTemplate, setOutSegTemplate, generateOutSegFileNames, currentSegIndexSafe, getOutSegError }) => {
const { safeOutputFileName, toggleSafeOutputFileName, outputFileNameMinZeroPadding, setOutputFileNameMinZeroPadding } = useUserSettings(); const { safeOutputFileName, toggleSafeOutputFileName, outputFileNameMinZeroPadding, setOutputFileNameMinZeroPadding } = useUserSettings();
@ -117,7 +116,7 @@ const OutSegTemplateEditor = memo(({ outSegTemplate, setOutSegTemplate, generate
exit={{ opacity: 0, height: 0, marginTop: 0 }} exit={{ opacity: 0, height: 0, marginTop: 0 }}
> >
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '.2em' }}> <div style={{ display: 'flex', alignItems: 'center', marginBottom: '.2em' }}>
<input type="text" ref={inputRef} style={inputStyle} onChange={onTextChange} value={text} autoComplete="off" autoCapitalize="off" autoCorrect="off" /> <TextInput ref={inputRef} onChange={onTextChange} value={text} autoComplete="off" autoCapitalize="off" autoCorrect="off" />
{outSegFileNames != null && <Button height={20} onClick={onAllSegmentsPreviewPress} marginLeft={5}>{t('Preview')}</Button>} {outSegFileNames != null && <Button height={20} onClick={onAllSegmentsPreviewPress} marginLeft={5}>{t('Preview')}</Button>}

View File

@ -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
<input type="text" ref={forwardedRef} style={{ ...inputStyle, ...style }} {...props} />
));
export default TextInput;

View File

@ -437,9 +437,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
} }
}, [concatFiles, cutSingle, filePath, needSmartCut, shouldSkipExistingFile]); }, [concatFiles, cutSingle, filePath, needSmartCut, shouldSkipExistingFile]);
const autoConcatCutSegments = useCallback(async ({ customOutDir, isCustomFormatSelected, outFormat, segmentPaths, ffmpegExperimental, onProgress, preserveMovData, movFastStart, autoDeleteMergedSegments, chapterNames, preserveMetadataOnMerge, appendFfmpegCommandLog }) => { const autoConcatCutSegments = useCallback(async ({ customOutDir, outFormat, segmentPaths, ffmpegExperimental, onProgress, preserveMovData, movFastStart, autoDeleteMergedSegments, chapterNames, preserveMetadataOnMerge, appendFfmpegCommandLog, mergedOutFilePath }) => {
const ext = getOutFileExtension({ isCustomFormatSelected, outFormat, filePath });
const outPath = getSuffixedOutPath({ customOutDir, filePath, nameSuffix: `cut-merged-${new Date().getTime()}${ext}` });
const outDir = getOutDir(customOutDir, filePath); const outDir = getOutDir(customOutDir, filePath);
const chapters = await createChaptersFromSegments({ segmentPaths, chapterNames }); const chapters = await createChaptersFromSegments({ segmentPaths, chapterNames });
@ -447,10 +445,8 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
const metadataFromPath = segmentPaths[0]; const metadataFromPath = segmentPaths[0];
// need to re-read streams because may have changed // need to re-read streams because may have changed
const { streams } = await readFileMeta(metadataFromPath); 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); if (autoDeleteMergedSegments) await tryDeleteFiles(segmentPaths);
return outPath;
}, [concatFiles, filePath]); }, [concatFiles, filePath]);
const html5ify = useCallback(async ({ customOutDir, filePath: filePathArg, speed, hasAudio, hasVideo, onProgress }) => { const html5ify = useCallback(async ({ customOutDir, filePath: filePathArg, speed, hasAudio, hasVideo, onProgress }) => {