diff --git a/src/renderer/src/components/ExportConfirm.module.css b/src/renderer/src/components/ExportConfirm.module.css index 715d5314..a494e773 100644 --- a/src/renderer/src/components/ExportConfirm.module.css +++ b/src/renderer/src/components/ExportConfirm.module.css @@ -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); } diff --git a/src/renderer/src/components/ExportConfirm.tsx b/src/renderer/src/components/ExportConfirm.tsx index 1f1ffbea..fba7dd7b 100644 --- a/src/renderer/src/components/ExportConfirm.tsx +++ b/src/renderer/src/components/ExportConfirm.tsx @@ -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 ? ( + + ) : ( + + ); +} + +function renderNotice(notice: { warning?: boolean | undefined, text: ReactNode } | undefined) { + if (notice == null) return null; + const { warning, text } = notice; + return ( +
+ {renderNoticeIcon({ warning })} {text} +
+ ); +} + 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: Warning: Cutting thumbnail tracks is known to cause problems. Consider disabling track {{ trackNumber: mainCopiedThumbnailStreams[0] ? mainCopiedThumbnailStreams[0].index + 1 : 0 }}. } : 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'] }} > -
-
-

{t('Export options')}

- - - - - - {notices.map(({ warning, text }) => ( - - - - ))} - - {segmentsOrInverse.selected.length !== segmentsOrInverse.all.length && ( - - - - )} - - - - - + + + + -

{t('Advanced options')}

