improve export options in simple mode

pull/2599/head
Mikael Finstad 8 months ago
parent 2511045acd
commit 251104c11e
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26

@ -0,0 +1,14 @@
import { motion, HTMLMotionProps } from 'framer-motion';
export default function AnimatedTr(props: HTMLMotionProps<'tr'>) {
return (
<motion.tr
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
transition={{ duration: 0.5, ease: 'easeIn' }}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
/>
);
}

@ -1,4 +1,4 @@
import { CSSProperties, Dispatch, ReactNode, SetStateAction, memo, useCallback, useMemo } from 'react';
import { CSSProperties, Dispatch, ReactNode, SetStateAction, memo, useCallback, useMemo, useState } from 'react';
import { FaExclamationTriangle, FaInfoCircle, FaRegCheckCircle } from 'react-icons/fa';
import i18n from 'i18next';
import { useTranslation, Trans } from 'react-i18next';
@ -27,11 +27,12 @@ import { UseSegments } from '../hooks/useSegments';
import ExportSheet from './ExportSheet';
import ToggleExportConfirm from './ToggleExportConfirm';
import { LossyMode } from '../../../main';
import AnimatedTr from './AnimatedTr';
const outDirStyle: CSSProperties = { ...highlightedTextStyle, wordBreak: 'break-all', cursor: 'pointer' };
const noticeStyle: CSSProperties = { fontSize: '85%', marginBottom: '.5em' };
const noticeStyle: CSSProperties = { marginBottom: '.5em' };
const infoStyle: CSSProperties = { ...noticeStyle, color: 'var(--blue-12)' };
const warningStyle: CSSProperties = { ...noticeStyle, color: 'var(--orange-8)' };
@ -62,11 +63,11 @@ function renderNoticeIcon(notice: { warning?: boolean | undefined } | undefined,
);
}
function renderNotice(notice: { warning?: boolean | undefined, text: ReactNode } | undefined) {
function renderNotice(notice: { warning?: boolean | undefined, text: ReactNode } | undefined, { key, style }: { key?: string, style?: CSSProperties }) {
if (notice == null) return null;
const { warning, text } = notice;
return (
<div style={{ ...(warning ? warningStyle : infoStyle), gap: '0 .5em' }}>
<div key={key} style={{ ...(warning ? warningStyle : infoStyle), gap: '0 .5em', ...style }}>
{renderNoticeIcon({ warning })} {text}
</div>
);
@ -129,7 +130,9 @@ function ExportConfirm({
}) {
const { t } = useTranslation();
const { changeOutDir, keyframeCut, toggleKeyframeCut, preserveMovData, setPreserveMovData, preserveMetadata, setPreserveMetadata, preserveChapters, setPreserveChapters, movFastStart, setMovFastStart, avoidNegativeTs, setAvoidNegativeTs, autoDeleteMergedSegments, exportConfirmEnabled, toggleExportConfirmEnabled, segmentsToChapters, setSegmentsToChapters, preserveMetadataOnMerge, setPreserveMetadataOnMerge, enableSmartCut, setEnableSmartCut, effectiveExportMode, enableOverwriteOutput, setEnableOverwriteOutput, ffmpegExperimental, setFfmpegExperimental, cutFromAdjustmentFrames, setCutFromAdjustmentFrames, cutToAdjustmentFrames, setCutToAdjustmentFrames, setCutFileTemplate, setCutMergedFileTemplate } = useUserSettings();
const { changeOutDir, keyframeCut, toggleKeyframeCut, preserveMovData, setPreserveMovData, preserveMetadata, setPreserveMetadata, preserveChapters, setPreserveChapters, movFastStart, setMovFastStart, avoidNegativeTs, setAvoidNegativeTs, autoDeleteMergedSegments, exportConfirmEnabled, toggleExportConfirmEnabled, segmentsToChapters, setSegmentsToChapters, preserveMetadataOnMerge, setPreserveMetadataOnMerge, enableSmartCut, setEnableSmartCut, effectiveExportMode, enableOverwriteOutput, setEnableOverwriteOutput, ffmpegExperimental, setFfmpegExperimental, cutFromAdjustmentFrames, setCutFromAdjustmentFrames, cutToAdjustmentFrames, setCutToAdjustmentFrames, setCutFileTemplate, setCutMergedFileTemplate, simpleMode } = useUserSettings();
const [showAdvanced, setShowAdvanced] = useState(!simpleMode);
const togglePreserveChapters = useCallback(() => setPreserveChapters((val) => !val), [setPreserveChapters]);
const togglePreserveMovData = useCallback(() => setPreserveMovData((val) => !val), [setPreserveMovData]);
@ -171,6 +174,7 @@ function ExportConfirm({
if (effectiveExportMode === 'separate' && !areWeCutting) {
generic.push({ text: t('Exporting whole file without cutting, because there are no segments to export.') });
}
// https://github.com/mifi/lossless-cut/issues/1809
if (areWeCutting && outFormat === 'flac') {
generic.push({ warning: true, text: t('There is a known issue in FFmpeg with cutting FLAC files. The file will be re-encoded, which is still lossless, but the export may be slower.') });
@ -302,16 +306,14 @@ function ExportConfirm({
>
<table className={styles['options']}>
<tbody>
{notices.generic.map(({ warning, text }) => (
<tr key={text}>
<td colSpan={2}>
<div style={{ ...(warning ? { ...noticeStyle, color: 'var(--orange-8)' } : infoStyle), display: 'flex', alignItems: 'center', gap: '0 .5em' }}>
{renderNotice({ warning, text })}
</div>
</td>
<td />
</tr>
))}
<tr>
<td colSpan={2}>
{notices.generic.map(({ warning, text }) => (
renderNotice({ warning, text }, { key: text })
))}
</td>
<td />
</tr>
{segmentsOrInverse.selected.length !== segmentsOrInverse.all.length && (
<tr>
@ -325,7 +327,7 @@ function ExportConfirm({
<tr>
<td>
{segmentsOrInverse.selected.length > 1 ? t('Export mode for {{segments}} segments', { segments: segmentsOrInverse.selected.length }) : t('Export mode')}
{renderNotice(notices.specific['exportMode'])}
{renderNotice(notices.specific['exportMode'], { style: { fontSize: '85%' } })}
</td>
<td>
<ExportModeButton selectedSegments={segmentsOrInverse.selected} style={{ height: '1.8em' }} />
@ -350,7 +352,7 @@ function ExportConfirm({
<tr>
<td>
<Trans>Input has {{ numStreamsTotal }} tracks</Trans>
{renderNotice(notices.specific['problematicStreams'])}
{renderNotice(notices.specific['problematicStreams'], { style: { fontSize: '85%' } })}
</td>
<td>
<HighlightedText style={{ cursor: 'pointer' }} onClick={onShowStreamsSelectorClick}><Trans>Keeping {{ numStreamsToCopy }} tracks</Trans></HighlightedText>
@ -395,7 +397,7 @@ function ExportConfirm({
<tr>
<td>
{t('Overwrite existing files')}
{renderNotice(notices.specific['overwriteOutput'])}
{renderNotice(notices.specific['overwriteOutput'], { style: { fontSize: '85%' } })}
</td>
<td>
<Switch checked={enableOverwriteOutput} onCheckedChange={setEnableOverwriteOutput} />
@ -418,224 +420,240 @@ function ExportConfirm({
<td />
</tr>
{areWeCutting && (
<>
<tr>
<td>
{t('Shift all start times')}
</td>
<td>
<ShiftTimes values={adjustCutFromValues} num={cutFromAdjustmentFrames} setNum={setCutFromAdjustmentFrames} />
</td>
<td>
<HelpIcon onClick={onCutFromAdjustmentFramesHelpPress} />
</td>
</tr>
<tr>
<td>
{t('Shift all end times')}
</td>
<td>
<ShiftTimes values={adjustCutToValues} num={cutToAdjustmentFrames} setNum={setCutToAdjustmentFrames} />
</td>
<td />
</tr>
</>
{simpleMode && (
<tr>
<td>
{t('Show advanced options')}
</td>
<td>
<Switch checked={showAdvanced} onCheckedChange={setShowAdvanced} />
</td>
<td />
</tr>
)}
{isMov && (
{showAdvanced && (
<>
<tr>
{areWeCutting && (
<>
<AnimatedTr>
<td>
{t('Shift all start times')}
</td>
<td>
<ShiftTimes values={adjustCutFromValues} num={cutFromAdjustmentFrames} setNum={setCutFromAdjustmentFrames} />
</td>
<td>
<HelpIcon onClick={onCutFromAdjustmentFramesHelpPress} />
</td>
</AnimatedTr>
<AnimatedTr>
<td>
{t('Shift all end times')}
</td>
<td>
<ShiftTimes values={adjustCutToValues} num={cutToAdjustmentFrames} setNum={setCutToAdjustmentFrames} />
</td>
<td />
</AnimatedTr>
</>
)}
{isMov && (
<>
<AnimatedTr>
<td>
{t('Enable MOV Faststart?')}
</td>
<td>
<Switch checked={movFastStart} onCheckedChange={toggleMovFastStart} />
{renderNotice(notices.specific['movFastStart'], { style: { fontSize: '85%' } })}
</td>
<td>
{renderNoticeIcon(notices.specific['movFastStart'], rightIconStyle) ?? <HelpIcon onClick={onMovFastStartHelpPress} />}
</td>
</AnimatedTr>
<AnimatedTr>
<td>
{t('Preserve all MP4/MOV metadata?')}
{renderNotice(notices.specific['preserveMovData'], { style: { fontSize: '85%' } })}
</td>
<td>
<Switch checked={preserveMovData} onCheckedChange={togglePreserveMovData} />
</td>
<td>
{renderNoticeIcon(notices.specific['preserveMovData'], rightIconStyle) ?? <HelpIcon onClick={onPreserveMovDataHelpPress} />}
</td>
</AnimatedTr>
</>
)}
<AnimatedTr>
<td>
{t('Enable MOV Faststart?')}
{t('Preserve chapters')}
</td>
<td>
<Switch checked={movFastStart} onCheckedChange={toggleMovFastStart} />
{renderNotice(notices.specific['movFastStart'])}
<Switch checked={preserveChapters} onCheckedChange={togglePreserveChapters} />
</td>
<td>
{renderNoticeIcon(notices.specific['movFastStart'], rightIconStyle) ?? <HelpIcon onClick={onMovFastStartHelpPress} />}
<HelpIcon onClick={onPreserveChaptersPress} />
</td>
</tr>
</AnimatedTr>
<tr>
<AnimatedTr>
<td>
{t('Preserve all MP4/MOV metadata?')}
{renderNotice(notices.specific['preserveMovData'])}
{t('Preserve metadata')}
</td>
<td>
<Switch checked={preserveMovData} onCheckedChange={togglePreserveMovData} />
<Select value={preserveMetadata} onChange={(e) => setPreserveMetadata(e.target.value as PreserveMetadata)} style={{ height: 20, marginLeft: 5 }}>
<option value={'default' satisfies PreserveMetadata}>{t('Default')}</option>
<option value={'none' satisfies PreserveMetadata}>{t('None')}</option>
<option value={'nonglobal' satisfies PreserveMetadata}>{t('Non-global')}</option>
</Select>
</td>
<td>
{renderNoticeIcon(notices.specific['preserveMovData'], rightIconStyle) ?? <HelpIcon onClick={onPreserveMovDataHelpPress} />}
<HelpIcon onClick={onPreserveMetadataHelpPress} />
</td>
</tr>
</>
)}
</AnimatedTr>
{willMerge && (
<>
<AnimatedTr>
<td>
{t('Create chapters from merged segments? (slow)')}
</td>
<td>
<Switch checked={segmentsToChapters} onCheckedChange={toggleSegmentsToChapters} />
</td>
<td>
<HelpIcon onClick={onSegmentsToChaptersHelpPress} />
</td>
</AnimatedTr>
<AnimatedTr>
<td>
{t('Preserve original metadata when merging? (slow)')}
</td>
<td>
<Switch checked={preserveMetadataOnMerge} onCheckedChange={togglePreserveMetadataOnMerge} />
</td>
<td>
<HelpIcon onClick={onPreserveMetadataOnMergeHelpPress} />
</td>
</AnimatedTr>
</>
)}
<tr>
<td>
{t('Preserve chapters')}
</td>
<td>
<Switch checked={preserveChapters} onCheckedChange={togglePreserveChapters} />
</td>
<td>
<HelpIcon onClick={onPreserveChaptersPress} />
</td>
</tr>
{areWeCutting && (
<>
<AnimatedTr>
<td>
{t('Smart cut (experimental):')}
{renderNotice(notices.specific['smartCut'], { style: { fontSize: '85%' } })}
</td>
<td>
<Switch checked={enableSmartCut} onCheckedChange={() => setEnableSmartCut((v) => !v)} />
</td>
<td>
{renderNoticeIcon(notices.specific['smartCut'], rightIconStyle) ?? <HelpIcon onClick={onSmartCutHelpPress} />}
</td>
</AnimatedTr>
{!isEncoding && (
<AnimatedTr>
<td>
{t('Keyframe cut mode')}
{renderNotice(notices.specific['cutMode'], { style: { fontSize: '85%' } })}
</td>
<td>
<Switch checked={keyframeCut} onCheckedChange={() => toggleKeyframeCut()} />
</td>
<td>
{renderNoticeIcon(notices.specific['cutMode'], rightIconStyle) ?? <HelpIcon onClick={onKeyframeCutHelpPress} />}
</td>
</AnimatedTr>
)}
</>
)}
<tr>
<td>
{t('Preserve metadata')}
</td>
<td>
<Select value={preserveMetadata} onChange={(e) => setPreserveMetadata(e.target.value as PreserveMetadata)} style={{ height: 20, marginLeft: 5 }}>
<option value={'default' satisfies PreserveMetadata}>{t('Default')}</option>
<option value={'none' satisfies PreserveMetadata}>{t('None')}</option>
<option value={'nonglobal' satisfies PreserveMetadata}>{t('Non-global')}</option>
</Select>
</td>
<td>
<HelpIcon onClick={onPreserveMetadataHelpPress} />
</td>
</tr>
{willMerge && (
<>
<tr>
<td>
{t('Create chapters from merged segments? (slow)')}
</td>
<td>
<Switch checked={segmentsToChapters} onCheckedChange={toggleSegmentsToChapters} />
</td>
<td>
<HelpIcon onClick={onSegmentsToChaptersHelpPress} />
</td>
</tr>
{isEncoding && (
<AnimatedTr>
<td>
{t('Smart cut auto detect bitrate')}
</td>
<td>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}>
{encBitrate != null && (
<>
<TextInput value={encBitrate} onChange={handleEncBitrateChange} style={{ width: '4em', flexGrow: 0, marginRight: '.3em' }} />
<span style={{ marginRight: '.3em' }}>{t('kbit/s')}</span>
</>
)}
<span><Switch checked={encBitrate == null} onCheckedChange={handleEncBitrateToggle} /></span>
</div>
</td>
<td />
</AnimatedTr>
)}
<tr>
{lossyMode != null && (
<AnimatedTr>
<td>
Lossy mode
</td>
<td>
<Switch disabled checked={lossyMode != null} />
<div>{lossyMode.videoEncoder}</div>
</td>
<td />
</AnimatedTr>
)}
{!isEncoding && (
<AnimatedTr>
<td>
&quot;ffmpeg&quot; <code className="highlighted">avoid_negative_ts</code>
{renderNotice(notices.specific['avoidNegativeTs'], { style: { fontSize: '85%' } })}
</td>
<td>
<Select value={avoidNegativeTs} onChange={(e) => setAvoidNegativeTs(e.target.value as AvoidNegativeTs)} style={{ height: 20, marginLeft: 5 }}>
<option value={'auto' satisfies AvoidNegativeTs}>auto</option>
<option value={'make_zero' satisfies AvoidNegativeTs}>make_zero</option>
<option value={'make_non_negative' satisfies AvoidNegativeTs}>make_non_negative</option>
<option value={'disabled' satisfies AvoidNegativeTs}>disabled</option>
</Select>
</td>
<td>
{renderNoticeIcon(notices.specific['avoidNegativeTs'], rightIconStyle) ?? <HelpIcon onClick={onAvoidNegativeTsHelpPress} />}
</td>
</AnimatedTr>
)}
<AnimatedTr>
<td>
{t('Preserve original metadata when merging? (slow)')}
{t('"ffmpeg" experimental flag')}
</td>
<td>
<Switch checked={preserveMetadataOnMerge} onCheckedChange={togglePreserveMetadataOnMerge} />
<Switch checked={ffmpegExperimental} onCheckedChange={setFfmpegExperimental} />
</td>
<td>
<HelpIcon onClick={onPreserveMetadataOnMergeHelpPress} />
<HelpIcon onClick={onFfmpegExperimentalHelpPress} />
</td>
</tr>
</>
)}
</AnimatedTr>
{areWeCutting && (
<>
<tr>
<AnimatedTr>
<td>
{t('Smart cut (experimental):')}
{renderNotice(notices.specific['smartCut'])}
{t('More settings')}
</td>
<td>
<Switch checked={enableSmartCut} onCheckedChange={() => setEnableSmartCut((v) => !v)} />
<IoIosSettings size={24} role="button" onClick={toggleSettings} style={{ marginLeft: 5 }} />
</td>
<td>
{renderNoticeIcon(notices.specific['smartCut'], rightIconStyle) ?? <HelpIcon onClick={onSmartCutHelpPress} />}
</td>
</tr>
{!isEncoding && (
<tr>
<td>
{t('Keyframe cut mode')}
{renderNotice(notices.specific['cutMode'])}
</td>
<td>
<Switch checked={keyframeCut} onCheckedChange={() => toggleKeyframeCut()} />
</td>
<td>
{renderNoticeIcon(notices.specific['cutMode'], rightIconStyle) ?? <HelpIcon onClick={onKeyframeCutHelpPress} />}
</td>
</tr>
)}
<td />
</AnimatedTr>
</>
)}
{isEncoding && (
<tr>
<td>
{t('Smart cut auto detect bitrate')}
</td>
<td>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}>
{encBitrate != null && (
<>
<TextInput value={encBitrate} onChange={handleEncBitrateChange} style={{ width: '4em', flexGrow: 0, marginRight: '.3em' }} />
<span style={{ marginRight: '.3em' }}>{t('kbit/s')}</span>
</>
)}
<span><Switch checked={encBitrate == null} onCheckedChange={handleEncBitrateToggle} /></span>
</div>
</td>
<td />
</tr>
)}
{lossyMode != null && (
<tr>
<td>
Lossy mode
</td>
<td>
<Switch disabled checked={lossyMode != null} />
<div>{lossyMode.videoEncoder}</div>
</td>
<td />
</tr>
)}
{!isEncoding && (
<tr>
<td>
&quot;ffmpeg&quot; <code className="highlighted">avoid_negative_ts</code>
{renderNotice(notices.specific['avoidNegativeTs'])}
</td>
<td>
<Select value={avoidNegativeTs} onChange={(e) => setAvoidNegativeTs(e.target.value as AvoidNegativeTs)} style={{ height: 20, marginLeft: 5 }}>
<option value={'auto' satisfies AvoidNegativeTs}>auto</option>
<option value={'make_zero' satisfies AvoidNegativeTs}>make_zero</option>
<option value={'make_non_negative' satisfies AvoidNegativeTs}>make_non_negative</option>
<option value={'disabled' satisfies AvoidNegativeTs}>disabled</option>
</Select>
</td>
<td>
{renderNoticeIcon(notices.specific['avoidNegativeTs'], rightIconStyle) ?? <HelpIcon onClick={onAvoidNegativeTsHelpPress} />}
</td>
</tr>
)}
<tr>
<td>
{t('"ffmpeg" experimental flag')}
</td>
<td>
<Switch checked={ffmpegExperimental} onCheckedChange={setFfmpegExperimental} />
</td>
<td>
<HelpIcon onClick={onFfmpegExperimentalHelpPress} />
</td>
</tr>
<tr>
<td>
{t('More settings')}
</td>
<td>
<IoIosSettings size={24} role="button" onClick={toggleSettings} style={{ marginLeft: 5 }} />
</td>
<td />
</tr>
</tbody>
</table>
</ExportSheet>

@ -35,7 +35,7 @@ function ExportSheet({
transition={{ duration: 0.3, easings: ['easeOut'] }}
>
<div className={styles['box']} style={{ width }}>
<h1 style={{ textTransform: 'uppercase', fontSize: '1.4em', marginTop: 0, marginBottom: '.5em' }}>{title}</h1>
<h1 style={{ fontSize: '1.4em', marginTop: 0, marginBottom: '.5em' }}>{title}</h1>
<CloseButton type="submit" style={{ top: 0, right: 0 }} onClick={onClosePress} />

@ -1,9 +1,9 @@
import { CSSProperties, ChangeEventHandler, TdHTMLAttributes, memo, useCallback, useMemo, useState } from 'react';
import { FaYinYang, FaKeyboard, FaGlobe, FaBroom, FaCogs, FaHashtag, FaClock, FaFolder, FaFile, FaTimes } from 'react-icons/fa';
import { useTranslation } from 'react-i18next';
import { HTMLMotionProps, motion } from 'framer-motion';
import invariant from 'tiny-invariant';
import Row from './AnimatedTr';
import CaptureFormatButton from './CaptureFormatButton';
import AutoExportToggler from './AutoExportToggler';
import Switch from './Switch';
@ -25,16 +25,6 @@ const Button = ({ style, ...props }: ButtonProps) => <ButtonRaw style={{ padding
const Select = ({ style, ...props }: SelectProps) => <SelectRaw style={{ padding: '.5em 1.9em .5em .9em', ...style }} {...props} />;
const Row = (props: HTMLMotionProps<'tr'>) => (
<motion.tr
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
transition={{ duration: 0.5, ease: 'easeIn' }}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
/>
);
// eslint-disable-next-line react/jsx-props-no-spreading
const KeyCell = (props: TdHTMLAttributes<HTMLTableCellElement>) => <td {...props} />;
@ -106,9 +96,14 @@ function Settings({
const changeCustomFfPath = useCallback(async () => {
const newCustomFfPath = await askForFfPath(customFfPath);
if (newCustomFfPath == null) return;
setCustomFfPath(newCustomFfPath);
}, [customFfPath, setCustomFfPath]);
const clearCustomFfPath = useCallback(() => {
setCustomFfPath(undefined);
}, [setCustomFfPath]);
return (
<table className={styles['settings']}>
<thead>
@ -185,10 +180,15 @@ function Settings({
</div>
</KeyCell>
<td>
<Truncated maxWidth="15em">{customFfPath}</Truncated>
<Button onClick={changeCustomFfPath}>
<FaCogs style={{ verticalAlign: 'middle', marginRight: '.4em' }} />{customFfPath ? t('Using external ffmpeg') : t('Using built-in ffmpeg')}
</Button>
<Truncated maxWidth="15em">{customFfPath}</Truncated>
{customFfPath && (
<Button onClick={clearCustomFfPath} title={t('Clear')}>
<FaTimes />
</Button>
)}
</td>
</Row>
)}
@ -246,8 +246,8 @@ function Settings({
{customOutDir ? t('Custom working directory') : t('Same directory as input file')}...
</Button>
{customOutDir && (
<Button onClick={clearOutDir}>
<FaTimes style={{ marginRight: '.3em' }} />{t('Clear working directory')}
<Button onClick={clearOutDir} title={t('Clear')}>
<FaTimes />
</Button>
)}
</td>

Loading…
Cancel
Save