@ -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