Implement batch file list #89

pull/841/head
Mikael Finstad 4 years ago
parent b487db5e14
commit d3c8fce967
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26

@ -28,6 +28,7 @@ The main feature is lossless trimming and cutting of video and audio files, whic
- Lossless merge/concatenation of arbitrary files (with identical codecs parameters, e.g. from the same camera)
- Lossless stream editing: Combine arbitrary tracks from multiple files (ex. add music or subtitle track to a video file)
- Losslessly extract all tracks from a file (extract video, audio, subtitle, attachments and other tracks from one file into separate files)
- Batch view for fast multi-file workflow
- Remux into any compatible output format
- Take full-resolution snapshots from videos in JPEG/PNG format
- Manual input of cutpoint times

@ -29,6 +29,12 @@ module.exports = (app, mainWindow, newVersion) => {
mainWindow.webContents.send('close-file');
},
},
{
label: i18n.t('Close batch'),
async click() {
mainWindow.webContents.send('close-batch-files');
},
},
{ type: 'separator' },
{
label: i18n.t('Import project (LLC)...'),

@ -1,5 +1,6 @@
import React, { memo, useEffect, useState, useCallback, useRef, useMemo } from 'react';
import { FaAngleLeft, FaWindowClose } from 'react-icons/fa';
import { unstable_batchedUpdates as batchedUpdates } from 'react-dom';
import { FaAngleLeft, FaWindowClose, FaTimes, FaAngleRight, FaFile } from 'react-icons/fa';
import { AnimatePresence, motion } from 'framer-motion';
import Swal from 'sweetalert2';
import Lottie from 'react-lottie-player';
@ -42,7 +43,7 @@ import ValueTuner from './components/ValueTuner';
import VolumeControl from './components/VolumeControl';
import SubtitleControl from './components/SubtitleControl';
import { loadMifiLink } from './mifi';
import { primaryColor, controlsBackground } from './colors';
import { primaryColor, controlsBackground, timelineBackground } from './colors';
import allOutFormats from './outFormats';
import { captureFrameFromTag, captureFrameFfmpeg } from './capture-frame';
import {
@ -61,7 +62,7 @@ import {
hasDuplicates, havePermissionToReadFile, isMac, resolvePathIfNeeded, pathExists, html5ifiedPrefix, html5dummySuffix, findExistingHtml5FriendlyFile,
} from './util';
import { formatDuration } from './util/duration';
import { askForOutDir, askForImportChapters, createNumSegments, createFixedDurationSegments, promptTimeOffset, askForHtml5ifySpeed, askForFileOpenAction, confirmExtractAllStreamsDialog, cleanupFilesDialog, showDiskFull, showCutFailedDialog, labelSegmentDialog, openYouTubeChaptersDialog, showMergeDialog, showOpenAndMergeDialog, openAbout, showEditableJsonDialog } from './dialogs';
import { askForOutDir, askForImportChapters, createNumSegments, createFixedDurationSegments, promptTimeOffset, askForHtml5ifySpeed, askForFileOpenAction, confirmExtractAllStreamsDialog, cleanupFilesDialog, showDiskFull, showCutFailedDialog, labelSegmentDialog, openYouTubeChaptersDialog, showMultipleFilesDialog, showOpenAndMergeDialog, openAbout, showEditableJsonDialog } from './dialogs';
import { openSendReportDialog } from './reporting';
import { fallbackLng } from './i18n';
import { createSegment, createInitialCutSegments, getCleanCutSegments, getSegApparentStart, findSegmentsAtCursor, sortSegments, invertSegments, getSegmentTags } from './segments';
@ -141,11 +142,14 @@ const App = memo(() => {
const [keyframesEnabled, setKeyframesEnabled] = useState(true);
const [waveformEnabled, setWaveformEnabled] = useState(false);
const [thumbnailsEnabled, setThumbnailsEnabled] = useState(false);
const [showSideBar, setShowSideBar] = useState(true);
const [showRightBar, setShowRightBar] = useState(true);
const [hideCanvasPreview, setHideCanvasPreview] = useState(false);
const [cleanupChoices, setCleanupChoices] = useState({ tmpFiles: true });
const [rememberConvertToSupportedFormat, setRememberConvertToSupportedFormat] = useState();
// Batch state
const [batchFiles, setBatchFiles] = useState([]);
// Segment related state
const [currentSegIndex, setCurrentSegIndex] = useState(0);
const [cutStartTimeManual, setCutStartTimeManual] = useState();
@ -243,7 +247,7 @@ const App = memo(() => {
});
}
const toggleSideBar = useCallback(() => setShowSideBar(v => !v), []);
const toggleRightBar = useCallback(() => setShowRightBar(v => !v), []);
const toggleCopyStreamId = useCallback((path, index) => {
setCopyStreamIdsForPath(path, (old) => ({ ...old, [index]: !old[index] }));
@ -935,14 +939,24 @@ const App = memo(() => {
});
}, [playing, filePath]);
const closeFile = useCallback(() => {
if (!isFileOpened || workingRef.current) return false;
const closeFileWithConfirm = useCallback(() => {
// eslint-disable-next-line no-alert
if (askBeforeClose && !window.confirm(i18n.t('Are you sure you want to close the current file?'))) return false;
resetState();
return true;
}, [askBeforeClose, isFileOpened, resetState]);
}, [askBeforeClose, resetState]);
const closeFile = useCallback(() => {
if (!isFileOpened || workingRef.current) return false;
return closeFileWithConfirm();
}, [closeFileWithConfirm, isFileOpened]);
const closeBatch = useCallback(() => {
// eslint-disable-next-line no-alert
if (askBeforeClose && !window.confirm(i18n.t('Are you sure you want to close the loaded batch of files?'))) return;
setBatchFiles([]);
}, [askBeforeClose]);
const cleanupFiles = useCallback(async () => {
// Because we will reset state before deleting files
@ -1285,6 +1299,8 @@ const App = memo(() => {
resetState();
console.log('state reset');
setWorking(i18n.t('Loading file'));
async function checkAndSetExistingHtml5FriendlyFile() {
@ -1397,98 +1413,6 @@ const App = memo(() => {
const seekAccelerationRef = useRef(1);
// TODO split up?
useEffect(() => {
if (exportConfirmVisible) return () => {};
const togglePlayNoReset = () => togglePlay();
const togglePlayReset = () => togglePlay(true);
const reducePlaybackRate = () => changePlaybackRate(-1);
const increasePlaybackRate = () => changePlaybackRate(1);
function seekBackwards() {
seekRel(keyboardNormalSeekSpeed * seekAccelerationRef.current * -1);
seekAccelerationRef.current *= keyboardSeekAccFactor;
}
function seekForwards() {
seekRel(keyboardNormalSeekSpeed * seekAccelerationRef.current);
seekAccelerationRef.current *= keyboardSeekAccFactor;
}
const seekReset = () => {
seekAccelerationRef.current = 1;
};
const seekBackwardsPercent = () => { seekRelPercent(-0.01); return false; };
const seekForwardsPercent = () => { seekRelPercent(0.01); return false; };
const seekBackwardsKeyframe = () => seekClosestKeyframe(-1);
const seekForwardsKeyframe = () => seekClosestKeyframe(1);
const seekBackwardsShort = () => shortStep(-1);
const seekForwardsShort = () => shortStep(1);
const jumpPrevSegment = () => jumpSeg(-1);
const jumpNextSegment = () => jumpSeg(1);
const zoomIn = () => { zoomRel(1); return false; };
const zoomOut = () => { zoomRel(-1); return false; };
// mousetrap seems to be the only lib properly handling layouts that require shift to be pressed to get a particular key #520
// Also document.addEventListener needs custom handling of modifier keys or C will be triggered by CTRL+C, etc
const mousetrap = new Mousetrap();
// mousetrap.bind(':', () => console.log('test'));
mousetrap.bind('plus', () => addCutSegment());
mousetrap.bind('space', () => togglePlayReset());
mousetrap.bind('k', () => togglePlayNoReset());
mousetrap.bind('j', () => reducePlaybackRate());
mousetrap.bind('l', () => increasePlaybackRate());
mousetrap.bind('z', () => toggleComfortZoom());
mousetrap.bind(',', () => seekBackwardsShort());
mousetrap.bind('.', () => seekForwardsShort());
mousetrap.bind('c', () => capture());
mousetrap.bind('i', () => setCutStart());
mousetrap.bind('o', () => setCutEnd());
mousetrap.bind('backspace', () => removeCutSegment(currentSegIndexSafe));
mousetrap.bind('d', () => cleanupFiles());
mousetrap.bind('b', () => splitCurrentSegment());
mousetrap.bind('r', () => increaseRotation());
mousetrap.bind('left', () => seekBackwards());
mousetrap.bind('left', () => seekReset(), 'keyup');
mousetrap.bind(['ctrl+left', 'command+left'], () => seekBackwardsPercent());
mousetrap.bind('alt+left', () => seekBackwardsKeyframe());
mousetrap.bind('shift+left', () => jumpCutStart());
mousetrap.bind('right', () => seekForwards());
mousetrap.bind('right', () => seekReset(), 'keyup');
mousetrap.bind(['ctrl+right', 'command+right'], () => seekForwardsPercent());
mousetrap.bind('alt+right', () => seekForwardsKeyframe());
mousetrap.bind('shift+right', () => jumpCutEnd());
mousetrap.bind('up', () => jumpPrevSegment());
mousetrap.bind(['ctrl+up', 'command+up'], () => zoomIn());
mousetrap.bind('down', () => jumpNextSegment());
mousetrap.bind(['ctrl+down', 'command+down'], () => zoomOut());
// https://github.com/mifi/lossless-cut/issues/610
Mousetrap.bind(['ctrl+z', 'command+z'], (e) => {
e.preventDefault();
cutSegmentsHistory.back();
});
Mousetrap.bind(['ctrl+shift+z', 'command+shift+z'], (e) => {
e.preventDefault();
cutSegmentsHistory.forward();
});
mousetrap.bind(['enter'], () => {
onLabelSegmentPress(currentSegIndexSafe);
return false;
});
return () => mousetrap.reset();
}, [
addCutSegment, capture, changePlaybackRate, togglePlay, removeCutSegment,
setCutEnd, setCutStart, seekRel, seekRelPercent, shortStep, cleanupFiles, jumpSeg,
seekClosestKeyframe, zoomRel, toggleComfortZoom, splitCurrentSegment, exportConfirmVisible,
increaseRotation, jumpCutStart, jumpCutEnd, cutSegmentsHistory, keyboardSeekAccFactor,
keyboardNormalSeekSpeed, onLabelSegmentPress, currentSegIndexSafe,
]);
useEffect(() => {
function onKeyPress() {
if (exportConfirmVisible) onExportConfirm();
@ -1612,6 +1536,32 @@ const App = memo(() => {
return false;
}, [isFileOpened]);
const batchOpenSingleFile = useCallback(async (path) => {
if (workingRef.current) return;
if (filePath === path) return;
try {
setWorking(i18n.t('Loading file'));
await userOpenSingleFile({ path });
} catch (err) {
handleError(err);
} finally {
setWorking();
}
}, [userOpenSingleFile, setWorking, filePath]);
const batchFileJump = useCallback((direction) => {
const pathIndex = batchFiles.findIndex(({ path }) => path === filePath);
if (pathIndex === -1) return;
const nextFile = batchFiles[pathIndex + direction];
if (!nextFile) return;
batchOpenSingleFile(nextFile.path);
}, [filePath, batchFiles, batchOpenSingleFile]);
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;
@ -1619,7 +1569,7 @@ const App = memo(() => {
console.log(filePaths.join('\n'));
if (filePaths.length > 1) {
showMergeDialog(filePaths, mergeFiles);
showMultipleFilesDialog(filePaths, mergeFiles, batchLoadFiles);
return;
}
@ -1677,7 +1627,7 @@ const App = memo(() => {
} finally {
setWorking();
}
}, [addStreamSourceFile, checkFileOpened, enableAskForFileOpenAction, isFileOpened, loadEdlFile, mergeFiles, userOpenSingleFile, setWorking]);
}, [addStreamSourceFile, checkFileOpened, enableAskForFileOpenAction, isFileOpened, loadEdlFile, mergeFiles, userOpenSingleFile, setWorking, batchLoadFiles]);
const userHtml5ifyCurrentFile = useCallback(async () => {
if (!filePath) return;
@ -1708,6 +1658,103 @@ const App = memo(() => {
}
}, [customOutDir, filePath, html5ifyAndLoad, hasVideo, hasAudio, rememberConvertToSupportedFormat, setWorking]);
// TODO split up?
useEffect(() => {
if (exportConfirmVisible) return () => {};
const togglePlayNoReset = () => togglePlay();
const togglePlayReset = () => togglePlay(true);
const reducePlaybackRate = () => changePlaybackRate(-1);
const increasePlaybackRate = () => changePlaybackRate(1);
function seekBackwards() {
seekRel(keyboardNormalSeekSpeed * seekAccelerationRef.current * -1);
seekAccelerationRef.current *= keyboardSeekAccFactor;
}
function seekForwards() {
seekRel(keyboardNormalSeekSpeed * seekAccelerationRef.current);
seekAccelerationRef.current *= keyboardSeekAccFactor;
}
const seekReset = () => {
seekAccelerationRef.current = 1;
};
const seekBackwardsPercent = () => { seekRelPercent(-0.01); return false; };
const seekForwardsPercent = () => { seekRelPercent(0.01); return false; };
const seekBackwardsKeyframe = () => seekClosestKeyframe(-1);
const seekForwardsKeyframe = () => seekClosestKeyframe(1);
const seekBackwardsShort = () => shortStep(-1);
const seekForwardsShort = () => shortStep(1);
const jumpPrevSegment = () => jumpSeg(-1);
const jumpNextSegment = () => jumpSeg(1);
const zoomIn = () => { zoomRel(1); return false; };
const zoomOut = () => { zoomRel(-1); return false; };
const batchPreviousFile = () => batchFileJump(-1);
const batchNextFile = () => batchFileJump(1);
// mousetrap seems to be the only lib properly handling layouts that require shift to be pressed to get a particular key #520
// Also document.addEventListener needs custom handling of modifier keys or C will be triggered by CTRL+C, etc
const mousetrap = new Mousetrap();
// mousetrap.bind(':', () => console.log('test'));
mousetrap.bind('plus', () => addCutSegment());
mousetrap.bind('space', () => togglePlayReset());
mousetrap.bind('k', () => togglePlayNoReset());
mousetrap.bind('j', () => reducePlaybackRate());
mousetrap.bind('l', () => increasePlaybackRate());
mousetrap.bind('z', () => toggleComfortZoom());
mousetrap.bind(',', () => seekBackwardsShort());
mousetrap.bind('.', () => seekForwardsShort());
mousetrap.bind('c', () => capture());
mousetrap.bind('i', () => setCutStart());
mousetrap.bind('o', () => setCutEnd());
mousetrap.bind('backspace', () => removeCutSegment(currentSegIndexSafe));
mousetrap.bind('d', () => cleanupFiles());
mousetrap.bind('b', () => splitCurrentSegment());
mousetrap.bind('r', () => increaseRotation());
mousetrap.bind('left', () => seekBackwards());
mousetrap.bind('left', () => seekReset(), 'keyup');
mousetrap.bind(['ctrl+left', 'command+left'], () => seekBackwardsPercent());
mousetrap.bind('alt+left', () => seekBackwardsKeyframe());
mousetrap.bind('shift+left', () => jumpCutStart());
mousetrap.bind('right', () => seekForwards());
mousetrap.bind('right', () => seekReset(), 'keyup');
mousetrap.bind(['ctrl+right', 'command+right'], () => seekForwardsPercent());
mousetrap.bind('alt+right', () => seekForwardsKeyframe());
mousetrap.bind('shift+right', () => jumpCutEnd());
mousetrap.bind('up', () => jumpPrevSegment());
mousetrap.bind(['ctrl+up', 'command+up'], () => zoomIn());
mousetrap.bind(['shift+up'], () => batchPreviousFile());
mousetrap.bind('down', () => jumpNextSegment());
mousetrap.bind(['ctrl+down', 'command+down'], () => zoomOut());
mousetrap.bind(['shift+down'], () => batchNextFile());
// https://github.com/mifi/lossless-cut/issues/610
Mousetrap.bind(['ctrl+z', 'command+z'], (e) => {
e.preventDefault();
cutSegmentsHistory.back();
});
Mousetrap.bind(['ctrl+shift+z', 'command+shift+z'], (e) => {
e.preventDefault();
cutSegmentsHistory.forward();
});
mousetrap.bind(['enter'], () => {
onLabelSegmentPress(currentSegIndexSafe);
return false;
});
return () => mousetrap.reset();
}, [
addCutSegment, capture, changePlaybackRate, togglePlay, removeCutSegment,
setCutEnd, setCutStart, seekRel, seekRelPercent, shortStep, cleanupFiles, jumpSeg,
seekClosestKeyframe, zoomRel, toggleComfortZoom, splitCurrentSegment, exportConfirmVisible,
increaseRotation, jumpCutStart, jumpCutEnd, cutSegmentsHistory, keyboardSeekAccFactor,
keyboardNormalSeekSpeed, onLabelSegmentPress, currentSegIndexSafe, batchFileJump,
]);
const onVideoError = useCallback(async () => {
const { error } = videoRef.current;
if (!error) return;
@ -1870,9 +1917,11 @@ const App = memo(() => {
const showStreamsSelector = () => setStreamsSelectorShown(true);
const openSendReportDialog2 = () => { openSendReportDialogWithState(); };
const closeFile2 = () => { closeFile(); };
const closeBatch2 = () => { closeBatch(); };
electron.ipcRenderer.on('file-opened', fileOpened);
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('set-start-offset', setStartOffset);
@ -1896,6 +1945,7 @@ const App = memo(() => {
return () => {
electron.ipcRenderer.removeListener('file-opened', fileOpened);
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('set-start-offset', setStartOffset);
@ -1920,7 +1970,7 @@ const App = memo(() => {
mergeFiles, outputDir, filePath, customOutDir, startTimeOffset, userHtml5ifyCurrentFile,
extractAllStreams, userOpenFiles, openSendReportDialogWithState, setWorking,
loadEdlFile, cutSegments, apparentCutSegments, edlFilePath, toggleHelp, toggleSettings, ensureOutDirAccessible, html5ifyAndLoad, html5ify,
loadCutSegments, duration, checkFileOpened, loadMedia, fileFormat, reorderSegsByStartTime, closeFile, clearSegments, fixInvalidDuration, invertAllCutSegments,
loadCutSegments, duration, checkFileOpened, loadMedia, fileFormat, reorderSegsByStartTime, closeFile, closeBatch, clearSegments, fixInvalidDuration, invertAllCutSegments,
]);
async function showAddStreamSourceDialog() {
@ -2058,7 +2108,8 @@ const App = memo(() => {
return () => window.removeEventListener('keydown', keyScrollPreventer);
}, []);
const sideBarWidth = showSideBar && isFileOpened ? 200 : 0;
const rightBarWidth = showRightBar && isFileOpened ? 200 : 0;
const leftBarWidth = batchFiles.length > 0 ? 200 : 0;
const bottomBarHeight = 96 + ((hasAudio && waveformEnabled) || (hasVideo && thumbnailsEnabled) ? timelineHeight : 0);
@ -2173,7 +2224,7 @@ const App = memo(() => {
/>
</div>
{!isFileOpened && <NoFileLoaded topBarHeight={topBarHeight} bottomBarHeight={bottomBarHeight} mifiLink={mifiLink} toggleHelp={toggleHelp} currentCutSeg={currentCutSeg} simpleMode={simpleMode} toggleSimpleMode={toggleSimpleMode} />}
{!isFileOpened && <NoFileLoaded top={topBarHeight} bottom={bottomBarHeight} left={leftBarWidth} mifiLink={mifiLink} toggleHelp={toggleHelp} currentCutSeg={currentCutSeg} simpleMode={simpleMode} toggleSimpleMode={toggleSimpleMode} />}
<AnimatePresence>
{working && (
@ -2210,7 +2261,7 @@ const App = memo(() => {
)}
</AnimatePresence>
<div className="no-user-select" style={{ position: 'absolute', top: topBarHeight, left: 0, right: sideBarWidth, bottom: bottomBarHeight, visibility: !isFileOpened ? 'hidden' : undefined }} onWheel={onTimelineWheel}>
<div className="no-user-select" style={{ position: 'absolute', top: topBarHeight, left: leftBarWidth, right: rightBarWidth, bottom: bottomBarHeight, visibility: !isFileOpened ? 'hidden' : undefined }} onWheel={onTimelineWheel}>
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
<video
muted={playbackVolume === 0}
@ -2231,7 +2282,7 @@ const App = memo(() => {
{isRotationSet && !hideCanvasPreview && (
<div style={{
position: 'absolute', top: topBarHeight, marginTop: '1em', marginRight: '1em', right: sideBarWidth, color: 'white',
position: 'absolute', top: topBarHeight, marginTop: '1em', marginRight: '1em', right: rightBarWidth, left: leftBarWidth, color: 'white',
}}
>
{t('Rotation preview')}
@ -2239,36 +2290,66 @@ const App = memo(() => {
</div>
)}
<AnimatePresence>
{batchFiles.length > 0 && (
<motion.div
style={{ position: 'absolute', width: leftBarWidth, left: 0, bottom: bottomBarHeight, top: topBarHeight, background: timelineBackground, color: 'rgba(255,255,255,0.7)', display: 'flex', flexDirection: 'column' }}
initial={{ x: -leftBarWidth }}
animate={{ x: 0 }}
exit={{ x: -leftBarWidth }}
>
<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')}
<FaTimes size={18} role="button" style={{ cursor: 'pointer', color: 'white' }} onClick={() => closeBatch()} title={t('Close batch')} />
</div>
<div style={{ overflowX: 'hidden', overflowY: 'auto' }}>
{batchFiles.map(({ path, name }) => {
const isCurrent = path === filePath;
return (
<div 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' }} key={path} title={path} onClick={() => batchOpenSingleFile(path)}>
<FaFile size={14} style={{ color: primaryColor, marginLeft: 3, marginRight: 4, flexShrink: 0 }} />
<div style={{ wordBreak: 'break-all' }}>{name}</div>
<div style={{ flexGrow: 1 }} />
{isCurrent && <FaAngleRight size={14} style={{ color: 'white', marginRight: -3, flexShrink: 0 }} />}
</div>
);
})}
</div>
</motion.div>
)}
</AnimatePresence>
{isFileOpened && (
<>
<div
className="no-user-select"
style={{
position: 'absolute', right: sideBarWidth, bottom: bottomBarHeight, marginBottom: 10, color: 'rgba(255,255,255,0.7)', display: 'flex', alignItems: 'center',
position: 'absolute', right: rightBarWidth, bottom: bottomBarHeight, marginBottom: 10, color: 'rgba(255,255,255,0.7)', display: 'flex', alignItems: 'center',
}}
>
<VolumeControl playbackVolume={playbackVolume} setPlaybackVolume={setPlaybackVolume} usingDummyVideo={usingDummyVideo} />
{subtitleStreams.length > 0 && <SubtitleControl subtitleStreams={subtitleStreams} activeSubtitleStreamIndex={activeSubtitleStreamIndex} onActiveSubtitleChange={onActiveSubtitleChange} />}
{!showSideBar && (
{!showRightBar && (
<FaAngleLeft
title={t('Show sidebar')}
size={30}
role="button"
style={{ marginRight: 10 }}
onClick={toggleSideBar}
onClick={toggleRightBar}
/>
)}
</div>
<AnimatePresence>
{showSideBar && (
{showRightBar && (
<motion.div
style={{ position: 'absolute', width: sideBarWidth, right: 0, bottom: bottomBarHeight, top: topBarHeight, background: controlsBackground, color: 'rgba(255,255,255,0.7)', display: 'flex', flexDirection: 'column' }}
initial={{ x: sideBarWidth }}
style={{ position: 'absolute', width: rightBarWidth, right: 0, bottom: bottomBarHeight, top: topBarHeight, background: controlsBackground, color: 'rgba(255,255,255,0.7)', display: 'flex', flexDirection: 'column' }}
initial={{ x: rightBarWidth }}
animate={{ x: 0 }}
exit={{ x: sideBarWidth }}
exit={{ x: rightBarWidth }}
>
<SegmentList
simpleMode={simpleMode}
@ -2286,7 +2367,7 @@ const App = memo(() => {
segmentAtCursor={segmentAtCursor}
addCutSegment={addCutSegment}
removeCutSegment={removeCutSegment}
toggleSideBar={toggleSideBar}
toggleSideBar={toggleRightBar}
splitCurrentSegment={splitCurrentSegment}
enabledOutSegmentsRaw={enabledOutSegmentsRaw}
enabledOutSegments={enabledOutSegments}

@ -84,6 +84,10 @@ const HelpSheet = memo(({ visible, onTogglePress, ffmpegCommandLog, currentCutSe
<div><kbd>C</kbd> {t('Capture snapshot')}</div>
<div><kbd>D</kbd> {t('Delete source file')}</div>
<h2>{t('Batch file list')}</h2>
<div><kbd>SHIFT</kbd> + <kbd></kbd> {t('Previous file')}</div>
<div><kbd>SHIFT</kbd> + <kbd></kbd> {t('Next file')}</div>
<p style={{ fontWeight: 'bold' }}>{t('Hover mouse over buttons in the main interface to see which function they have')}</p>
<h1 style={{ marginTop: 40 }}>{t('Last ffmpeg commands')}</h1>

@ -9,11 +9,11 @@ import SimpleModeButton from './components/SimpleModeButton';
const electron = window.require('electron');
const NoFileLoaded = memo(({ topBarHeight, bottomBarHeight, mifiLink, toggleHelp, currentCutSeg, simpleMode, toggleSimpleMode }) => {
const NoFileLoaded = memo(({ top, bottom, left, mifiLink, toggleHelp, currentCutSeg, simpleMode, toggleSimpleMode }) => {
const { t } = useTranslation();
return (
<div className="no-user-select" style={{ position: 'fixed', left: 0, right: 0, top: topBarHeight, bottom: bottomBarHeight, border: '2vmin dashed #252525', color: '#505050', margin: '5vmin', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', whiteSpace: 'nowrap' }}>
<div className="no-user-select" style={{ position: 'fixed', left, right: 0, top, bottom, border: '2vmin dashed #252525', color: '#505050', margin: '5vmin', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', whiteSpace: 'nowrap' }}>
<div style={{ fontSize: '6vmin', textTransform: 'uppercase' }}>{t('DROP FILE(S)')}</div>
<div style={{ fontSize: '4vmin', color: '#777', cursor: 'pointer' }} role="button" onClick={toggleHelp}>

@ -92,7 +92,7 @@ const Segment = memo(({ seg, index, currentSegIndex, formatTimecode, getFrameCou
onClick={() => !invertCutSegments && onClick(index)}
onDoubleClick={onDoubleClick}
positionTransition
style={{ cursor: 'grab', 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 }}
style={{ cursor: 'grab', originY: 0, margin: '5px 0', background: 'rgba(0,0,0,0.1)', 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 }}
@ -237,7 +237,7 @@ const SegmentList = memo(({
<FaAngleRight
title={t('Close sidebar')}
size={18}
style={{ verticalAlign: 'middle', color: 'white' }}
style={{ verticalAlign: 'middle', color: 'white', cursor: 'pointer' }}
role="button"
onClick={toggleSideBar}
/>

@ -352,12 +352,6 @@ export function openAbout() {
}
export async function showMergeDialog(paths, onMergeClick) {
if (!paths) return;
if (paths.length < 2) {
errorToast(i18n.t('More than one file must be selected'));
return;
}
let swalElem;
let outPaths = paths;
let allStreams = false;
@ -382,6 +376,28 @@ export async function showMergeDialog(paths, onMergeClick) {
}
}
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 }) {
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');
@ -391,7 +407,12 @@ export async function showOpenAndMergeDialog({ defaultPath, onMergeClick }) {
properties: ['openFile', 'multiSelections'],
message,
});
if (canceled) return;
if (canceled || !filePaths) return;
if (filePaths.length < 2) {
errorToast(i18n.t('More than one file must be selected'));
return;
}
showMergeDialog(filePaths, onMergeClick);
}

Loading…
Cancel
Save