improve export confirm

pull/2465/head
Mikael Finstad 6 months ago
parent 250506404f
commit 250507db1c
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26

@ -9,6 +9,8 @@
backdrop-filter: blur(30px);
overflow-y: scroll;
display: flex;
justify-content: center;
align-items: flex-start;
table.options {
width: 100%;
@ -33,6 +35,13 @@
}
.box {
margin: 15px 15px 50px 15px;
border-radius: 10px;
padding: 10px 20px;
min-height: 500px;
position: relative;
max-width: 100%;
width: 50em;
background: var(--white-a11);
}

@ -1,7 +1,6 @@
import { CSSProperties, Dispatch, SetStateAction, memo, useCallback, useMemo } from 'react';
import { CSSProperties, Dispatch, ReactNode, SetStateAction, memo, useCallback, useMemo } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { WarningSignIcon, InfoSignIcon } from 'evergreen-ui';
import { FaRegCheckCircle } from 'react-icons/fa';
import { FaExclamationTriangle, FaInfoCircle, FaRegCheckCircle } from 'react-icons/fa';
import i18n from 'i18next';
import { useTranslation, Trans } from 'react-i18next';
import { IoIosHelpCircle, IoIosSettings } from 'react-icons/io';
@ -27,17 +26,16 @@ import { FFprobeStream } from '../../../../ffprobe';
import { AvoidNegativeTs, PreserveMetadata } from '../../../../types';
import TextInput from './TextInput';
import { UseSegments } from '../hooks/useSegments';
import Warning from './Warning';
import CloseButton from './CloseButton';
const boxStyle: CSSProperties = { margin: '15px 15px 50px 15px', borderRadius: 10, padding: '10px 20px', minHeight: 500, position: 'relative' };
const outDirStyle: CSSProperties = { ...highlightedTextStyle, wordBreak: 'break-all', cursor: 'pointer' };
const warningStyle: CSSProperties = { fontSize: '80%', marginBottom: '.5em' };
const noticeStyle: CSSProperties = { fontSize: '85%', marginBottom: '.5em' };
const infoStyle: CSSProperties = { ...noticeStyle, color: 'var(--blue-12)' };
const warningStyle: CSSProperties = { ...noticeStyle, color: 'var(--orange-8)' };
const infoStyle: CSSProperties = { color: 'var(--gray-12)', fontSize: '80%', marginBottom: '.5em' };
const rightIconStyle: CSSProperties = { fontSize: '1.2em', verticalAlign: 'middle' };
const adjustCutFromValues = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const adjustCutToValues = [-10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
@ -53,6 +51,25 @@ function ShiftTimes({ values, num, setNum }: { values: number[], num: number, se
);
}
function renderNoticeIcon(notice: { warning?: boolean | undefined } | undefined, style?: CSSProperties) {
if (!notice) return undefined;
return notice.warning ? (
<FaExclamationTriangle style={{ flexShrink: '0', fontSize: '.8em', verticalAlign: 'baseline', color: 'var(--orange-9)', ...style }} />
) : (
<FaInfoCircle style={{ flexShrink: '0', fontSize: '.8em', verticalAlign: 'baseline', color: 'var(--blue-10)', ...style }} />
);
}
function renderNotice(notice: { warning?: boolean | undefined, text: ReactNode } | undefined) {
if (notice == null) return null;
const { warning, text } = notice;
return (
<div style={{ ...(warning ? warningStyle : infoStyle), gap: '0 .5em' }}>
{renderNoticeIcon({ warning })} {text}
</div>
);
}
function ExportConfirm({
areWeCutting,
segmentsToExport,
@ -125,19 +142,49 @@ function ExportConfirm({
const areWeCuttingProblematicStreams = areWeCutting && mainCopiedThumbnailStreams.length > 0;
const notices = useMemo(() => {
const ret: { warning?: true, text: string }[] = [];
if (!areWeCutting) {
ret.push({ text: t('Exporting whole file without cutting, because there are no segments to export.') });
const generic: { warning?: true, text: string }[] = [];
const specific: Record<'exportMode' | 'problematicStreams' | 'movFastStart' | 'preserveMovData' | 'smartCut' | 'cutMode' | 'avoidNegativeTs' | 'overwriteOutput', { warning?: true, text: ReactNode } | undefined> = {
exportMode: effectiveExportMode === 'segments_to_chapters' ? { text: i18n.t('Segments to chapters mode is active, this means that the file will not be cut. Instead chapters will be created from the segments.') } : undefined,
problematicStreams: areWeCuttingProblematicStreams ? { warning: true, text: <Trans>Warning: Cutting thumbnail tracks is known to cause problems. Consider disabling track {{ trackNumber: mainCopiedThumbnailStreams[0] ? mainCopiedThumbnailStreams[0].index + 1 : 0 }}.</Trans> } : undefined,
movFastStart: isMov && isIpod && !movFastStart ? { warning: true, text: t('For the ipod format, it is recommended to activate this option') } : undefined,
preserveMovData: isMov && isIpod && preserveMovData ? { warning: true, text: t('For the ipod format, it is recommended to deactivate this option') } : undefined,
smartCut: areWeCutting && needSmartCut ? { warning: true, text: t('Smart cut is experimental and will not work on all files.') } : undefined,
cutMode: areWeCutting && !needSmartCut && !keyframeCut ? { text: t('Note: Keyframe cut is recommended for most common files') } : undefined,
avoidNegativeTs: !needSmartCut ? {
text: (() => {
if (willMerge) {
if (avoidNegativeTs !== 'make_non_negative') {
return t('When merging, it\'s generally recommended to set this to "make_non_negative"');
}
return undefined;
}
if (!['make_zero', 'auto'].includes(avoidNegativeTs)) {
return t('It\'s generally recommended to set this to one of: {{values}}', { values: '"auto", "make_zero"' });
}
return undefined;
})(),
} : undefined,
overwriteOutput: enableOverwriteOutput ? { text: t('Existing files will be overwritten without warning!') } : undefined,
};
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') {
ret.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.') });
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.') });
}
if (areWeCutting && outputPlaybackRate !== 1) {
ret.push({ warning: true, text: t('Adjusting the output FPS and cutting at the same time will cause incorrect cuts. Consider instead doing it in two separate steps.') });
generic.push({ warning: true, text: t('Adjusting the output FPS and cutting at the same time will cause incorrect cuts. Consider instead doing it in two separate steps.') });
}
return ret;
}, [areWeCutting, outFormat, outputPlaybackRate, t]);
return {
generic,
specific,
totalNum: generic.filter((n) => n.warning).length + Object.values(specific).filter((n) => n != null && n.warning).length,
};
}, [areWeCutting, areWeCuttingProblematicStreams, avoidNegativeTs, effectiveExportMode, enableOverwriteOutput, isIpod, isMov, keyframeCut, mainCopiedThumbnailStreams, movFastStart, needSmartCut, outFormat, outputPlaybackRate, preserveMovData, t, willMerge]);
const exportModeDescription = useMemo(() => ({
segments_to_chapters: t('Don\'t cut the file, but instead export an unmodified original which has chapters generated from segments'),
@ -244,386 +291,336 @@ function ExportConfirm({
className={styles['sheet']}
transition={{ duration: 0.3, easings: ['easeOut'] }}
>
<div style={{ margin: 'auto' }}>
<div style={boxStyle} className={styles['box']}>
<h1 style={{ textTransform: 'uppercase', fontSize: '1.4em', marginTop: 0, marginBottom: '.5em' }}>{t('Export options')}</h1>
<CloseButton type="submit" style={{ top: 0, right: 0 }} onClick={onClosePress} />
<table className={styles['options']}>
<tbody>
{notices.map(({ warning, text }) => (
<tr key={text}>
<td colSpan={2}>
<div style={{ ...(warning ? { ...warningStyle, color: 'var(--orange-8)' } : infoStyle), display: 'flex', alignItems: 'center', gap: '0 .5em' }}>
{warning ? (
<WarningSignIcon verticalAlign="middle" color="warning" flexShrink="0" />
) : (
<InfoSignIcon verticalAlign="middle" color="info" flexShrink="0" />
)} {text}
</div>
</td>
<td />
</tr>
))}
{segmentsOrInverse.selected.length !== segmentsOrInverse.all.length && (
<tr>
<td colSpan={2}>
<FaRegCheckCircle size={12} style={{ marginRight: 3 }} />{t('{{selectedSegments}} of {{nonFilteredSegments}} segments selected', { selectedSegments: segmentsOrInverse.selected.length, nonFilteredSegments: segmentsOrInverse.all.length })}
</td>
<td />
</tr>
)}
<tr>
<td>
{segmentsOrInverse.selected.length > 1 ? t('Export mode for {{segments}} segments', { segments: segmentsOrInverse.selected.length }) : t('Export mode')}
</td>
<td>
<ExportModeButton selectedSegments={segmentsOrInverse.selected} />
</td>
<td>
{effectiveExportMode === 'segments_to_chapters' ? (
<WarningSignIcon verticalAlign="middle" color="warning" title={i18n.t('Segments to chapters mode is active, this means that the file will not be cut. Instead chapters will be created from the segments.')} />
) : (
<HelpIcon onClick={onExportModeHelpPress} />
)}
<div className={styles['box']}>
<h1 style={{ textTransform: 'uppercase', fontSize: '1.4em', marginTop: 0, marginBottom: '.5em' }}>{t('Export options')}</h1>
<CloseButton type="submit" style={{ top: 0, right: 0 }} onClick={onClosePress} />
<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>
))}
{segmentsOrInverse.selected.length !== segmentsOrInverse.all.length && (
<tr>
<td>
{t('Output container format:')}
</td>
<td>
{renderOutFmt({ height: 20, maxWidth: 150 })}
</td>
<td>
<HelpIcon onClick={onOutFmtHelpPress} />
<td colSpan={2}>
<FaRegCheckCircle size={12} style={{ marginRight: 3 }} />{t('{{selectedSegments}} of {{nonFilteredSegments}} segments selected', { selectedSegments: segmentsOrInverse.selected.length, nonFilteredSegments: segmentsOrInverse.all.length })}
</td>
<td />
</tr>
)}
<tr>
<td>
{segmentsOrInverse.selected.length > 1 ? t('Export mode for {{segments}} segments', { segments: segmentsOrInverse.selected.length }) : t('Export mode')}
{renderNotice(notices.specific['exportMode'])}
</td>
<td>
<ExportModeButton selectedSegments={segmentsOrInverse.selected} />
</td>
<td>
{renderNoticeIcon(notices.specific['exportMode'], rightIconStyle) ?? <HelpIcon onClick={onExportModeHelpPress} />}
</td>
</tr>
<tr>
<td>
{t('Output container format:')}
</td>
<td>
{renderOutFmt({ height: 20, maxWidth: 150 })}
</td>
<td>
<HelpIcon onClick={onOutFmtHelpPress} />
</td>
</tr>
<tr>
<td>
<Trans>Input has {{ numStreamsTotal }} tracks</Trans>
{renderNotice(notices.specific['problematicStreams'])}
</td>
<td>
<HighlightedText style={{ cursor: 'pointer' }} onClick={onShowStreamsSelectorClick}><Trans>Keeping {{ numStreamsToCopy }} tracks</Trans></HighlightedText>
</td>
<td>
{renderNoticeIcon(notices.specific['problematicStreams'], rightIconStyle) ?? <HelpIcon onClick={onTracksHelpPress} />}
</td>
</tr>
<tr>
<td>
{t('Save output to path:')}
</td>
<td>
<span role="button" onClick={changeOutDir} style={outDirStyle}>{outputDir}</span>
</td>
<td />
</tr>
{canEditSegTemplate && (
<tr>
<td>
<Trans>Input has {{ numStreamsTotal }} tracks</Trans>
{areWeCuttingProblematicStreams && (
<Warning style={warningStyle}><Trans>Warning: Cutting thumbnail tracks is known to cause problems. Consider disabling track {{ trackNumber: mainCopiedThumbnailStreams[0] ? mainCopiedThumbnailStreams[0].index + 1 : 0 }}.</Trans></Warning>
)}
<td colSpan={2}>
<FileNameTemplateEditor template={outSegTemplate} setTemplate={setOutSegTemplate} defaultTemplate={defaultOutSegTemplate} generateFileNames={generateOutSegFileNames} currentSegIndexSafe={currentSegIndexSafe} />
</td>
<td>
<HighlightedText style={{ cursor: 'pointer' }} onClick={onShowStreamsSelectorClick}><Trans>Keeping {{ numStreamsToCopy }} tracks</Trans></HighlightedText>
</td>
<td>
{areWeCuttingProblematicStreams ? (
<WarningSignIcon verticalAlign="middle" color="warning" />
) : (
<HelpIcon onClick={onTracksHelpPress} />
)}
<HelpIcon onClick={onOutSegTemplateHelpPress} />
</td>
</tr>
)}
{willMerge && (
<tr>
<td>
{t('Save output to path:')}
<td colSpan={2}>
<FileNameTemplateEditor template={mergedFileTemplate} setTemplate={setMergedFileTemplate} defaultTemplate={defaultMergedFileTemplate} generateFileNames={generateMergedFileNames} mergeMode />
</td>
<td>
<span role="button" onClick={changeOutDir} style={outDirStyle}>{outputDir}</span>
<HelpIcon onClick={onMergedFileTemplateHelpPress} />
</td>
<td />
</tr>
{canEditSegTemplate && (
)}
<tr>
<td>
{t('Overwrite existing files')}
{renderNotice(notices.specific['overwriteOutput'])}
</td>
<td>
<Switch checked={enableOverwriteOutput} onCheckedChange={setEnableOverwriteOutput} />
</td>
<td>
{renderNoticeIcon(notices.specific['overwriteOutput'], rightIconStyle) ?? <HelpIcon onClick={() => showHelpText({ text: t('Overwrite files when exporting, if a file with the same name as the output file name exists?') })} />}
</td>
</tr>
</tbody>
</table>
<h3 style={{ marginBottom: '.5em' }}>{t('Advanced options')}</h3>
<table className={styles['options']}>
<tbody>
{areWeCutting && (
<>
<tr>
<td colSpan={2}>
<FileNameTemplateEditor template={outSegTemplate} setTemplate={setOutSegTemplate} defaultTemplate={defaultOutSegTemplate} generateFileNames={generateOutSegFileNames} currentSegIndexSafe={currentSegIndexSafe} />
<td>
{t('Shift all start times')}
</td>
<td>
<HelpIcon onClick={onOutSegTemplateHelpPress} />
<ShiftTimes values={adjustCutFromValues} num={cutFromAdjustmentFrames} setNum={setCutFromAdjustmentFrames} />
</td>
<td>
<HelpIcon onClick={onCutFromAdjustmentFramesHelpPress} />
</td>
</tr>
)}
{willMerge && (
<tr>
<td colSpan={2}>
<FileNameTemplateEditor template={mergedFileTemplate} setTemplate={setMergedFileTemplate} defaultTemplate={defaultMergedFileTemplate} generateFileNames={generateMergedFileNames} mergeMode />
<td>
{t('Shift all end times')}
</td>
<td>
<HelpIcon onClick={onMergedFileTemplateHelpPress} />
<ShiftTimes values={adjustCutToValues} num={cutToAdjustmentFrames} setNum={setCutToAdjustmentFrames} />
</td>
<td />
</tr>
)}
</>
)}
<tr>
<td>
{t('Overwrite existing files')}
</td>
<td>
<Switch checked={enableOverwriteOutput} onCheckedChange={setEnableOverwriteOutput} />
</td>
<td>
<HelpIcon onClick={() => showHelpText({ text: t('Overwrite files when exporting, if a file with the same name as the output file name exists?') })} />
</td>
</tr>
</tbody>
</table>
{isMov && (
<>
<tr>
<td>
{t('Enable MOV Faststart?')}
</td>
<td>
<Switch checked={movFastStart} onCheckedChange={toggleMovFastStart} />
{renderNotice(notices.specific['movFastStart'])}
</td>
<td>
{renderNoticeIcon(notices.specific['movFastStart'], rightIconStyle) ?? <HelpIcon onClick={onMovFastStartHelpPress} />}
</td>
</tr>
<h3 style={{ marginBottom: '.5em' }}>{t('Advanced options')}</h3>
<tr>
<td>
{t('Preserve all MP4/MOV metadata?')}
{renderNotice(notices.specific['preserveMovData'])}
</td>
<td>
<Switch checked={preserveMovData} onCheckedChange={togglePreserveMovData} />
</td>
<td>
{renderNoticeIcon(notices.specific['preserveMovData'], rightIconStyle) ?? <HelpIcon onClick={onPreserveMovDataHelpPress} />}
</td>
</tr>
</>
)}
<tr>
<td>
{t('Preserve chapters')}
</td>
<td>
<Switch checked={preserveChapters} onCheckedChange={togglePreserveChapters} />
</td>
<td>
<HelpIcon onClick={onPreserveChaptersPress} />
</td>
</tr>
<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' as 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>
<table className={styles['options']}>
<tbody>
<tr>
<td>
{t('Preserve original metadata when merging? (slow)')}
</td>
<td>
<Switch checked={preserveMetadataOnMerge} onCheckedChange={togglePreserveMetadataOnMerge} />
</td>
<td>
<HelpIcon onClick={onPreserveMetadataOnMergeHelpPress} />
</td>
</tr>
</>
)}
<tr>
<td style={{ paddingTop: '.5em', color: 'var(--gray-11)', fontSize: '.9em' }} colSpan={2}>
{t('Depending on your specific file/player, you may have to try different options for best results.')}
</td>
<td />
</tr>
{areWeCutting && (
<>
<tr>
<td>
{t('Smart cut (experimental):')}
{renderNotice(notices.specific['smartCut'])}
</td>
<td>
<Switch checked={enableSmartCut} onCheckedChange={() => setEnableSmartCut((v) => !v)} />
</td>
<td>
{renderNoticeIcon(notices.specific['smartCut'], rightIconStyle) ?? <HelpIcon onClick={onSmartCutHelpPress} />}
</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>
{needSmartCut && (
<tr>
<td>
{t('Shift all end times')}
{t('Smart cut auto detect bitrate')}
</td>
<td>
<ShiftTimes values={adjustCutToValues} num={cutToAdjustmentFrames} setNum={setCutToAdjustmentFrames} />
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}>
{smartCutBitrate != null && (
<>
<TextInput value={smartCutBitrate} onChange={handleSmartCutBitrateChange} style={{ width: '4em', flexGrow: 0, marginRight: '.3em' }} />
<span style={{ marginRight: '.3em' }}>{t('kbit/s')}</span>
</>
)}
<span><Switch checked={smartCutBitrate == null} onCheckedChange={handleSmartCutBitrateToggle} /></span>
</div>
</td>
<td />
</tr>
</>
)}
)}
{isMov && (
<>
{!needSmartCut && (
<tr>
<td>
{t('Enable MOV Faststart?')}
{t('Keyframe cut mode')}
{renderNotice(notices.specific['cutMode'])}
</td>
<td>
<Switch checked={movFastStart} onCheckedChange={toggleMovFastStart} />
{isIpod && !movFastStart && <Warning style={warningStyle}>{t('For the ipod format, it is recommended to activate this option')}</Warning>}
<Switch checked={keyframeCut} onCheckedChange={() => toggleKeyframeCut()} />
</td>
<td>
{isIpod && !movFastStart ? (
<WarningSignIcon verticalAlign="middle" color="warning" />
) : (
<HelpIcon onClick={onMovFastStartHelpPress} />
)}
{renderNoticeIcon(notices.specific['cutMode'], rightIconStyle) ?? <HelpIcon onClick={onKeyframeCutHelpPress} />}
</td>
</tr>
)}
</>
)}
<tr>
<td>
{t('Preserve all MP4/MOV metadata?')}
{isIpod && preserveMovData && <Warning style={warningStyle}>{t('For the ipod format, it is recommended to deactivate this option')}</Warning>}
</td>
<td>
<Switch checked={preserveMovData} onCheckedChange={togglePreserveMovData} />
</td>
<td>
{isIpod && preserveMovData ? (
<WarningSignIcon verticalAlign="middle" color="warning" />
) : (
<HelpIcon onClick={onPreserveMovDataHelpPress} />
)}
</td>
</tr>
</>
)}
<tr>
<td>
{t('Preserve chapters')}
</td>
<td>
<Switch checked={preserveChapters} onCheckedChange={togglePreserveChapters} />
</td>
<td>
<HelpIcon onClick={onPreserveChaptersPress} />
</td>
</tr>
{!needSmartCut && (
<tr>
<td>
{t('Preserve metadata')}
&quot;ffmpeg&quot; <code className="highlighted">avoid_negative_ts</code>
{renderNotice(notices.specific['avoidNegativeTs'])}
</td>
<td>
<Select value={preserveMetadata} onChange={(e) => setPreserveMetadata(e.target.value as PreserveMetadata)} style={{ height: 20, marginLeft: 5 }}>
<option value={'default' as PreserveMetadata}>{t('Default')}</option>
<option value={'none' satisfies PreserveMetadata}>{t('None')}</option>
<option value={'nonglobal' satisfies PreserveMetadata}>{t('Non-global')}</option>
<Select value={avoidNegativeTs} onChange={(e) => setAvoidNegativeTs(e.target.value as AvoidNegativeTs)} style={{ height: 20, marginLeft: 5 }}>
<option value={'auto' as 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>
<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>
<tr>
<td>
{t('Preserve original metadata when merging? (slow)')}
</td>
<td>
<Switch checked={preserveMetadataOnMerge} onCheckedChange={togglePreserveMetadataOnMerge} />
</td>
<td>
<HelpIcon onClick={onPreserveMetadataOnMergeHelpPress} />
</td>
</tr>
</>
)}
<tr>
<td style={{ paddingTop: '.5em', color: 'var(--gray-11)', fontSize: '.9em' }} colSpan={2}>
{t('Depending on your specific file/player, you may have to try different options for best results.')}
</td>
<td />
</tr>
{areWeCutting && (
<>
<tr>
<td>
{t('Smart cut (experimental):')}
{needSmartCut && <Warning style={warningStyle}>{t('Smart cut is experimental and will not work on all files.')}</Warning>}
</td>
<td>
<Switch checked={enableSmartCut} onCheckedChange={() => setEnableSmartCut((v) => !v)} />
</td>
<td>
{needSmartCut ? (
<WarningSignIcon verticalAlign="middle" color="warning" title={i18n.t('Experimental functionality has been activated!')} />
) : (
<HelpIcon onClick={onSmartCutHelpPress} />
)}
</td>
</tr>
{needSmartCut && (
<tr>
<td>
{t('Smart cut auto detect bitrate')}
</td>
<td>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}>
{smartCutBitrate != null && (
<>
<TextInput value={smartCutBitrate} onChange={handleSmartCutBitrateChange} style={{ width: '4em', flexGrow: 0, marginRight: '.3em' }} />
<span style={{ marginRight: '.3em' }}>{t('kbit/s')}</span>
</>
)}
<span><Switch checked={smartCutBitrate == null} onCheckedChange={handleSmartCutBitrateToggle} /></span>
</div>
</td>
<td />
</tr>
)}
{!needSmartCut && (
<tr>
<td>
{t('Keyframe cut mode')}
{!keyframeCut && <Warning style={warningStyle}>{t('Note: Keyframe cut is recommended for most common files')}</Warning>}
</td>
<td>
<Switch checked={keyframeCut} onCheckedChange={() => toggleKeyframeCut()} />
</td>
<td>
{!keyframeCut ? (
<WarningSignIcon verticalAlign="middle" color="warning" />
) : (
<HelpIcon onClick={onKeyframeCutHelpPress} />
)}
</td>
</tr>
)}
</>
)}
{!needSmartCut && (() => {
const avoidNegativeTsWarn = (() => {
if (willMerge) {
if (avoidNegativeTs !== 'make_non_negative') {
return t('When merging, it\'s generally recommended to set this to "make_non_negative"');
}
return undefined;
}
if (!['make_zero', 'auto'].includes(avoidNegativeTs)) {
return t('It\'s generally recommended to set this to one of: {{values}}', { values: '"auto", "make_zero"' });
}
return undefined;
})();
return (
<tr>
<td>
&quot;ffmpeg&quot; <code className="highlighted">avoid_negative_ts</code>
{avoidNegativeTsWarn != null && <Warning style={warningStyle}>{avoidNegativeTsWarn}</Warning>}
</td>
<td>
<Select value={avoidNegativeTs} onChange={(e) => setAvoidNegativeTs(e.target.value as AvoidNegativeTs)} style={{ height: 20, marginLeft: 5 }}>
<option value={'auto' as 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>
{avoidNegativeTsWarn != null ? (
<WarningSignIcon verticalAlign="middle" color="warning" />
) : (
<HelpIcon onClick={onAvoidNegativeTsHelpPress} />
)}
</td>
</tr>
);
})()}
<tr>
<td>
{t('"ffmpeg" experimental flag')}
</td>
<td>
<Switch checked={ffmpegExperimental} onCheckedChange={setFfmpegExperimental} />
</td>
<td>
<HelpIcon onClick={onFfmpegExperimentalHelpPress} />
{renderNoticeIcon(notices.specific['avoidNegativeTs'], rightIconStyle) ?? <HelpIcon onClick={onAvoidNegativeTsHelpPress} />}
</td>
</tr>
<tr>
<td>
{t('More settings')}
</td>
<td>
<IoIosSettings size={24} role="button" onClick={toggleSettings} style={{ marginLeft: 5 }} />
</td>
<td />
</tr>
</tbody>
</table>
</div>
)}
<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>
</div>
</motion.div>
@ -633,10 +630,16 @@ function ExportConfirm({
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 50 }}
transition={{ duration: 0.4, easings: ['easeOut'] }}
style={{ display: 'flex', alignItems: 'flex-end', background: 'var(--gray-2)' }}
style={{ display: 'flex', alignItems: 'flex-end', background: 'var(--gray-2)', borderRadius: '.5em', padding: '.3em' }}
>
<ToggleExportConfirm size={25} />
<div style={{ fontSize: 13, marginLeft: 3, marginRight: 7, maxWidth: 120, lineHeight: '100%', color: exportConfirmEnabled ? 'var(--gray-12)' : 'var(--gray-11)' }} role="button" onClick={toggleExportConfirmEnabled}>{t('Show this page before exporting?')}</div>
<ToggleExportConfirm size="1.5em" />
<div style={{ fontSize: '.8em', marginLeft: '.4em', marginRight: '.5em', maxWidth: '8.5em', lineHeight: '100%', color: exportConfirmEnabled ? 'var(--gray-12)' : 'var(--gray-11)' }} role="button" onClick={toggleExportConfirmEnabled}>
{t('Show this page before exporting?')}
</div>
{notices.totalNum > 0 && (
renderNoticeIcon({ warning: true }, { fontSize: '1.5em', marginRight: '.5em' })
)}
</motion.div>
<motion.div

