improve export confirm

pull/2465/head
Mikael Finstad 1 year 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,23 +291,18 @@ function ExportConfirm({
className={styles['sheet']}
transition={{ duration: 0.3, easings: ['easeOut'] }}
>
<div style={{ margin: 'auto' }}>
<div style={boxStyle} className={styles['box']}>
<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.map(({ warning, text }) => (
{notices.generic.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 style={{ ...(warning ? { ...noticeStyle, color: 'var(--orange-8)' } : infoStyle), display: 'flex', alignItems: 'center', gap: '0 .5em' }}>
{renderNotice({ warning, text })}
</div>
</td>
<td />
@ -279,16 +321,13 @@ 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'])}
</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} />
)}
{renderNoticeIcon(notices.specific['exportMode'], rightIconStyle) ?? <HelpIcon onClick={onExportModeHelpPress} />}
</td>
</tr>
@ -307,19 +346,13 @@ function ExportConfirm({
<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>
)}
{renderNotice(notices.specific['problematicStreams'])}
</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} />
)}
{renderNoticeIcon(notices.specific['problematicStreams'], rightIconStyle) ?? <HelpIcon onClick={onTracksHelpPress} />}
</td>
</tr>
@ -358,12 +391,13 @@ function ExportConfirm({
<tr>
<td>
{t('Overwrite existing files')}
{renderNotice(notices.specific['overwriteOutput'])}
</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?') })} />
{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>
@ -407,31 +441,23 @@ function ExportConfirm({
</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>}
{renderNotice(notices.specific['movFastStart'])}
</td>
<td>
{isIpod && !movFastStart ? (
<WarningSignIcon verticalAlign="middle" color="warning" />
) : (
<HelpIcon onClick={onMovFastStartHelpPress} />
)}
{renderNoticeIcon(notices.specific['movFastStart'], rightIconStyle) ?? <HelpIcon onClick={onMovFastStartHelpPress} />}
</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>}
{renderNotice(notices.specific['preserveMovData'])}
</td>
<td>
<Switch checked={preserveMovData} onCheckedChange={togglePreserveMovData} />
</td>
<td>
{isIpod && preserveMovData ? (
<WarningSignIcon verticalAlign="middle" color="warning" />
) : (
<HelpIcon onClick={onPreserveMovDataHelpPress} />
)}
{renderNoticeIcon(notices.specific['preserveMovData'], rightIconStyle) ?? <HelpIcon onClick={onPreserveMovDataHelpPress} />}
</td>
</tr>
</>
@ -505,18 +531,13 @@ function ExportConfirm({
<tr>
<td>
{t('Smart cut (experimental):')}
{needSmartCut && <Warning style={warningStyle}>{t('Smart cut is experimental and will not work on all files.')}</Warning>}
{renderNotice(notices.specific['smartCut'])}
</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} />
)}
{renderNoticeIcon(notices.specific['smartCut'], rightIconStyle) ?? <HelpIcon onClick={onSmartCutHelpPress} />}
</td>
</tr>
@ -544,42 +565,24 @@ function ExportConfirm({
<tr>
<td>
{t('Keyframe cut mode')}
{!keyframeCut && <Warning style={warningStyle}>{t('Note: Keyframe cut is recommended for most common files')}</Warning>}
{renderNotice(notices.specific['cutMode'])}
</td>
<td>
<Switch checked={keyframeCut} onCheckedChange={() => toggleKeyframeCut()} />
</td>
<td>
{!keyframeCut ? (
<WarningSignIcon verticalAlign="middle" color="warning" />
) : (
<HelpIcon onClick={onKeyframeCutHelpPress} />
)}
{renderNoticeIcon(notices.specific['cutMode'], rightIconStyle) ?? <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 (
{!needSmartCut && (
<tr>
<td>
&quot;ffmpeg&quot; <code className="highlighted">avoid_negative_ts</code>
{avoidNegativeTsWarn != null && <Warning style={warningStyle}>{avoidNegativeTsWarn}</Warning>}
{renderNotice(notices.specific['avoidNegativeTs'])}
</td>
<td>
<Select value={avoidNegativeTs} onChange={(e) => setAvoidNegativeTs(e.target.value as AvoidNegativeTs)} style={{ height: 20, marginLeft: 5 }}>
@ -590,15 +593,10 @@ function ExportConfirm({
</Select>
</td>
<td>
{avoidNegativeTsWarn != null ? (
<WarningSignIcon verticalAlign="middle" color="warning" />
) : (
<HelpIcon onClick={onAvoidNegativeTsHelpPress} />
)}
{renderNoticeIcon(notices.specific['avoidNegativeTs'], rightIconStyle) ?? <HelpIcon onClick={onAvoidNegativeTsHelpPress} />}
</td>
</tr>
);
})()}
)}
<tr>
<td>
@ -624,7 +622,6 @@ function ExportConfirm({
</tbody>
</table>
</div>
</div>
</motion.div>
<div style={{ position: 'fixed', right: 0, bottom: 0, display: 'flex', alignItems: 'center', margin: 5 }}>
@ -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 && (
<>
{fileNames != null && (
<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,15 +145,15 @@ function FileNameTemplateEditor(opts: {
{!needToShow && <FaEdit style={{ fontSize: '.9em', marginLeft: '.4em', verticalAlign: 'middle' }} />}
</HighlightedText>
</div>
</>
)}
<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: '.5em' }}
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>
@ -214,6 +216,7 @@ function FileNameTemplateEditor(opts: {
</>
)}
</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