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,
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(() => {
)}
</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
visible={lastCommandsVisible}

View File

@ -155,7 +155,7 @@ const ConcatDialog = memo(({
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 (
<>

View File

@ -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(({
</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>
<td>
{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 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 }}
>
<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>}

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]);
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 }) => {