combine batch list and merge list #89

- add button to merge current batch list
- show more options in merge dialog
- allow preserving metadata when merging also when not using allStreams #1008
- remember concat/batch choice #371
pull/1023/head
Mikael Finstad 2022-02-07 00:50:57 +07:00
rodzic e2175ab825
commit 9ffc781c29
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 25AB36E3E81CBC26
8 zmienionych plików z 168 dodań i 165 usunięć

Wyświetl plik

@ -243,7 +243,7 @@ module.exports = (app, mainWindow, newVersion) => {
label: i18n.t('Tools'),
submenu: [
{
label: i18n.t('Merge files'),
label: i18n.t('Merge/concatenate files'),
click() {
mainWindow.webContents.send('show-merge-dialog', true);
},

Wyświetl plik

@ -3,7 +3,7 @@ import { unstable_batchedUpdates as batchedUpdates } from 'react-dom';
import { FaAngleLeft, FaWindowClose, FaTimes } from 'react-icons/fa';
import { AnimatePresence, motion } from 'framer-motion';
import Lottie from 'react-lottie-player';
import { SideSheet, Button, Position, ForkIcon, DisableIcon, Select, ThemeProvider } from 'evergreen-ui';
import { SideSheet, Button, Position, ForkIcon, DisableIcon, Select, ThemeProvider, MergeColumnsIcon } from 'evergreen-ui';
import { useStateWithHistory } from 'react-use/lib/useStateWithHistory';
import useDebounceOld from 'react-use/lib/useDebounce'; // Want to phase out this
import { useDebounce } from 'use-debounce';
@ -40,6 +40,7 @@ import ValueTuner from './components/ValueTuner';
import VolumeControl from './components/VolumeControl';
import SubtitleControl from './components/SubtitleControl';
import BatchFile from './components/BatchFile';
import ConcatDialog from './components/ConcatDialog';
import { loadMifiLink } from './mifi';
import { primaryColor, controlsBackground, timelineBackground } from './colors';
@ -63,7 +64,7 @@ import {
} from './util';
import { formatDuration } from './util/duration';
import { adjustRate } from './util/rate-calculator';
import { askForOutDir, askForImportChapters, createNumSegments, createFixedDurationSegments, promptTimeOffset, askForHtml5ifySpeed, askForFileOpenAction, confirmExtractAllStreamsDialog, cleanupFilesDialog, showDiskFull, showCutFailedDialog, labelSegmentDialog, openYouTubeChaptersDialog, showMultipleFilesDialog, showOpenAndMergeDialog, openAbout, showEditableJsonDialog } from './dialogs';
import { askForOutDir, askForImportChapters, createNumSegments, createFixedDurationSegments, promptTimeOffset, askForHtml5ifySpeed, askForFileOpenAction, confirmExtractAllStreamsDialog, cleanupFilesDialog, showDiskFull, showCutFailedDialog, labelSegmentDialog, openYouTubeChaptersDialog, showMergeFilesOpenDialog, openAbout, showEditableJsonDialog } from './dialogs';
import { openSendReportDialog } from './reporting';
import { fallbackLng } from './i18n';
import { createSegment, getCleanCutSegments, getSegApparentStart, findSegmentsAtCursor, sortSegments, invertSegments, getSegmentTags } from './segments';
@ -125,6 +126,7 @@ const App = memo(() => {
const [mainAudioStream, setMainAudioStream] = useState();
const [copyStreamIdsByFile, setCopyStreamIdsByFile] = useState({});
const [streamsSelectorShown, setStreamsSelectorShown] = useState(false);
const [concatDialogVisible, setConcatDialogVisible] = useState(false);
const [zoom, setZoom] = useState(1);
const [thumbnails, setThumbnails] = useState([]);
const [shortestFlag, setShortestFlag] = useState(false);
@ -146,8 +148,9 @@ const App = memo(() => {
const [settingsVisible, setSettingsVisible] = useState(false);
const [tunerVisible, setTunerVisible] = useState();
const [mifiLink, setMifiLink] = useState();
const [alwaysConcatMultipleFiles, setAlwaysConcatMultipleFiles] = useState(false);
// Batch state
// Batch state / concat files
const [batchFiles, setBatchFiles] = useState([]);
// Segment related state
@ -642,9 +645,10 @@ const App = memo(() => {
return { cancel: false, newCustomOutDir };
}, [customOutDir, setCustomOutDir]);
const mergeFiles = useCallback(async ({ paths, allStreams, segmentsToChapters: segmentsToChapters2 }) => {
const mergeFiles = useCallback(async ({ paths, allStreams }) => {
if (workingRef.current) return;
try {
setConcatDialogVisible(false);
setWorking(i18n.t('Merging'));
const firstPath = paths[0];
@ -656,7 +660,7 @@ const App = memo(() => {
const outDir = getOutDir(customOutDir, firstPath);
let chapters;
if (segmentsToChapters2) {
if (segmentsToChapters) {
const chapterNames = paths.map((path) => parsePath(path).name);
chapters = await createChaptersFromSegments({ segmentPaths: paths, chapterNames });
}
@ -675,7 +679,7 @@ const App = memo(() => {
setWorking();
setCutProgress();
}
}, [ensureOutDirAccessible, ffmpegExperimental, preserveMovData, movFastStart, preserveMetadataOnMerge, customOutDir, ffmpegMergeFiles, setWorking]);
}, [setWorking, ensureOutDirAccessible, customOutDir, segmentsToChapters, ffmpegMergeFiles, ffmpegExperimental, preserveMovData, movFastStart, preserveMetadataOnMerge]);
const toggleCaptureFormat = useCallback(() => setCaptureFormat(f => (f === 'png' ? 'jpeg' : 'png')), [setCaptureFormat]);
@ -1427,6 +1431,8 @@ const App = memo(() => {
const seekAccelerationRef = useRef(1);
useEffect(() => {
if (concatDialogVisible) return () => {};
function onKeyPress() {
if (exportConfirmVisible) onExportConfirm();
else onExportPress();
@ -1435,7 +1441,7 @@ const App = memo(() => {
const mousetrap = new Mousetrap();
mousetrap.bind('e', onKeyPress);
return () => mousetrap.reset();
}, [exportConfirmVisible, onExportConfirm, onExportPress]);
}, [exportConfirmVisible, onExportConfirm, onExportPress, concatDialogVisible]);
useEffect(() => {
function onEscPress() {
@ -1562,6 +1568,8 @@ const App = memo(() => {
}
}, [userOpenSingleFile, setWorking, filePath]);
const batchFilePaths = useMemo(() => batchFiles.map((f) => f.path), [batchFiles]);
const batchFileJump = useCallback((direction) => {
const pathIndex = batchFiles.findIndex(({ path }) => path === filePath);
if (pathIndex === -1) return;
@ -1572,8 +1580,7 @@ const App = memo(() => {
const batchLoadFiles = useCallback((paths) => {
setBatchFiles(paths.map((path) => ({ path, name: basename(path) })));
batchOpenSingleFile(paths[0]);
}, [batchOpenSingleFile]);
}, []);
const userOpenFiles = useCallback(async (filePaths) => {
if (!filePaths || filePaths.length < 1) return;
@ -1582,10 +1589,16 @@ const App = memo(() => {
console.log(filePaths.join('\n'));
if (filePaths.length > 1) {
showMultipleFilesDialog(filePaths, mergeFiles, batchLoadFiles);
batchLoadFiles(filePaths);
if (alwaysConcatMultipleFiles) {
setConcatDialogVisible(true);
} else {
batchOpenSingleFile(filePaths[0]);
}
return;
}
// filePaths.length is now 1
const firstFilePath = filePaths[0];
@ -1642,7 +1655,7 @@ const App = memo(() => {
} finally {
setWorking();
}
}, [addStreamSourceFile, checkFileOpened, enableAskForFileOpenAction, isFileOpened, loadEdlFile, mergeFiles, userOpenSingleFile, setWorking, batchLoadFiles]);
}, [batchLoadFiles, alwaysConcatMultipleFiles, batchOpenSingleFile, setWorking, isFileOpened, userOpenSingleFile, checkFileOpened, loadEdlFile, enableAskForFileOpenAction, addStreamSourceFile]);
const userHtml5ifyCurrentFile = useCallback(async () => {
if (!filePath) return;
@ -1693,7 +1706,7 @@ const App = memo(() => {
// TODO split up?
useEffect(() => {
if (exportConfirmVisible) return () => {};
if (exportConfirmVisible || concatDialogVisible) return () => {};
const togglePlayNoReset = () => togglePlay();
const togglePlayReset = () => togglePlay(true);
@ -1787,7 +1800,7 @@ const App = memo(() => {
}, [
addCutSegment, capture, changePlaybackRate, togglePlay, removeCutSegment,
setCutEnd, setCutStart, seekRel, seekRelPercent, shortStep, cleanupFiles, jumpSeg,
seekClosestKeyframe, zoomRel, toggleComfortZoom, splitCurrentSegment, exportConfirmVisible,
seekClosestKeyframe, zoomRel, toggleComfortZoom, splitCurrentSegment, exportConfirmVisible, concatDialogVisible,
increaseRotation, jumpCutStart, jumpCutEnd, cutSegmentsHistory, keyboardSeekAccFactor,
keyboardNormalSeekSpeed, onLabelSegmentPress, currentSegIndexSafe, batchFileJump, goToTimecode,
]);
@ -1831,11 +1844,12 @@ const App = memo(() => {
}, [fileUri, usingPreviewFile, hasVideo, hasAudio, html5ifyAndLoadWithPreferences, customOutDir, filePath, setWorking]);
useEffect(() => {
function showOpenAndMergeDialog2() {
showOpenAndMergeDialog({
defaultPath: outputDir,
onMergeClick: mergeFiles,
});
async function showOpenAndMergeDialog() {
const files = await showMergeFilesOpenDialog(outputDir);
if (files) {
batchLoadFiles(files);
setConcatDialogVisible(true);
}
}
async function setStartOffset() {
@ -1961,7 +1975,7 @@ const App = memo(() => {
electron.ipcRenderer.on('close-file', closeFile2);
electron.ipcRenderer.on('close-batch-files', closeBatch2);
electron.ipcRenderer.on('html5ify', userHtml5ifyCurrentFile);
electron.ipcRenderer.on('show-merge-dialog', showOpenAndMergeDialog2);
electron.ipcRenderer.on('show-merge-dialog', showOpenAndMergeDialog);
electron.ipcRenderer.on('set-start-offset', setStartOffset);
electron.ipcRenderer.on('extract-all-streams', extractAllStreams);
electron.ipcRenderer.on('showStreamsSelector', showStreamsSelector);
@ -1985,7 +1999,7 @@ const App = memo(() => {
electron.ipcRenderer.removeListener('close-file', closeFile2);
electron.ipcRenderer.removeListener('close-batch-files', closeBatch2);
electron.ipcRenderer.removeListener('html5ify', userHtml5ifyCurrentFile);
electron.ipcRenderer.removeListener('show-merge-dialog', showOpenAndMergeDialog2);
electron.ipcRenderer.removeListener('show-merge-dialog', showOpenAndMergeDialog);
electron.ipcRenderer.removeListener('set-start-offset', setStartOffset);
electron.ipcRenderer.removeListener('extract-all-streams', extractAllStreams);
electron.ipcRenderer.removeListener('showStreamsSelector', showStreamsSelector);
@ -2005,7 +2019,7 @@ const App = memo(() => {
electron.ipcRenderer.removeListener('reorderSegsByStartTime', reorderSegsByStartTime);
};
}, [
mergeFiles, outputDir, filePath, customOutDir, startTimeOffset, userHtml5ifyCurrentFile,
outputDir, filePath, customOutDir, startTimeOffset, userHtml5ifyCurrentFile, batchLoadFiles,
extractAllStreams, userOpenFiles, openSendReportDialogWithState, setWorking,
loadEdlFile, cutSegments, apparentCutSegments, edlFilePath, toggleHelp, toggleSettings, ensureOutDirAccessible, html5ifyAndLoad, html5ify,
loadCutSegments, duration, checkFileOpened, loadMedia, fileFormat, reorderSegsByStartTime, closeFileWithConfirm, closeBatch, clearSegments, clearSegCounter, fixInvalidDuration, invertAllCutSegments, getFrameCount,
@ -2344,6 +2358,11 @@ const App = memo(() => {
>
<div style={{ background: controlsBackground, fontSize: 14, paddingBottom: 7, paddingTop: 3, paddingLeft: 10, paddingRight: 5, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
{t('Batch file list')}
<div style={{ flexGrow: 1 }} />
<MergeColumnsIcon role="button" title={t('Merge/concatenate files')} color="white" style={{ marginRight: 10, cursor: 'pointer' }} onClick={() => setConcatDialogVisible(true)} />
<FaTimes size={18} role="button" style={{ cursor: 'pointer', color: 'white' }} onClick={() => closeBatch()} title={t('Close batch')} />
</div>
@ -2532,6 +2551,8 @@ const App = memo(() => {
renderSettings={renderSettings}
/>
<ConcatDialog isShown={batchFiles.length > 0 && concatDialogVisible} onHide={() => setConcatDialogVisible(false)} initialPaths={batchFilePaths} onConcat={mergeFiles} segmentsToChapters={segmentsToChapters} setSegmentsToChapters={setSegmentsToChapters} setAlwaysConcatMultipleFiles={setAlwaysConcatMultipleFiles} alwaysConcatMultipleFiles={alwaysConcatMultipleFiles} preserveMetadataOnMerge={preserveMetadataOnMerge} setPreserveMetadataOnMerge={setPreserveMetadataOnMerge} preserveMovData={preserveMovData} setPreserveMovData={setPreserveMovData} />
{tunerVisible && renderTuner(tunerVisible)}
</div>
</ThemeProvider>

Wyświetl plik

@ -1,80 +0,0 @@
import React, { memo, useState, useCallback, useEffect } from 'react';
import { sortableContainer, sortableElement } from 'react-sortable-hoc';
import { Pane, Checkbox, SortAlphabeticalIcon, SortAlphabeticalDescIcon, IconButton } from 'evergreen-ui';
import { useTranslation } from 'react-i18next';
import arrayMove from 'array-move';
import orderBy from 'lodash/orderBy';
const { basename } = window.require('path');
const rowStyle = {
padding: '3px 10px', fontSize: 14, margin: '7px 0', overflowY: 'auto', whiteSpace: 'nowrap', cursor: 'grab',
};
const SortableItem = sortableElement(({ value, sortIndex }) => (
<Pane elevation={1} style={rowStyle} title={value}>
{sortIndex + 1}
{'. '}
{basename(value)}
</Pane>
));
const SortableContainer = sortableContainer(({ items }) => (
<div style={{ padding: '0 3px' }}>
{items.map((value, index) => (
<SortableItem key={value} index={index} sortIndex={index} value={value} />
))}
</div>
));
const SortableFiles = memo(({
items: itemsProp, onChange, helperContainer, onAllStreamsChange, onSegmentsToChaptersChange,
}) => {
const [items, setItems] = useState(itemsProp);
const [allStreams, setAllStreams] = useState(false);
const [segmentsToChapters, setSegmentsToChapters] = useState(false);
const [sortDesc, setSortDesc] = useState();
useEffect(() => onAllStreamsChange(allStreams), [allStreams, onAllStreamsChange]);
useEffect(() => onSegmentsToChaptersChange(segmentsToChapters), [segmentsToChapters, onSegmentsToChaptersChange]);
useEffect(() => onChange(items), [items, onChange]);
const onSortEnd = useCallback(({ oldIndex, newIndex }) => {
const newItems = arrayMove(items, oldIndex, newIndex);
setItems(newItems);
}, [items]);
const { t } = useTranslation();
const onSortClick = useCallback(() => {
const newSortDesc = sortDesc == null ? false : !sortDesc;
setItems(orderBy(items, undefined, [newSortDesc ? 'desc' : 'asc']));
setSortDesc(newSortDesc);
}, [items, sortDesc]);
return (
<div style={{ textAlign: 'left' }}>
<div style={{ whiteSpace: 'pre-wrap', fontSize: 14, marginBottom: 10 }}>{t('This dialog can be used to concatenate files in series, e.g. one after the other:\n[file1][file2][file3]\nIt can NOT be used for merging tracks in parallell (like adding an audio track to a video).\nMake sure all files are of the exact same codecs & codec parameters (fps, resolution etc).\n\nDrag and drop to change the order of your files here:')}</div>
<SortableContainer
items={items}
onSortEnd={onSortEnd}
helperContainer={helperContainer}
getContainer={() => helperContainer().parentNode}
helperClass="dragging-helper-class"
/>
<div style={{ marginTop: 10 }}>
<Checkbox checked={allStreams} onChange={e => setAllStreams(e.target.checked)} label={`${t('Include all tracks?')} ${t('If this is checked, all audio/video/subtitle/data tracks will be included. This may not always work for all file types. If not checked, only default streams will be included.')}`} />
<Checkbox checked={segmentsToChapters} onChange={e => setSegmentsToChapters(e.target.checked)} label={t('Create chapters from merged segments? (slow)')} />
<div>
<IconButton icon={sortDesc ? SortAlphabeticalDescIcon : SortAlphabeticalIcon} onClick={onSortClick} />
</div>
</div>
</div>
);
});
export default SortableFiles;

Wyświetl plik

@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import { FaAngleRight, FaFile } from 'react-icons/fa';
import useContextMenu from '../hooks/useContextMenu';
import { primaryColor } from '../colors';
import { primaryTextColor } from '../colors';
const BatchFile = memo(({ path, filePath, name, onOpen, onDelete }) => {
const ref = useRef();
@ -18,7 +18,9 @@ const BatchFile = memo(({ path, filePath, name, onOpen, onDelete }) => {
return (
<div ref={ref} role="button" style={{ background: isCurrent ? 'rgba(255,255,255,0.15)' : undefined, fontSize: 13, padding: '1px 3px', cursor: 'pointer', display: 'flex', alignItems: 'center', minHeight: 30, alignContent: 'flex-start' }} title={path} onClick={() => onOpen(path)}>
<FaFile size={14} style={{ color: primaryColor, marginLeft: 3, marginRight: 4, flexShrink: 0 }} />
<div style={{ flexBasis: 5, flexShrink: 1 }} />
<FaFile size={14} style={{ color: primaryTextColor, flexShrink: 0 }} />
<div style={{ flexBasis: 10, flexShrink: 1 }} />
<div style={{ wordBreak: 'break-all' }}>{name}</div>
<div style={{ flexGrow: 1 }} />
{isCurrent && <FaAngleRight size={14} style={{ color: 'white', marginRight: -3, flexShrink: 0 }} />}

Wyświetl plik

@ -0,0 +1,101 @@
import React, { memo, useState, useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Dialog, Pane, Checkbox, SortAlphabeticalIcon, SortAlphabeticalDescIcon, Button, Paragraph } from 'evergreen-ui';
import { sortableContainer, sortableElement } from 'react-sortable-hoc';
import arrayMove from 'array-move';
import orderBy from 'lodash/orderBy';
const { basename } = window.require('path');
const containerStyle = { color: 'black' };
const rowStyle = {
color: 'black', padding: '3px 10px', fontSize: 14, margin: '7px 0', overflowY: 'auto', whiteSpace: 'nowrap', cursor: 'grab',
};
const SortableItem = sortableElement(({ value, sortIndex }) => (
<Pane elevation={1} style={rowStyle} title={value}>
{sortIndex + 1}
{'. '}
{basename(value)}
</Pane>
));
const SortableContainer = sortableContainer(({ items }) => (
<div style={{ padding: '0 3px' }}>
{items.map((value, index) => (
<SortableItem key={value} index={index} sortIndex={index} value={value} />
))}
</div>
));
const ConcatDialog = memo(({
isShown, onHide, initialPaths, onConcat,
segmentsToChapters, setSegmentsToChapters,
alwaysConcatMultipleFiles, setAlwaysConcatMultipleFiles,
preserveMetadataOnMerge, setPreserveMetadataOnMerge,
preserveMovData, setPreserveMovData,
}) => {
const { t } = useTranslation();
const [paths, setPaths] = useState(initialPaths);
const [allStreams, setAllStreams] = useState(false);
const [sortDesc, setSortDesc] = useState();
useEffect(() => {
setPaths(initialPaths);
}, [initialPaths]);
const onSortEnd = useCallback(({ oldIndex, newIndex }) => {
const newPaths = arrayMove(paths, oldIndex, newIndex);
setPaths(newPaths);
}, [paths]);
const onSortClick = useCallback(() => {
const newSortDesc = sortDesc == null ? false : !sortDesc;
setPaths(orderBy(paths, undefined, [newSortDesc ? 'desc' : 'asc']));
setSortDesc(newSortDesc);
}, [paths, sortDesc]);
return (
<Dialog
title={t('Merge/concatenate files')}
isShown={isShown}
confirmLabel={t('Merge!')}
cancelLabel={t('Cancel')}
onCloseComplete={onHide}
onConfirm={() => onConcat({ paths, allStreams })}
topOffset="3vh"
>
<div style={containerStyle}>
<div style={{ whiteSpace: 'pre-wrap', fontSize: 14, marginBottom: 10 }}>
{t('This dialog can be used to concatenate files in series, e.g. one after the other:\n[file1][file2][file3]\nIt can NOT be used for merging tracks in parallell (like adding an audio track to a video).\nMake sure all files are of the exact same codecs & codec parameters (fps, resolution etc).\n\nDrag and drop to change the order of your files here:')}
</div>
<SortableContainer
items={paths}
onSortEnd={onSortEnd}
helperClass="dragging-helper-class"
/>
<Button iconBefore={sortDesc ? SortAlphabeticalDescIcon : SortAlphabeticalIcon} onClick={onSortClick}>{t('Sort items')}</Button>
<div style={{ marginTop: 10 }}>
<Checkbox checked={allStreams} onChange={(e) => setAllStreams(e.target.checked)} label={`${t('Include all tracks?')} ${t('If this is checked, all audio/video/subtitle/data tracks will be included. This may not always work for all file types. If not checked, only default streams will be included.')}`} />
<Checkbox checked={preserveMetadataOnMerge} onChange={(e) => setPreserveMetadataOnMerge(e.target.checked)} label={t('Preserve original metadata when merging? (slow)')} />
<Checkbox checked={preserveMovData} onChange={(e) => setPreserveMovData(e.target.checked)} label={t('Preserve all MP4/MOV metadata?')} />
<Checkbox checked={segmentsToChapters} onChange={(e) => setSegmentsToChapters(e.target.checked)} label={t('Create chapters from merged segments? (slow)')} />
<Paragraph>{t('Note that also other settings from the normal export dialog apply to this merge function. For more information about all options, see the export dialog.')}</Paragraph>
<Checkbox checked={alwaysConcatMultipleFiles} onChange={(e) => setAlwaysConcatMultipleFiles(e.target.checked)} label={t('Always open this dialog when opening multiple files')} />
</div>
</div>
</Dialog>
);
});
export default ConcatDialog;

Wyświetl plik

@ -13,8 +13,6 @@ import { parseYouTube } from './edlFormats';
import CopyClipboardButton from './components/CopyClipboardButton';
import { errorToast } from './util';
import SortableFiles from './SortableFiles';
const electron = window.require('electron'); // eslint-disable-line
const { dialog, app } = electron.remote;
@ -352,55 +350,7 @@ export function openAbout() {
});
}
export async function showMergeDialog(paths, onMergeClick) {
let swalElem;
let outPaths = paths;
let allStreams = false;
let segmentsToChapters = false;
const { dismiss } = await ReactSwal.fire({
width: '90%',
showCancelButton: true,
confirmButtonText: i18n.t('Merge!'),
cancelButtonText: i18n.t('Cancel'),
willOpen: (el) => { swalElem = el; },
title: i18n.t('Merge/concatenate files'),
html: (<SortableFiles
items={outPaths}
onChange={(val) => { outPaths = val; }}
onAllStreamsChange={(val) => { allStreams = val; }}
onSegmentsToChaptersChange={(val) => { segmentsToChapters = val; }}
helperContainer={() => swalElem}
/>),
});
if (!dismiss) {
await onMergeClick({ paths: outPaths, allStreams, segmentsToChapters });
}
}
export async function showMultipleFilesDialog(paths, onMergeClick, onBatchLoadFilesClick) {
const { isConfirmed, isDenied } = await Swal.fire({
showCloseButton: true,
showDenyButton: true,
denyButtonAriaLabel: '',
denyButtonColor: '#7367f0',
denyButtonText: i18n.t('Batch files'),
confirmButtonText: i18n.t('Merge/concatenate files'),
title: i18n.t('Multiple files'),
text: i18n.t('Do you want to merge/concatenate the files or load them for batch processing?'),
});
if (isDenied) {
await onBatchLoadFilesClick(paths);
return;
}
if (isConfirmed) {
await showMergeDialog(paths, onMergeClick);
}
}
export async function showOpenAndMergeDialog({ defaultPath, onMergeClick }) {
export async function showMergeFilesOpenDialog(defaultPath) {
const title = i18n.t('Please select files to be merged');
const message = i18n.t('Please select files to be merged. The files need to be of the exact same format and codecs');
const { canceled, filePaths } = await dialog.showOpenDialog({
@ -409,14 +359,14 @@ export async function showOpenAndMergeDialog({ defaultPath, onMergeClick }) {
properties: ['openFile', 'multiSelections'],
message,
});
if (canceled || !filePaths) return;
if (canceled || !filePaths) return undefined;
if (filePaths.length < 2) {
errorToast(i18n.t('More than one file must be selected'));
return;
return undefined;
}
showMergeDialog(filePaths, onMergeClick);
return filePaths;
}
export async function showEditableJsonDialog({ text, title, inputLabel, inputValue, inputValidator }) {

Wyświetl plik

@ -227,9 +227,16 @@ function useFfmpegOperations({ filePath, enableTransferTimestamps }) {
const ffmetadataPath = await writeChaptersFfmetadata(outDir, chapters);
const shouldMapMetadata = preserveMetadataOnMerge && allStreams;
try {
let map;
if (allStreams) map = ['-map', '0'];
// If preserveMetadataOnMerge option is enabled, we need to explicitly map even if allStreams=false.
// We cannot use the ffmpeg's automatic stream selection or else ffmpeg might use the metadata source input (index 1)
// instead of the concat input (index 0)
// https://ffmpeg.org/ffmpeg.html#Automatic-stream-selection
else if (preserveMetadataOnMerge) map = ['-map', 'v:0?', '-map', 'a:0?', '-map', 's:0?'];
else map = []; // ffmpeg default mapping
// Keep this similar to cutSingle()
const ffmpegArgs = [
'-hide_banner',
@ -240,20 +247,20 @@ function useFfmpegOperations({ filePath, enableTransferTimestamps }) {
'-f', 'concat', '-safe', '0', '-protocol_whitelist', 'file,pipe',
'-i', '-',
// Add the first file for using its metadata. Can only do this if allStreams (-map 0) is set, or else ffmpeg might output this input instead of the concat
...(shouldMapMetadata ? ['-i', paths[0]] : []),
// Add the first file (we will get metadata from this input)
...(preserveMetadataOnMerge ? ['-i', paths[0]] : []),
// Chapters?
...(ffmetadataPath ? ['-f', 'ffmetadata', '-i', ffmetadataPath] : []),
'-c', 'copy',
...(allStreams ? ['-map', '0'] : []),
...map,
// -map_metadata 0 with concat demuxer doesn't transfer metadata from the concat'ed files when merging.
// -map_metadata 0 with concat demuxer doesn't transfer metadata from the concat'ed file input (index 0) when merging.
// So we use the first file file (index 1) for metadata
// Can only do this if allStreams (-map 0) is set
...(shouldMapMetadata ? ['-map_metadata', '1'] : []),
...(preserveMetadataOnMerge ? ['-map_metadata', '1'] : []),
...getMovFlags({ preserveMovData, movFastStart }),
...getMatroskaFlags(),

Wyświetl plik

@ -48,6 +48,8 @@ kbd {
.dragging-helper-class {
color: rgba(0,0,0,0.5);
/* https://github.com/clauderic/react-sortable-hoc/issues/803 */
z-index: 99999;
}
.hide-scrollbar::-webkit-scrollbar {