@ -131,11 +131,13 @@ function FileNameTemplateEditor(opts: {
}, [text]);
return (
<motion.div style={{ maxWidth: 600 }} animate={{ margin: needToShow ? '1.5em 0' : '0 0 .3em 0' }}>
<>
{fileNames != null && (
<>
<div>{(mergeMode ? t('Merged output file name:') : t('Output name(s):', { count: fileNames.length }))}</div>
<div>{(mergeMode ? t('Merged output file name:') : t('Output name(s):', { count: fileNames.length }))}</div>
)}
<motion.div animate={{ marginBottom: needToShow ? '1.5em' : '.3em' }}>
{fileNames != null && (
<div style={{ marginBottom: '.3em' }}>
<HighlightedText role="button" onClick={onShowClick} style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word', cursor: needToShow ? undefined : 'pointer' }}>
{/* eslint-disable-next-line react/destructuring-assignment */}
@ -143,77 +145,78 @@ function FileNameTemplateEditor(opts: {
{!needToShow && <FaEdit style={{ fontSize: '.9em', marginLeft: '.4em', verticalAlign: 'middle' }} />}
</HighlightedText>
</div>
</>
)}
)}
<AnimatePresence>
{needToShow && (
<motion.div
key="1"
initial={{ opacity: 0, height: 0, marginTop: 0 }}
animate={{ opacity: 1, height: 'auto', marginTop: '.5em' }}
exit={{ opacity: 0, height: 0, marginTop: 0 }}
>
<div style={{ color: 'var(--gray-11)', fontSize: '.8em' }}>{t('Output file name template')}:</div>
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '.2em' }}>
<TextInput ref={inputRef} onChange={onTextChange} value={text} autoComplete="off" autoCapitalize="off" autoCorrect="off" />
{!mergeMode && fileNames != null && <Button height={20} onClick={onAllFilesPreviewPress} marginLeft={5}>{t('Preview')}</Button>}
<IconButton title={t('Reset')} icon={ResetIcon} height={20} onClick={reset} marginLeft={5} intent="danger" />
{!haveImportantMessage && <IconButton title={t('Close')} icon={TickIcon} height={20} onClick={onHideClick} marginLeft={5} intent="success" appearance="primary" />}
</div>
<div style={{ fontSize: '.8em', color: 'var(--gray-11)', display: 'flex', gap: '.3em', flexWrap: 'wrap', alignItems: 'center', marginBottom: '.7em' }}>
{`${i18n.t('Variables')}:`}
<IoIosHelpCircle fontSize="1.3em" color="var(--gray-12)" role="button" cursor="pointer" onClick={() => electron.shell.openExternal('https://github.com/mifi/lossless-cut/blob/master/docs.md#custom-exported-file-names')} />
{availableVariables.map((variable) => (
<span key={variable} role="button" style={{ cursor: 'pointer', marginRight: '.2em', textDecoration: 'underline', textDecorationStyle: 'dashed', fontSize: '.9em' }} onClick={() => onVariableClick(variable)}>{variable}</span>
))}
</div>
{hasTextNumericPaddedValue && (
<div style={{ marginBottom: '.3em' }}>
<Select value={outputFileNameMinZeroPadding} onChange={(e) => setOutputFileNameMinZeroPadding(parseInt(e.target.value, 10))} style={{ marginRight: '.5em', fontSize: '1em' }}>
{Array.from({ length: 10 }).map((_v, i) => i + 1).map((v) => <option key={v} value={v}>{v}</option>)}
</Select>
{t('Minimum numeric padded length')}
<AnimatePresence>
{needToShow && (
<motion.div
key="1"
style={{ background: 'var(--gray-1)', padding: '.3em .5em', borderRadius: '.3em', margin: '0 -.5em' }}
initial={{ opacity: 0, height: 0, marginTop: 0 }}
animate={{ opacity: 1, height: 'auto', marginTop: '.7em' }}
exit={{ opacity: 0, height: 0, marginTop: 0 }}
>
<div style={{ color: 'var(--gray-11)', fontSize: '.8em' }}>{t('Output file name template')}:</div>
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '.2em' }}>
<TextInput ref={inputRef} onChange={onTextChange} value={text} autoComplete="off" autoCapitalize="off" autoCorrect="off" />
{!mergeMode && fileNames != null && <Button height={20} onClick={onAllFilesPreviewPress} marginLeft={5}>{t('Preview')}</Button>}
<IconButton title={t('Reset')} icon={ResetIcon} height={20} onClick={reset} marginLeft={5} intent="danger" />
{!haveImportantMessage && <IconButton title={t('Close')} icon={TickIcon} height={20} onClick={onHideClick} marginLeft={5} intent="success" appearance="primary" />}
</div>
)}
<div title={t('Whether or not to sanitize output file names (sanitizing removes special characters)')} style={{ marginBottom: '.3em' }}>
<Switch checked={safeOutputFileName} onCheckedChange={toggleSafeOutputFileName} style={{ verticalAlign: 'middle', marginRight: '.5em' }} />
<span>{t('Sanitize file names')}</span>
<div style={{ fontSize: '.8em', color: 'var(--gray-11)', display: 'flex', gap: '.3em', flexWrap: 'wrap', alignItems: 'center', marginBottom: '.7em' }}>
{`${i18n.t('Variables')}:`}
{!safeOutputFileName && <WarningSignIcon color="var(--amber-9)" style={{ marginLeft: '.5em', verticalAlign: 'middle' }} />}
</div>
</motion.div>
)}
</AnimatePresence>
<IoIosHelpCircle fontSize="1.3em" color="var(--gray-12)" role="button" cursor="pointer" onClick={() => electron.shell.openExternal('https://github.com/mifi/lossless-cut/blob/master/docs.md#custom-exported-file-names')} />
{availableVariables.map((variable) => (
<span key={variable} role="button" style={{ cursor: 'pointer', marginRight: '.2em', textDecoration: 'underline', textDecorationStyle: 'dashed', fontSize: '.9em' }} onClick={() => onVariableClick(variable)}>{variable}</span>
))}
</div>
{problems.error != null ? (
<div style={{ marginBottom: '1em' }}>
<ErrorIcon color="var(--red-9)" size={14} verticalAlign="baseline" /> {problems.error}
</div>
) : (
<>
{problems.sameAsInputFileNameWarning && (
<div style={{ marginBottom: '1em' }}>
<WarningSignIcon verticalAlign="middle" color="var(--amber-9)" />{' '}
{i18n.t('Output file name is the same as the source file name. This increases the risk of accidentally overwriting or deleting source files!')}
</div>
)}
{hasTextNumericPaddedValue && (
<div style={{ marginBottom: '.3em' }}>
<Select value={outputFileNameMinZeroPadding} onChange={(e) => setOutputFileNameMinZeroPadding(parseInt(e.target.value, 10))} style={{ marginRight: '.5em', fontSize: '1em' }}>
{Array.from({ length: 10 }).map((_v, i) => i + 1).map((v) => <option key={v} value={v}>{v}</option>)}
</Select>
{t('Minimum numeric padded length')}
</div>
)}
{isMissingExtension && (
<div style={{ marginBottom: '1em' }}>
<WarningSignIcon verticalAlign="middle" color="var(--amber-9)" />{' '}
{i18n.t('The file name template is missing {{ext}} and will result in a file without the suggested extension. This may result in an unplayable output file.', { ext: extVariableFormatted })}
</div>
<div title={t('Whether or not to sanitize output file names (sanitizing removes special characters)')} style={{ marginBottom: '.3em' }}>
<Switch checked={safeOutputFileName} onCheckedChange={toggleSafeOutputFileName} style={{ verticalAlign: 'middle', marginRight: '.5em' }} />
<span>{t('Sanitize file names')}</span>
{!safeOutputFileName && <WarningSignIcon color="var(--amber-9)" style={{ marginLeft: '.5em', verticalAlign: 'middle' }} />}
</div>
</motion.div>
)}
</>
)}
</motion.div>
</AnimatePresence>
{problems.error != null ? (
<div style={{ marginBottom: '1em' }}>
<ErrorIcon color="var(--red-9)" size={14} verticalAlign="baseline" /> {problems.error}
</div>
) : (
<>
{problems.sameAsInputFileNameWarning && (
<div style={{ marginBottom: '1em' }}>
<WarningSignIcon verticalAlign="middle" color="var(--amber-9)" />{' '}
{i18n.t('Output file name is the same as the source file name. This increases the risk of accidentally overwriting or deleting source files!')}
</div>
)}
{isMissingExtension && (
<div style={{ marginBottom: '1em' }}>
<WarningSignIcon verticalAlign="middle" color="var(--amber-9)" />{' '}
{i18n.t('The file name template is missing {{ext}} and will result in a file without the suggested extension. This may result in an unplayable output file.', { ext: extVariableFormatted })}
</div>
)}
</>
)}
</motion.div>
</>
);
}

@ -7,7 +7,7 @@ import { primaryTextColor } from '../colors';
import useUserSettings from '../hooks/useUserSettings';
function ToggleExportConfirm({ size = 23, style }: { size?: number | undefined, style?: CSSProperties }) {
function ToggleExportConfirm({ size = 23, style }: { size?: string | number | undefined, style?: CSSProperties }) {
const { t } = useTranslation();
const { exportConfirmEnabled, toggleExportConfirmEnabled } = useUserSettings();

Loading…
Cancel
Save