+ + + + + + + )} + + + + + + + + + + + + + + {willMerge && ( + <> + + + + + -
-
- {warning ? ( - - ) : ( - - )} {text} -
-
-
- {t('{{selectedSegments}} of {{nonFilteredSegments}} segments selected', { selectedSegments: segmentsOrInverse.selected.length, nonFilteredSegments: segmentsOrInverse.all.length })} - -
- {segmentsOrInverse.selected.length > 1 ? t('Export mode for {{segments}} segments', { segments: segmentsOrInverse.selected.length }) : t('Export mode')} - - - - {effectiveExportMode === 'segments_to_chapters' ? ( - - ) : ( - - )} +
+

{t('Export options')}

+ + + + + + {notices.generic.map(({ warning, text }) => ( + + + + ))} + {segmentsOrInverse.selected.length !== segmentsOrInverse.all.length && ( - - - + - + )} + + + + + + + + + + + + + + + + + + + + + + + + + {canEditSegTemplate && ( - - + )} + {willMerge && ( - - - - {canEditSegTemplate && ( + )} + + + + + + + +
+
+ {renderNotice({ warning, text })} +
- {t('Output container format:')} - - {renderOutFmt({ height: 20, maxWidth: 150 })} - - + + {t('{{selectedSegments}} of {{nonFilteredSegments}} segments selected', { selectedSegments: segmentsOrInverse.selected.length, nonFilteredSegments: segmentsOrInverse.all.length })}
+ {segmentsOrInverse.selected.length > 1 ? t('Export mode for {{segments}} segments', { segments: segmentsOrInverse.selected.length }) : t('Export mode')} + {renderNotice(notices.specific['exportMode'])} + + + + {renderNoticeIcon(notices.specific['exportMode'], rightIconStyle) ?? } +
+ {t('Output container format:')} + + {renderOutFmt({ height: 20, maxWidth: 150 })} + + +
+ Input has {{ numStreamsTotal }} tracks + {renderNotice(notices.specific['problematicStreams'])} + + Keeping {{ numStreamsToCopy }} tracks + + {renderNoticeIcon(notices.specific['problematicStreams'], rightIconStyle) ?? } +
+ {t('Save output to path:')} + + {outputDir} + +
- Input has {{ numStreamsTotal }} tracks - {areWeCuttingProblematicStreams && ( - Warning: Cutting thumbnail tracks is known to cause problems. Consider disabling track {{ trackNumber: mainCopiedThumbnailStreams[0] ? mainCopiedThumbnailStreams[0].index + 1 : 0 }}. - )} + + - Keeping {{ numStreamsToCopy }} tracks - - {areWeCuttingProblematicStreams ? ( - - ) : ( - - )} +
- {t('Save output to path:')} + + - {outputDir} +
+ {t('Overwrite existing files')} + {renderNotice(notices.specific['overwriteOutput'])} + + + + {renderNoticeIcon(notices.specific['overwriteOutput'], rightIconStyle) ?? showHelpText({ text: t('Overwrite files when exporting, if a file with the same name as the output file name exists?') })} />} +
+ +

{t('Advanced options')}

+ + + + + {areWeCutting && ( + <> - + - )} - - {willMerge && ( - + - )} + + )} - - - - - - -
- + + {t('Shift all start times')} - + + +
- + + {t('Shift all end times')} - +
- {t('Overwrite existing files')} - - - - showHelpText({ text: t('Overwrite files when exporting, if a file with the same name as the output file name exists?') })} /> -
+ {isMov && ( + <> +
+ {t('Enable MOV Faststart?')} + + + {renderNotice(notices.specific['movFastStart'])} + + {renderNoticeIcon(notices.specific['movFastStart'], rightIconStyle) ?? } +
+ {t('Preserve all MP4/MOV metadata?')} + {renderNotice(notices.specific['preserveMovData'])} + + + + {renderNoticeIcon(notices.specific['preserveMovData'], rightIconStyle) ?? } +
+ {t('Preserve chapters')} + + + + +
+ {t('Preserve metadata')} + + + + +
+ {t('Create chapters from merged segments? (slow)')} + + + + +
- + + + + + + + )} + + + + + + {areWeCutting && ( + <> + + + + + - {areWeCutting && ( - <> - - - - - + {needSmartCut && ( - - )} + )} - {isMov && ( - <> + {!needSmartCut && ( + )} + + )} - - - - - - - )} - - - - - - - + {!needSmartCut && ( - - - {willMerge && ( - <> - - - - - - - - - - - - - )} - - - - - - {areWeCutting && ( - <> - - - - - - - {needSmartCut && ( - - - - - )} - - {!needSmartCut && ( - - - - - - )} - - )} - - {!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 ( - - - - - - ); - })()} - - - - - - - - - - - -
+ {t('Preserve original metadata when merging? (slow)')} + + + + +
+ {t('Depending on your specific file/player, you may have to try different options for best results.')} + +
+ {t('Smart cut (experimental):')} + {renderNotice(notices.specific['smartCut'])} + + setEnableSmartCut((v) => !v)} /> + + {renderNoticeIcon(notices.specific['smartCut'], rightIconStyle) ?? } +
- {t('Shift all start times')} - - - - -
- {t('Shift all end times')} + {t('Smart cut auto detect bitrate')} - +
+ {smartCutBitrate != null && ( + <> + + {t('kbit/s')} + + )} + +
- {t('Enable MOV Faststart?')} + {t('Keyframe cut mode')} + {renderNotice(notices.specific['cutMode'])} - - {isIpod && !movFastStart && {t('For the ipod format, it is recommended to activate this option')}} + toggleKeyframeCut()} /> - {isIpod && !movFastStart ? ( - - ) : ( - - )} + {renderNoticeIcon(notices.specific['cutMode'], rightIconStyle) ?? }
- {t('Preserve all MP4/MOV metadata?')} - {isIpod && preserveMovData && {t('For the ipod format, it is recommended to deactivate this option')}} - - - - {isIpod && preserveMovData ? ( - - ) : ( - - )} -
- {t('Preserve chapters')} - - - - -
- {t('Preserve metadata')} + "ffmpeg" avoid_negative_ts + {renderNotice(notices.specific['avoidNegativeTs'])} - setAvoidNegativeTs(e.target.value as AvoidNegativeTs)} style={{ height: 20, marginLeft: 5 }}> + + + + - -
- {t('Create chapters from merged segments? (slow)')} - - - - -
- {t('Preserve original metadata when merging? (slow)')} - - - - -
- {t('Depending on your specific file/player, you may have to try different options for best results.')} - -
- {t('Smart cut (experimental):')} - {needSmartCut && {t('Smart cut is experimental and will not work on all files.')}} - - - setEnableSmartCut((v) => !v)} /> - - {needSmartCut ? ( - - ) : ( - - )} -
- {t('Smart cut auto detect bitrate')} - -
- {smartCutBitrate != null && ( - <> - - {t('kbit/s')} - - )} - -
-
-
- {t('Keyframe cut mode')} - {!keyframeCut && {t('Note: Keyframe cut is recommended for most common files')}} - - toggleKeyframeCut()} /> - - {!keyframeCut ? ( - - ) : ( - - )} -
- "ffmpeg" avoid_negative_ts - {avoidNegativeTsWarn != null && {avoidNegativeTsWarn}} - - - - {avoidNegativeTsWarn != null ? ( - - ) : ( - - )} -
- {t('"ffmpeg" experimental flag')} - - - - + {renderNoticeIcon(notices.specific['avoidNegativeTs'], rightIconStyle) ?? }
- {t('More settings')} - - - -
-
+ )} + + + + {t('"ffmpeg" experimental flag')} + + + + + + + + + + + + {t('More settings')} + + + + + + + +
@@ -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' }} > - -
{t('Show this page before exporting?')}
+ +
+ {t('Show this page before exporting?')} +
+ + {notices.totalNum > 0 && ( + renderNoticeIcon({ warning: true }, { fontSize: '1.5em', marginRight: '.5em' }) + )} + <> {fileNames != null && ( - <> -
{(mergeMode ? t('Merged output file name:') : t('Output name(s):', { count: fileNames.length }))}
+
{(mergeMode ? t('Merged output file name:') : t('Output name(s):', { count: fileNames.length }))}
+ )} + + {fileNames != null && (
{/* eslint-disable-next-line react/destructuring-assignment */} @@ -143,77 +145,78 @@ function FileNameTemplateEditor(opts: { {!needToShow && }
- - )} + )} - - {needToShow && ( - -
{t('Output file name template')}:
-
- - - {!mergeMode && fileNames != null && } - - - {!haveImportantMessage && } -
- -
- {`${i18n.t('Variables')}:`} - - electron.shell.openExternal('https://github.com/mifi/lossless-cut/blob/master/docs.md#custom-exported-file-names')} /> - {availableVariables.map((variable) => ( - onVariableClick(variable)}>{variable} - ))} -
- - {hasTextNumericPaddedValue && ( -
- - {t('Minimum numeric padded length')} + + {needToShow && ( + +
{t('Output file name template')}:
+
+ + + {!mergeMode && fileNames != null && } + + + {!haveImportantMessage && }
- )} -
- - {t('Sanitize file names')} +
+ {`${i18n.t('Variables')}:`} - {!safeOutputFileName && } -
- - )} - + electron.shell.openExternal('https://github.com/mifi/lossless-cut/blob/master/docs.md#custom-exported-file-names')} /> + {availableVariables.map((variable) => ( + onVariableClick(variable)}>{variable} + ))} +
- {problems.error != null ? ( -
- {problems.error} -
- ) : ( - <> - {problems.sameAsInputFileNameWarning && ( -
- {' '} - {i18n.t('Output file name is the same as the source file name. This increases the risk of accidentally overwriting or deleting source files!')} -
- )} + {hasTextNumericPaddedValue && ( +
+ + {t('Minimum numeric padded length')} +
+ )} - {isMissingExtension && ( -
- {' '} - {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 })} -
+
+ + {t('Sanitize file names')} + + {!safeOutputFileName && } +
+
)} - - )} - +
+ + {problems.error != null ? ( +
+ {problems.error} +
+ ) : ( + <> + {problems.sameAsInputFileNameWarning && ( +
+ {' '} + {i18n.t('Output file name is the same as the source file name. This increases the risk of accidentally overwriting or deleting source files!')} +
+ )} + + {isMissingExtension && ( +
+ {' '} + {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 })} +
+ )} + + )} + + ); } diff --git a/src/renderer/src/components/ToggleExportConfirm.tsx b/src/renderer/src/components/ToggleExportConfirm.tsx index 018a8e28..fe524298 100644 --- a/src/renderer/src/components/ToggleExportConfirm.tsx +++ b/src/renderer/src/components/ToggleExportConfirm.tsx @@ -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();