diff --git a/src/renderer/src/components/BatchFilesList.tsx b/src/renderer/src/components/BatchFilesList.tsx index e0bed676..ce4f3cdc 100644 --- a/src/renderer/src/components/BatchFilesList.tsx +++ b/src/renderer/src/components/BatchFilesList.tsx @@ -7,7 +7,7 @@ import { ReactSortable } from 'react-sortablejs'; import { SortAlphabeticalIcon, SortAlphabeticalDescIcon } from 'evergreen-ui'; import BatchFile from './BatchFile'; -import { controlsBackground, darkModeTransition } from '../colors'; +import { controlsBackground, darkModeTransition, primaryColor } from '../colors'; import { mySpring } from '../animations'; import { BatchFile as BatchFileType } from '../types'; @@ -70,8 +70,8 @@ function BatchFilesList({ selectedBatchFiles, filePath, width, batchFiles, setBa
{t('Batch file list')}{batchFiles.length > 0 && ` (${batchFiles.length})`}
- +
diff --git a/src/renderer/src/components/Button.tsx b/src/renderer/src/components/Button.tsx index 063443bf..3961cd6d 100644 --- a/src/renderer/src/components/Button.tsx +++ b/src/renderer/src/components/Button.tsx @@ -5,9 +5,9 @@ import styles from './Button.module.css'; export type ButtonProps = DetailedHTMLProps, HTMLButtonElement>; // eslint-disable-next-line react/display-name -const Button = forwardRef(({ type = 'button', ...props }, ref) => ( +const Button = forwardRef(({ type = 'button', className, ...props }, ref) => ( // eslint-disable-next-line react/jsx-props-no-spreading, react/button-has-type - + )} + width="70em" + > +
+
+ {t('This dialog can be used to concatenate files in series, e.g. one after the other:\n[file1][file2][file3]\nIt can NOT be used for merging tracks in parallell (like adding an audio track to a video).\nMake sure all files are of the exact same codecs & codec parameters (fps, resolution etc).')} +
+ +
+ {paths.map((path, index) => ( +
+
+ {`${index + 1}.`} + {basename(path)} + {!allFilesMetaCache[path] && } + {problemsByFile[path] && onProblemsByFileClick(path)} title={i18n.t('Mismatches detected')} style={{ color: 'var(--orange-8)', marginLeft: '1em' }} />}
- ))} -
+
+ ))}
+ -
- setEnableReadFileMeta(!!checked)} label={t('Check compatibility')} /> +
+ setEnableReadFileMeta(!!checked)} label={t('Check compatibility')} /> - + - {fileFormat && detectedFileFormat && ( - - )} -
+
{t('Output container format:')}
+ + {fileFormat && detectedFileFormat && ( + + )} + +
{t('Output file name')}:
+ setOutFileName(e.target.value)} /> {isOutFileNameTooLong && ( @@ -242,15 +252,7 @@ function ConcatDialog({ isShown, onHide, paths, onConcat, alwaysConcatMultipleFi {!enableReadFileMeta && ( )} - -
-
{t('Output file name')}:
- setOutFileName(e.target.value)} /> - -
- +
{settingsVisible && ( setSettingsVisible(false)} style={{ maxWidth: '40em' }}> @@ -271,7 +273,7 @@ function ConcatDialog({ isShown, onHide, paths, onConcat, alwaysConcatMultipleFi

{t('Note that also other settings from the normal export dialog apply to this merge function. For more information about all options, see the export dialog.')}

)} - + ); } diff --git a/src/renderer/src/components/ExportButton.tsx b/src/renderer/src/components/ExportButton.tsx index 021b654c..77c076be 100644 --- a/src/renderer/src/components/ExportButton.tsx +++ b/src/renderer/src/components/ExportButton.tsx @@ -31,6 +31,7 @@ function ExportButton({ segmentsToExport, areWeCutting, onClick, size = 1 }: { return (
0; const notices = useMemo(() => { - 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, @@ -151,23 +148,23 @@ function ExportConfirm({ 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"' }); + avoidNegativeTs: !needSmartCut ? (() => { + if (willMerge) { + if (avoidNegativeTs !== 'make_non_negative') { + return { text: t('When merging, it\'s generally recommended to set this to "make_non_negative"') }; } return undefined; - })(), - } : undefined, + } + if (!['make_zero', 'auto'].includes(avoidNegativeTs)) { + return { text: 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, }; + const generic: { warning?: true, text: string }[] = []; + if (effectiveExportMode === 'separate' && !areWeCutting) { generic.push({ text: t('Exporting whole file without cutting, because there are no segments to export.') }); } @@ -279,382 +276,352 @@ function ExportConfirm({ setSmartCutBitrate(v); }, [setSmartCutBitrate]); - // https://stackoverflow.com/questions/33454533/cant-scroll-to-top-of-flex-item-that-is-overflowing-container return ( - - {visible && ( + ( + onExportConfirm()} size={1.7} /> + )} + renderBottom={() => ( <> - -
-

{t('Export options')}

- - - - - - {notices.generic.map(({ warning, text }) => ( - - - - ))} - - {segmentsOrInverse.selected.length !== segmentsOrInverse.all.length && ( - - - - )} - - - - - - - - - - - - - - - - - - - - - - - - - {canEditSegTemplate && ( - - - - - )} - - {willMerge && ( - - - - - )} - - - - - - - -
-
- {renderNotice({ 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')} - {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} - -
- - - -
- - - -
- {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 && ( - <> - - - - - - - - - - - )} - - {isMov && ( - <> - - - - - - - - - - - - - )} - - - - - - - - - - - - - - {willMerge && ( - <> - - - - - - - - - - - - - )} - - - - - - {areWeCutting && ( - <> - - - - - - - {needSmartCut && ( - - - - - )} - - {!needSmartCut && ( - - - - - - )} - - )} - - {!needSmartCut && ( - - - - - - )} - - - - - - - - - - - - -
- {t('Shift all start times')} - - - - -
- {t('Shift all end times')} - - - -
- {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)')} - - - - -
- {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('Smart cut auto detect bitrate')} - -
- {smartCutBitrate != null && ( - <> - - {t('kbit/s')} - - )} - -
-
-
- {t('Keyframe cut mode')} - {renderNotice(notices.specific['cutMode'])} - - toggleKeyframeCut()} /> - - {renderNoticeIcon(notices.specific['cutMode'], rightIconStyle) ?? } -
- "ffmpeg" avoid_negative_ts - {renderNotice(notices.specific['avoidNegativeTs'])} - - - - {renderNoticeIcon(notices.specific['avoidNegativeTs'], rightIconStyle) ?? } -
- {t('"ffmpeg" experimental flag')} - - - - -
- {t('More settings')} - - - -
-
-
- -
- - -
- {t('Show this page before exporting?')} -
- - {notices.totalNum > 0 && ( - renderNoticeIcon({ warning: true }, { fontSize: '1.5em', marginRight: '.5em' }) - )} -
- - - onExportConfirm()} size={1.7} /> - + +
+ {t('Show this page before exporting?')}
+ {notices.totalNum > 0 && ( + renderNoticeIcon({ warning: true }, { fontSize: '1.5em', marginRight: '.5em' }) + )} )} - + > + + + {notices.generic.map(({ warning, text }) => ( + + + + ))} + + {segmentsOrInverse.selected.length !== segmentsOrInverse.all.length && ( + + + + )} + + + + + + + + + + + + + + + + + + + + + + + + + {canEditSegTemplate && ( + + + + + )} + + {willMerge && ( + + + + + )} + + + + + + + +
+
+ {renderNotice({ 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')} + {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} + +
+ + + +
+ + + +
+ {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 && ( + <> + + + + + + + + + + + )} + + {isMov && ( + <> + + + + + + + + + + + + + )} + + + + + + + + + + + + + + {willMerge && ( + <> + + + + + + + + + + + + + )} + + + + + + {areWeCutting && ( + <> + + + + + + + {needSmartCut && ( + + + + + )} + + {!needSmartCut && ( + + + + + + )} + + )} + + {!needSmartCut && ( + + + + + + )} + + + + + + + + + + + + +
+ {t('Shift all start times')} + + + + +
+ {t('Shift all end times')} + + + +
+ {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)')} + + + + +
+ {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('Smart cut auto detect bitrate')} + +
+ {smartCutBitrate != null && ( + <> + + {t('kbit/s')} + + )} + +
+
+
+ {t('Keyframe cut mode')} + {renderNotice(notices.specific['cutMode'])} + + toggleKeyframeCut()} /> + + {renderNoticeIcon(notices.specific['cutMode'], rightIconStyle) ?? } +
+ "ffmpeg" avoid_negative_ts + {renderNotice(notices.specific['avoidNegativeTs'])} + + + + {renderNoticeIcon(notices.specific['avoidNegativeTs'], rightIconStyle) ?? } +
+ {t('"ffmpeg" experimental flag')} + + + + +
+ {t('More settings')} + + + +
+ ); } diff --git a/src/renderer/src/components/ExportDialog.module.css b/src/renderer/src/components/ExportDialog.module.css new file mode 100644 index 00000000..fd49fd05 --- /dev/null +++ b/src/renderer/src/components/ExportDialog.module.css @@ -0,0 +1,32 @@ +.sheet { + position: fixed; + left: 0; + right: 0; + top: 0; + bottom: 0; + background: var(--white-a11); + color: var(--gray-12); + backdrop-filter: blur(30px); + overflow-y: scroll; + display: flex; + justify-content: center; + align-items: flex-start; +} + +:global(.dark-theme) .sheet { + background: var(--black-a11); +} + +.box { + margin: 15px 15px 50px 15px; + border-radius: 10px; + padding: 10px 20px; + min-height: 500px; + position: relative; + max-width: 100%; + background: var(--white-a11); +} + +:global(.dark-theme) .box { + background: var(--black-a11); +} diff --git a/src/renderer/src/components/ExportDialog.tsx b/src/renderer/src/components/ExportDialog.tsx new file mode 100644 index 00000000..b0a839e2 --- /dev/null +++ b/src/renderer/src/components/ExportDialog.tsx @@ -0,0 +1,72 @@ +import { CSSProperties, ReactNode } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; + +import styles from './ExportDialog.module.css'; +import CloseButton from './CloseButton'; + +function ExportDialog({ + visible, + children, + renderBottom, + renderButton, + onClosePress, + title, + width, +} : { + visible: boolean, + renderBottom?: (() => ReactNode | null) | undefined, + renderButton?: (() => ReactNode | null) | undefined, + children: ReactNode, + onClosePress: () => void, + title: string, + width: CSSProperties['width'], +}) { + // https://stackoverflow.com/questions/33454533/cant-scroll-to-top-of-flex-item-that-is-overflowing-container + return ( + + {visible && ( + <> + +
+

{title}

+ + + + {children} +
+
+ +
+ + {renderBottom?.()} + + + + {renderButton?.()} + +
+ + )} +
+ ); +} + +export default ExportDialog; diff --git a/src/renderer/src/main.css b/src/renderer/src/main.css index 70abe02d..4ed81a62 100644 --- a/src/renderer/src/main.css +++ b/src/renderer/src/main.css @@ -113,3 +113,16 @@ input:focus-visible, select:focus-visible { outline: 0.1em solid var(--gray-11); outline-offset: 0; } + +@keyframes pulse { + 0% { transform: scale(1) } + 45% { transform: scale(1.03) } + 50% { transform: scale(1.15) } + 55% { transform: scale(1.03) } + 100% { transform: scale(1) } +} + +/* because some people have a hard time finding the button */ +.export-animation { + animation: pulse 2s linear infinite +}