|
|
|
@ -6,7 +6,7 @@ import { FiScissors } from 'react-icons/fi';
|
|
|
|
import { AnimatePresence, motion } from 'framer-motion';
|
|
|
|
import { AnimatePresence, motion } from 'framer-motion';
|
|
|
|
import Swal from 'sweetalert2';
|
|
|
|
import Swal from 'sweetalert2';
|
|
|
|
import Lottie from 'react-lottie';
|
|
|
|
import Lottie from 'react-lottie';
|
|
|
|
import { SideSheet, Button, Position } from 'evergreen-ui';
|
|
|
|
import { SideSheet, Button, Position, Table, SegmentedControl, Checkbox } from 'evergreen-ui';
|
|
|
|
import { useStateWithHistory } from 'react-use/lib/useStateWithHistory';
|
|
|
|
import { useStateWithHistory } from 'react-use/lib/useStateWithHistory';
|
|
|
|
|
|
|
|
|
|
|
|
import fromPairs from 'lodash/fromPairs';
|
|
|
|
import fromPairs from 'lodash/fromPairs';
|
|
|
|
@ -1220,9 +1220,9 @@ const App = memo(() => {
|
|
|
|
<option key={f} value={f}>{f} - {name}</option>
|
|
|
|
<option key={f} value={f}>{f} - {name}</option>
|
|
|
|
));
|
|
|
|
));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
function renderOutFmt({ width } = {}) {
|
|
|
|
function renderOutFmt(style = {}) {
|
|
|
|
return (
|
|
|
|
return (
|
|
|
|
<select style={{ width }} defaultValue="" value={fileFormat} title="Output format" onChange={withBlur(e => setFileFormat(e.target.value))}>
|
|
|
|
<select style={style} defaultValue="" value={fileFormat} title="Output format" onChange={withBlur(e => setFileFormat(e.target.value))}>
|
|
|
|
<option key="disabled1" value="" disabled>Format</option>
|
|
|
|
<option key="disabled1" value="" disabled>Format</option>
|
|
|
|
|
|
|
|
|
|
|
|
{detectedFileFormat && (
|
|
|
|
{detectedFileFormat && (
|
|
|
|
@ -1252,145 +1252,137 @@ const App = memo(() => {
|
|
|
|
);
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const renderSettings = () => (
|
|
|
|
const renderSettings = () => {
|
|
|
|
<Fragment>
|
|
|
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
|
|
|
<tr>
|
|
|
|
const Row = (props) => <Table.Row height="auto" paddingY={12} {...props} />;
|
|
|
|
<td>Output format (default autodetected)</td>
|
|
|
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
|
|
|
<td style={{ width: '50%' }}>{renderOutFmt()}</td>
|
|
|
|
const KeyCell = (props) => <Table.TextCell textProps={{ whiteSpace: 'auto' }} {...props} />;
|
|
|
|
</tr>
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
<tr>
|
|
|
|
<Fragment>
|
|
|
|
<td>
|
|
|
|
<Row>
|
|
|
|
Working directory<br />
|
|
|
|
<KeyCell textProps={{ whiteSpace: 'auto' }}>Output format (default autodetected)</KeyCell>
|
|
|
|
This is where working files, exported files, project files (CSV) are stored.
|
|
|
|
<Table.TextCell>{renderOutFmt({ width: '100%' })}</Table.TextCell>
|
|
|
|
</td>
|
|
|
|
</Row>
|
|
|
|
<td>
|
|
|
|
|
|
|
|
<button
|
|
|
|
<Row>
|
|
|
|
type="button"
|
|
|
|
<KeyCell>
|
|
|
|
onClick={setOutputDir}
|
|
|
|
Working directory<br />
|
|
|
|
>
|
|
|
|
This is where working files, exported files, project files (CSV) are stored.
|
|
|
|
{customOutDir ? 'Custom working directory' : 'Same directory as input file'}
|
|
|
|
</KeyCell>
|
|
|
|
</button>
|
|
|
|
<Table.TextCell>
|
|
|
|
<div>{customOutDir}</div>
|
|
|
|
<button
|
|
|
|
</td>
|
|
|
|
type="button"
|
|
|
|
</tr>
|
|
|
|
onClick={setOutputDir}
|
|
|
|
|
|
|
|
>
|
|
|
|
<tr>
|
|
|
|
{customOutDir ? 'Custom working directory' : 'Same directory as input file'}
|
|
|
|
<td>Auto merge segments to one file after export?</td>
|
|
|
|
</button>
|
|
|
|
<td>
|
|
|
|
<div>{customOutDir}</div>
|
|
|
|
<button
|
|
|
|
</Table.TextCell>
|
|
|
|
type="button"
|
|
|
|
</Row>
|
|
|
|
onClick={toggleAutoMerge}
|
|
|
|
|
|
|
|
>
|
|
|
|
<Row>
|
|
|
|
{autoMerge ? 'Auto merge segments to one file' : 'Export separate files'}
|
|
|
|
<KeyCell>Auto merge segments to one file during export or export to separate files?</KeyCell>
|
|
|
|
</button>
|
|
|
|
<Table.TextCell>
|
|
|
|
</td>
|
|
|
|
<SegmentedControl
|
|
|
|
</tr>
|
|
|
|
options={[{ label: 'Auto merge', value: 'automerge' }, { label: 'Separate', value: 'separate' }]}
|
|
|
|
|
|
|
|
value={autoMerge ? 'automerge' : 'separate'}
|
|
|
|
<tr>
|
|
|
|
onChange={value => setAutoMerge(value === 'automerge')}
|
|
|
|
<td>keyframe cut mode</td>
|
|
|
|
/>
|
|
|
|
<td>
|
|
|
|
</Table.TextCell>
|
|
|
|
<button
|
|
|
|
</Row>
|
|
|
|
type="button"
|
|
|
|
|
|
|
|
onClick={toggleKeyframeCut}
|
|
|
|
<Row>
|
|
|
|
>
|
|
|
|
<KeyCell>
|
|
|
|
{keyframeCut ? 'Nearest keyframe cut - will cut at the nearest keyframe' : 'Normal cut - cut accurate position but could leave an empty portion'}
|
|
|
|
Keyframe cut mode<br />
|
|
|
|
</button>
|
|
|
|
<b>Nearest keyframe</b>: Cut at the nearest keyframe (not accurate time.)<br />
|
|
|
|
</td>
|
|
|
|
<b>Normal cut</b>: Accurate time but could leave an empty portion at the beginning of the video.<br />
|
|
|
|
</tr>
|
|
|
|
</KeyCell>
|
|
|
|
|
|
|
|
<Table.TextCell>
|
|
|
|
<tr>
|
|
|
|
<SegmentedControl
|
|
|
|
<td>
|
|
|
|
options={[{ label: 'Nearest keyframe', value: 'keyframe' }, { label: 'Normal cut', value: 'normal' }]}
|
|
|
|
Discard (cut away) or keep selected segments from video when exporting
|
|
|
|
value={keyframeCut ? 'keyframe' : 'normal'}
|
|
|
|
</td>
|
|
|
|
onChange={value => setKeyframeCut(value === 'keyframe')}
|
|
|
|
<td>
|
|
|
|
/>
|
|
|
|
<button
|
|
|
|
</Table.TextCell>
|
|
|
|
type="button"
|
|
|
|
</Row>
|
|
|
|
onClick={withBlur(() => setInvertCutSegments(v => !v))}
|
|
|
|
|
|
|
|
>
|
|
|
|
<Row>
|
|
|
|
{invertCutSegments ? 'Discard' : 'Keep'}
|
|
|
|
<KeyCell>
|
|
|
|
</button>
|
|
|
|
<span role="img" aria-label="Yin Yang">☯️</span> Choose cutting mode: Cut away or keep selected segments from video when exporting?<br />
|
|
|
|
</td>
|
|
|
|
When <b>Keep</b> is selected, the video inside segments will be kept, while the video outside will be discarded.<br />
|
|
|
|
</tr>
|
|
|
|
When <b>Cut away</b> is selected, the video inside segments will be discarded, while the video surrounding them will be kept.
|
|
|
|
|
|
|
|
</KeyCell>
|
|
|
|
<tr>
|
|
|
|
<Table.TextCell>
|
|
|
|
<td>
|
|
|
|
<SegmentedControl
|
|
|
|
Discard audio?
|
|
|
|
options={[{ label: 'Cut away', value: 'discard' }, { label: 'Keep', value: 'keep' }]}
|
|
|
|
</td>
|
|
|
|
value={invertCutSegments ? 'discard' : 'keep'}
|
|
|
|
<td>
|
|
|
|
onChange={value => setInvertCutSegments(value === 'discard')}
|
|
|
|
<button
|
|
|
|
/>
|
|
|
|
type="button"
|
|
|
|
</Table.TextCell>
|
|
|
|
onClick={toggleStripAudio}
|
|
|
|
</Row>
|
|
|
|
>
|
|
|
|
|
|
|
|
{copyAnyAudioTrack ? 'Keep audio tracks' : 'Discard all audio tracks'}
|
|
|
|
<Row>
|
|
|
|
</button>
|
|
|
|
<KeyCell>
|
|
|
|
</td>
|
|
|
|
Extract unprocessable tracks to separate files or discard them?<br />
|
|
|
|
</tr>
|
|
|
|
(data tracks such as GoPro GPS, telemetry etc. are not copied over by default because ffmpeg cannot cut them, thus they will cause the media duration to stay the same after cutting video/audio)
|
|
|
|
|
|
|
|
</KeyCell>
|
|
|
|
<tr>
|
|
|
|
<Table.TextCell>
|
|
|
|
<td>
|
|
|
|
<SegmentedControl
|
|
|
|
Extract unprocessable tracks to separate files?<br />
|
|
|
|
options={[{ label: 'Extract', value: 'extract' }, { label: 'Discard', value: 'discard' }]}
|
|
|
|
(data tracks such as GoPro GPS, telemetry etc. are not copied over by default because ffmpeg cannot cut them, thus they will cause the media duration to stay the same after cutting video/audio)
|
|
|
|
value={autoExportExtraStreams ? 'extract' : 'discard'}
|
|
|
|
</td>
|
|
|
|
onChange={value => setAutoExportExtraStreams(value === 'extract')}
|
|
|
|
<td>
|
|
|
|
/>
|
|
|
|
<button
|
|
|
|
</Table.TextCell>
|
|
|
|
type="button"
|
|
|
|
</Row>
|
|
|
|
onClick={() => setAutoExportExtraStreams(v => !v)}
|
|
|
|
|
|
|
|
>
|
|
|
|
<Row>
|
|
|
|
{autoExportExtraStreams ? 'Extract unprocessable tracks' : 'Discard all unprocessable tracks'}
|
|
|
|
<KeyCell>
|
|
|
|
</button>
|
|
|
|
Auto save project file?<br />
|
|
|
|
</td>
|
|
|
|
The project will be stored along with the output files as a CSV file
|
|
|
|
</tr>
|
|
|
|
</KeyCell>
|
|
|
|
|
|
|
|
<Table.TextCell>
|
|
|
|
<tr>
|
|
|
|
<Checkbox
|
|
|
|
<td>
|
|
|
|
label="Auto save project"
|
|
|
|
Auto save project?<br />
|
|
|
|
checked={autoSaveProjectFile}
|
|
|
|
The project will be stored along with the output files as a CSV file
|
|
|
|
onChange={e => setAutoSaveProjectFile(e.target.checked)}
|
|
|
|
</td>
|
|
|
|
/>
|
|
|
|
<td>
|
|
|
|
</Table.TextCell>
|
|
|
|
<button
|
|
|
|
</Row>
|
|
|
|
type="button"
|
|
|
|
|
|
|
|
onClick={() => setAutoSaveProjectFile(v => !v)}
|
|
|
|
<Row>
|
|
|
|
>
|
|
|
|
<KeyCell>
|
|
|
|
{autoSaveProjectFile ? 'Auto save project' : 'Don\'t save project file'}
|
|
|
|
Snapshot capture format
|
|
|
|
</button>
|
|
|
|
</KeyCell>
|
|
|
|
</td>
|
|
|
|
<Table.TextCell>
|
|
|
|
</tr>
|
|
|
|
{renderCaptureFormatButton()}
|
|
|
|
|
|
|
|
</Table.TextCell>
|
|
|
|
<tr>
|
|
|
|
</Row>
|
|
|
|
<td>
|
|
|
|
|
|
|
|
Snapshot capture format
|
|
|
|
<Row>
|
|
|
|
</td>
|
|
|
|
<KeyCell>In timecode show</KeyCell>
|
|
|
|
<td>
|
|
|
|
<Table.TextCell>
|
|
|
|
{renderCaptureFormatButton()}
|
|
|
|
<SegmentedControl
|
|
|
|
</td>
|
|
|
|
options={[{ label: 'Frame numbers', value: 'frames' }, { label: 'Millisecond fractions', value: 'ms' }]}
|
|
|
|
</tr>
|
|
|
|
value={timecodeShowFrames ? 'frames' : 'ms'}
|
|
|
|
|
|
|
|
onChange={value => setTimecodeShowFrames(value === 'frames')}
|
|
|
|
<tr>
|
|
|
|
/>
|
|
|
|
<td>In timecode show</td>
|
|
|
|
</Table.TextCell>
|
|
|
|
<td>
|
|
|
|
</Row>
|
|
|
|
<button
|
|
|
|
|
|
|
|
type="button"
|
|
|
|
<Row>
|
|
|
|
onClick={() => setTimecodeShowFrames(v => !v)}
|
|
|
|
<KeyCell>Ask for confirmation when closing app?</KeyCell>
|
|
|
|
>
|
|
|
|
<Table.TextCell>
|
|
|
|
{timecodeShowFrames ? 'Frame numbers' : 'Millisecond fractions'}
|
|
|
|
<Checkbox
|
|
|
|
</button>
|
|
|
|
label="Ask before closing"
|
|
|
|
</td>
|
|
|
|
checked={askBeforeClose}
|
|
|
|
</tr>
|
|
|
|
onChange={e => setAskBeforeClose(e.target.checked)}
|
|
|
|
|
|
|
|
/>
|
|
|
|
<tr>
|
|
|
|
</Table.TextCell>
|
|
|
|
<td>Ask for confirmation when closing app?</td>
|
|
|
|
</Row>
|
|
|
|
<td>
|
|
|
|
</Fragment>
|
|
|
|
<button
|
|
|
|
);
|
|
|
|
type="button"
|
|
|
|
};
|
|
|
|
onClick={() => setAskBeforeClose(v => !v)}
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
{askBeforeClose ? 'Ask before closing' : 'Don\'t ask before closing'}
|
|
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
</td>
|
|
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
|
|
</Fragment>
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
useEffect(() => {
|
|
|
|
loadMifiLink().then(setMifiLink);
|
|
|
|
loadMifiLink().then(setMifiLink);
|
|
|
|
|