2022-02-13 10:21:51 +01:00
import React , { memo , useCallback , useEffect } from 'react' ;
import { Select } from 'evergreen-ui' ;
import { motion } from 'framer-motion' ;
import { MdRotate90DegreesCcw } from 'react-icons/md' ;
import { useTranslation } from 'react-i18next' ;
import { IoIosCamera , IoMdKey } from 'react-icons/io' ;
import { FaYinYang , FaTrashAlt , FaStepBackward , FaStepForward , FaCaretLeft , FaCaretRight , FaPause , FaPlay , FaImages , FaKey } from 'react-icons/fa' ;
import { GiSoundWaves } from 'react-icons/gi' ;
// import useTraceUpdate from 'use-trace-update';
import { primaryTextColor , primaryColor } from './colors' ;
import SegmentCutpointButton from './components/SegmentCutpointButton' ;
import SetCutpointButton from './components/SetCutpointButton' ;
import ExportButton from './components/ExportButton' ;
import ToggleExportConfirm from './components/ToggleExportConfirm' ;
2022-03-01 06:53:44 +01:00
import CaptureFormatButton from './components/CaptureFormatButton' ;
2022-02-13 10:21:51 +01:00
import SimpleModeButton from './components/SimpleModeButton' ;
2023-01-02 10:17:41 +01:00
import { withBlur , toast , mirrorTransform , checkAppPath } from './util' ;
2022-02-13 10:21:51 +01:00
import { getSegColor } from './util/colors' ;
import { formatDuration , parseDuration } from './util/duration' ;
2022-03-01 06:53:44 +01:00
import useUserSettings from './hooks/useUserSettings' ;
2022-02-13 10:21:51 +01:00
const zoomOptions = Array ( 13 ) . fill ( ) . map ( ( unused , z ) => 2 * * z ) ;
const leftRightWidth = 100 ;
const BottomBar = memo ( ( {
2022-03-01 06:53:44 +01:00
zoom , setZoom , timelineToggleComfortZoom ,
isRotationSet , rotation , areWeCutting , increaseRotation , cleanupFilesDialog ,
2022-03-17 16:51:34 +01:00
captureSnapshot , onExportPress , segmentsToExport , hasVideo ,
2022-02-13 10:21:51 +01:00
seekAbs , currentSegIndexSafe , cutSegments , currentCutSeg , setCutStart , setCutEnd ,
setCurrentSegIndex , cutStartTimeManual , setCutStartTimeManual , cutEndTimeManual , setCutEndTimeManual ,
implement hotkeys
jumpTimelineStart (ctrl+home)
jumpTimelineEnd (ctrl+end)
reorderSegsByStartTime,
invertAllCutSegments,
createFixedDurationSegments,
createNumSegments,
shuffleSegments,
clearSegments,
toggleSegmentsList,
toggleStreamsSelector,
extractAllStreams,
convertFormatCurrentFile,
convertFormatBatch,
concatBatch,
toggleKeyframeCutMode,
toggleCaptureFormat,
toggleStripAudio,
setStartTimeOffset,
2022-02-20 11:34:10 +01:00
jumpTimelineStart , jumpTimelineEnd , jumpCutEnd , jumpCutStart , startTimeOffset , setCutTime , currentApparentCutSeg ,
2022-09-04 15:06:56 +02:00
playing , shortStep , togglePlay , toggleTimelineMode , hasAudio , timelineMode ,
2022-02-18 10:42:35 +01:00
keyframesEnabled , toggleKeyframesEnabled , seekClosestKeyframe , detectedFps ,
2022-02-13 10:21:51 +01:00
} ) => {
const { t } = useTranslation ( ) ;
2022-09-29 13:20:24 +02:00
const { invertCutSegments , setInvertCutSegments , simpleMode , toggleSimpleMode , exportConfirmEnabled } = useUserSettings ( ) ;
2022-03-01 06:53:44 +01:00
2022-02-13 10:21:51 +01:00
const onYinYangClick = useCallback ( ( ) => {
setInvertCutSegments ( v => {
const newVal = ! v ;
if ( newVal ) toast . fire ( { title : t ( 'When you export, selected segments on the timeline will be REMOVED - the surrounding areas will be KEPT' ) } ) ;
else toast . fire ( { title : t ( 'When you export, selected segments on the timeline will be KEPT - the surrounding areas will be REMOVED.' ) } ) ;
return newVal ;
} ) ;
} , [ setInvertCutSegments , t ] ) ;
const rotationStr = ` ${ rotation } ° ` ;
// Clear manual overrides if upstream cut time has changed
useEffect ( ( ) => {
setCutStartTimeManual ( ) ;
setCutEndTimeManual ( ) ;
} , [ setCutStartTimeManual , setCutEndTimeManual , currentApparentCutSeg . start , currentApparentCutSeg . end ] ) ;
2023-01-02 10:17:41 +01:00
useEffect ( ( ) => {
checkAppPath ( ) ;
} , [ ] ) ;
2022-02-13 10:21:51 +01:00
function renderJumpCutpointButton ( direction ) {
const newIndex = currentSegIndexSafe + direction ;
const seg = cutSegments [ newIndex ] ;
const backgroundColor = seg && getSegColor ( seg ) . alpha ( 0.5 ) . string ( ) ;
const opacity = seg ? undefined : 0.3 ;
2022-02-18 15:14:16 +01:00
const text = seg ? ` ${ newIndex + 1 } ` : '-' ;
const wide = text . length > 1 ;
2022-02-13 10:21:51 +01:00
const segButtonStyle = {
2022-02-18 15:14:16 +01:00
backgroundColor , opacity , padding : ` 6px ${ wide ? 4 : 6 } px ` , borderRadius : 10 , color : 'white' , fontSize : wide ? 12 : 14 , width : 20 , boxSizing : 'border-box' , letterSpacing : - 1 , lineHeight : '10px' , fontWeight : 'bold' , margin : '0 6px' ,
2022-02-13 10:21:51 +01:00
} ;
return (
< div
style = { segButtonStyle }
role = "button"
title = { ` ${ direction > 0 ? t ( 'Select next segment' ) : t ( 'Select previous segment' ) } ( ${ newIndex + 1 } ) ` }
onClick = { ( ) => seg && setCurrentSegIndex ( newIndex ) }
>
2022-02-18 15:14:16 +01:00
{ text }
2022-02-13 10:21:51 +01:00
< / div >
) ;
}
function renderCutTimeInput ( type ) {
const isStart = type === 'start' ;
const cutTimeManual = isStart ? cutStartTimeManual : cutEndTimeManual ;
const cutTime = isStart ? currentApparentCutSeg . start : currentApparentCutSeg . end ;
const setCutTimeManual = isStart ? setCutStartTimeManual : setCutEndTimeManual ;
const isCutTimeManualSet = ( ) => cutTimeManual !== undefined ;
2022-02-18 15:14:16 +01:00
const border = ` 1px solid ${ getSegColor ( currentCutSeg ) . alpha ( 0.8 ) . string ( ) } ` ;
2022-02-13 10:21:51 +01:00
const cutTimeInputStyle = {
background : 'white' , border , borderRadius : 5 , color : 'rgba(0, 0, 0, 0.7)' , fontSize : 13 , textAlign : 'center' , padding : '1px 5px' , marginTop : 0 , marginBottom : 0 , marginLeft : isStart ? 0 : 5 , marginRight : isStart ? 5 : 0 , boxSizing : 'border-box' , fontFamily : 'inherit' , width : 90 , outline : 'none' ,
} ;
function parseAndSetCutTime ( text ) {
setCutTimeManual ( text ) ;
// Don't proceed if not a valid time value
const timeWithOffset = parseDuration ( text ) ;
if ( timeWithOffset === undefined ) return ;
const timeWithoutOffset = Math . max ( timeWithOffset - startTimeOffset , 0 ) ;
try {
setCutTime ( type , timeWithoutOffset ) ;
seekAbs ( timeWithoutOffset ) ;
} catch ( err ) {
console . error ( 'Cannot set cut time' , err ) ;
// If we get an error from setCutTime, remain in the editing state (cutTimeManual)
// https://github.com/mifi/lossless-cut/issues/988
}
}
function handleCutTimeInput ( text ) {
// Allow the user to erase to reset
if ( text . length === 0 ) {
setCutTimeManual ( ) ;
return ;
}
parseAndSetCutTime ( text ) ;
}
async function handleCutTimePaste ( e ) {
e . preventDefault ( ) ;
try {
const clipboardData = e . clipboardData . getData ( 'Text' ) ;
parseAndSetCutTime ( clipboardData ) ;
} catch ( err ) {
console . error ( err ) ;
}
}
return (
< input
style = { { ... cutTimeInputStyle , color : isCutTimeManualSet ( ) ? '#dc1d1d' : undefined } }
type = "text"
2022-03-17 17:11:11 +01:00
title = { isStart ? t ( 'Manually input current segment\'s start time' ) : t ( 'Manually input current segment\'s end time' ) }
2022-02-13 10:21:51 +01:00
onChange = { e => handleCutTimeInput ( e . target . value ) }
onPaste = { handleCutTimePaste }
value = { isCutTimeManualSet ( )
? cutTimeManual
: formatDuration ( { seconds : cutTime + startTimeOffset } ) }
/ >
) ;
}
const PlayPause = playing ? FaPause : FaPlay ;
return (
< >
< div style = { { display : 'flex' , justifyContent : 'space-between' , alignItems : 'center' } } >
< div style = { { display : 'flex' , alignItems : 'center' , flexBasis : leftRightWidth } } >
2022-02-18 10:42:35 +01:00
{ ! simpleMode && (
2022-02-13 10:21:51 +01:00
< >
2022-02-18 10:42:35 +01:00
{ hasAudio && (
< GiSoundWaves
size = { 24 }
style = { { padding : '0 5px' , color : timelineMode === 'waveform' ? primaryTextColor : undefined } }
role = "button"
title = { t ( 'Show waveform' ) }
2022-09-04 15:06:56 +02:00
onClick = { ( ) => toggleTimelineMode ( 'waveform' ) }
2022-02-18 10:42:35 +01:00
/ >
) }
{ hasVideo && (
< >
< FaImages
size = { 20 }
style = { { padding : '0 5px' , color : timelineMode === 'thumbnails' ? primaryTextColor : undefined } }
role = "button"
title = { t ( 'Show thumbnails' ) }
2022-09-04 15:06:56 +02:00
onClick = { ( ) => toggleTimelineMode ( 'thumbnails' ) }
2022-02-18 10:42:35 +01:00
/ >
< FaKey
size = { 16 }
style = { { padding : '0 5px' , color : keyframesEnabled ? primaryTextColor : undefined } }
role = "button"
title = { t ( 'Show keyframes' ) }
onClick = { toggleKeyframesEnabled }
/ >
< / >
) }
2022-02-13 10:21:51 +01:00
< / >
) }
< / div >
< div style = { { flexGrow : 1 } } / >
{ ! simpleMode && (
2022-02-18 10:42:35 +01:00
< >
< FaStepBackward
size = { 16 }
title = { t ( 'Jump to start of video' ) }
role = "button"
implement hotkeys
jumpTimelineStart (ctrl+home)
jumpTimelineEnd (ctrl+end)
reorderSegsByStartTime,
invertAllCutSegments,
createFixedDurationSegments,
createNumSegments,
shuffleSegments,
clearSegments,
toggleSegmentsList,
toggleStreamsSelector,
extractAllStreams,
convertFormatCurrentFile,
convertFormatBatch,
concatBatch,
toggleKeyframeCutMode,
toggleCaptureFormat,
toggleStripAudio,
setStartTimeOffset,
2022-02-20 11:34:10 +01:00
onClick = { jumpTimelineStart }
2022-02-18 10:42:35 +01:00
/ >
2022-02-13 10:21:51 +01:00
2022-02-18 10:42:35 +01:00
{ renderJumpCutpointButton ( - 1 ) }
2022-03-17 17:11:11 +01:00
< SegmentCutpointButton currentCutSeg = { currentCutSeg } side = "start" Icon = { FaStepBackward } onClick = { jumpCutStart } title = { t ( 'Jump to current segment\'s start time' ) } style = { { marginRight : 5 } } / >
2022-02-18 10:42:35 +01:00
< / >
) }
2022-02-13 10:21:51 +01:00
2022-03-17 17:11:11 +01:00
< SetCutpointButton currentCutSeg = { currentCutSeg } side = "start" onClick = { setCutStart } title = { t ( 'Start current segment at current time' ) } style = { { marginRight : 5 } } / >
2022-02-13 10:21:51 +01:00
{ ! simpleMode && renderCutTimeInput ( 'start' ) }
< IoMdKey
size = { 25 }
role = "button"
title = { t ( 'Seek previous keyframe' ) }
style = { { flexShrink : 0 , marginRight : 2 , transform : mirrorTransform } }
onClick = { ( ) => seekClosestKeyframe ( - 1 ) }
/ >
{ ! simpleMode && (
< FaCaretLeft
style = { { flexShrink : 0 , marginLeft : - 6 , marginRight : - 4 } }
size = { 28 }
role = "button"
title = { t ( 'One frame back' ) }
onClick = { ( ) => shortStep ( - 1 ) }
/ >
) }
2022-03-01 10:49:01 +01:00
< div role = "button" onClick = { togglePlay } style = { { background : primaryColor , margin : '2px 5px 0 5px' , display : 'flex' , alignItems : 'center' , justifyContent : 'center' , width : 34 , height : 34 , borderRadius : 17 } } >
2022-02-13 10:21:51 +01:00
< PlayPause
style = { { marginLeft : playing ? 0 : 2 } }
size = { 16 }
/ >
< / div >
{ ! simpleMode && (
< FaCaretRight
style = { { flexShrink : 0 , marginRight : - 6 , marginLeft : - 4 } }
size = { 28 }
role = "button"
title = { t ( 'One frame forward' ) }
onClick = { ( ) => shortStep ( 1 ) }
/ >
) }
< IoMdKey
style = { { flexShrink : 0 , marginLeft : 2 } }
size = { 25 }
role = "button"
title = { t ( 'Seek next keyframe' ) }
onClick = { ( ) => seekClosestKeyframe ( 1 ) }
/ >
{ ! simpleMode && renderCutTimeInput ( 'end' ) }
2022-03-17 17:11:11 +01:00
< SetCutpointButton currentCutSeg = { currentCutSeg } side = "end" onClick = { setCutEnd } title = { t ( 'End current segment at current time' ) } style = { { marginLeft : 5 } } / >
2022-02-13 10:21:51 +01:00
{ ! simpleMode && (
2022-02-18 10:42:35 +01:00
< >
2022-03-17 17:11:11 +01:00
< SegmentCutpointButton currentCutSeg = { currentCutSeg } side = "end" Icon = { FaStepForward } onClick = { jumpCutEnd } title = { t ( 'Jump to current segment\'s end time' ) } style = { { marginLeft : 5 } } / >
2022-02-18 10:42:35 +01:00
{ renderJumpCutpointButton ( 1 ) }
< FaStepForward
size = { 16 }
title = { t ( 'Jump to end of video' ) }
role = "button"
implement hotkeys
jumpTimelineStart (ctrl+home)
jumpTimelineEnd (ctrl+end)
reorderSegsByStartTime,
invertAllCutSegments,
createFixedDurationSegments,
createNumSegments,
shuffleSegments,
clearSegments,
toggleSegmentsList,
toggleStreamsSelector,
extractAllStreams,
convertFormatCurrentFile,
convertFormatBatch,
concatBatch,
toggleKeyframeCutMode,
toggleCaptureFormat,
toggleStripAudio,
setStartTimeOffset,
2022-02-20 11:34:10 +01:00
onClick = { jumpTimelineEnd }
2022-02-18 10:42:35 +01:00
/ >
< / >
2022-02-13 10:21:51 +01:00
) }
< div style = { { flexGrow : 1 } } / >
< div style = { { flexBasis : leftRightWidth } } / >
< / div >
< div
className = "no-user-select"
style = { { display : 'flex' , alignItems : 'center' , justifyContent : 'space-between' , padding : '3px 4px' } }
>
2022-03-01 06:53:44 +01:00
< SimpleModeButton style = { { flexShrink : 0 } } / >
2022-02-13 10:21:51 +01:00
{ simpleMode && < div role = "button" onClick = { toggleSimpleMode } style = { { marginLeft : 5 , fontSize : '90%' } } > { t ( 'Toggle advanced view' ) } < / div > }
{ ! simpleMode && (
< >
2022-02-18 10:42:35 +01:00
< div style = { { marginLeft : 5 } } >
< motion.div
style = { { width : 24 , height : 24 } }
animate = { { rotateX : invertCutSegments ? 0 : 180 } }
transition = { { duration : 0.3 } }
>
< FaYinYang
size = { 24 }
role = "button"
title = { invertCutSegments ? t ( 'Discard selected segments' ) : t ( 'Keep selected segments' ) }
2022-03-17 16:28:37 +01:00
style = { { color : invertCutSegments ? primaryTextColor : undefined } }
2022-02-18 10:42:35 +01:00
onClick = { onYinYangClick }
/ >
< / motion.div >
< / div >
2022-02-20 10:23:18 +01:00
< div role = "button" style = { { marginRight : 5 , marginLeft : 10 } } title = { t ( 'Zoom' ) } onClick = { timelineToggleComfortZoom } > { Math . floor ( zoom ) } x < / div >
2022-02-13 10:21:51 +01:00
< Select height = { 20 } style = { { flexBasis : 85 , flexGrow : 0 } } value = { zoomOptions . includes ( zoom ) ? zoom . toString ( ) : '' } title = { t ( 'Zoom' ) } onChange = { withBlur ( e => setZoom ( parseInt ( e . target . value , 10 ) ) ) } >
< option key = "" value = "" disabled > { t ( 'Zoom' ) } < / option >
{ zoomOptions . map ( val => (
< option key = { val } value = { String ( val ) } > { t ( 'Zoom' ) } { val } x < / option >
) ) }
< / Select >
2022-02-18 10:42:35 +01:00
{ detectedFps != null && < div title = { t ( 'Video FPS' ) } style = { { color : 'rgba(255,255,255,0.6)' , fontSize : '.7em' , marginLeft : 6 } } > { detectedFps . toFixed ( 3 ) } < / div > }
2022-02-13 10:21:51 +01:00
< / >
) }
< div style = { { flexGrow : 1 } } / >
{ hasVideo && (
< >
< span style = { { textAlign : 'right' , display : 'inline-block' } } > { isRotationSet && rotationStr } < / span >
< MdRotate90DegreesCcw
size = { 24 }
style = { { margin : '0px 0px 0 2px' , verticalAlign : 'middle' , color : isRotationSet ? primaryTextColor : undefined } }
title = { ` ${ t ( 'Set output rotation. Current: ' ) } ${ isRotationSet ? rotationStr : t ( 'Don\'t modify' ) } ` }
onClick = { increaseRotation }
role = "button"
/ >
< / >
) }
{ ! simpleMode && (
< FaTrashAlt
title = { t ( 'Close file and clean up' ) }
style = { { padding : '5px 10px' } }
size = { 16 }
2022-02-20 10:23:18 +01:00
onClick = { cleanupFilesDialog }
2022-02-13 10:21:51 +01:00
role = "button"
/ >
) }
{ hasVideo && (
< >
2022-03-01 06:53:44 +01:00
{ ! simpleMode && < CaptureFormatButton height = { 20 } / > }
2022-02-13 10:21:51 +01:00
< IoIosCamera
style = { { paddingLeft : 5 , paddingRight : 15 } }
size = { 25 }
title = { t ( 'Capture frame' ) }
2022-02-20 10:23:18 +01:00
onClick = { captureSnapshot }
2022-02-13 10:21:51 +01:00
/ >
< / >
) }
2022-09-29 13:20:24 +02:00
{ ( ! simpleMode || ! exportConfirmEnabled ) && < ToggleExportConfirm style = { { marginRight : 5 } } / > }
2022-02-13 10:21:51 +01:00
2022-03-17 16:51:34 +01:00
< ExportButton size = { 1.3 } segmentsToExport = { segmentsToExport } areWeCutting = { areWeCutting } onClick = { onExportPress } / >
2022-02-13 10:21:51 +01:00
< / div >
< / >
) ;
} ) ;
export default BottomBar ;