@ -6,8 +6,9 @@ import { FiScissors } from 'react-icons/fi';
import { AnimatePresence , motion } from 'framer-motion' ;
import { AnimatePresence , motion } from 'framer-motion' ;
import Swal from 'sweetalert2' ;
import Swal from 'sweetalert2' ;
import Lottie from 'react-lottie' ;
import Lottie from 'react-lottie' ;
import { SideSheet , Button , Position , Table , SegmentedControl , Checkbox } from 'evergreen-ui' ;
import { SideSheet , Button , Position , Table , SegmentedControl , Checkbox , Select } from 'evergreen-ui' ;
import { useStateWithHistory } from 'react-use/lib/useStateWithHistory' ;
import { useStateWithHistory } from 'react-use/lib/useStateWithHistory' ;
import useDebounce from 'react-use/lib/useDebounce' ;
import fromPairs from 'lodash/fromPairs' ;
import fromPairs from 'lodash/fromPairs' ;
import clamp from 'lodash/clamp' ;
import clamp from 'lodash/clamp' ;
@ -106,12 +107,15 @@ const App = memo(() => {
const [ filePath , setFilePath ] = useState ( '' ) ;
const [ filePath , setFilePath ] = useState ( '' ) ;
const [ externalStreamFiles , setExternalStreamFiles ] = useState ( [ ] ) ;
const [ externalStreamFiles , setExternalStreamFiles ] = useState ( [ ] ) ;
const [ detectedFps , setDetectedFps ] = useState ( ) ;
const [ detectedFps , setDetectedFps ] = useState ( ) ;
const [ mainStreams , setStreams ] = useState ( [ ] ) ;
const [ mainStreams , setMainStreams ] = useState ( [ ] ) ;
const [ mainVideoStream , setMainVideoStream ] = useState ( ) ;
const [ copyStreamIdsByFile , setCopyStreamIdsByFile ] = useState ( { } ) ;
const [ copyStreamIdsByFile , setCopyStreamIdsByFile ] = useState ( { } ) ;
const [ streamsSelectorShown , setStreamsSelectorShown ] = useState ( false ) ;
const [ streamsSelectorShown , setStreamsSelectorShown ] = useState ( false ) ;
const [ zoom , setZoom ] = useState ( 1 ) ;
const [ zoom , setZoom ] = useState ( 1 ) ;
const [ commandedTime , setCommandedTime ] = useState ( 0 ) ;
const [ commandedTime , setCommandedTime ] = useState ( 0 ) ;
const [ debouncedCommandedTime , setDebouncedCommandedTime ] = useState ( 0 ) ;
const [ ffmpegCommandLog , setFfmpegCommandLog ] = useState ( [ ] ) ;
const [ ffmpegCommandLog , setFfmpegCommandLog ] = useState ( [ ] ) ;
const [ neighbouringFrames , setNeighbouringFrames ] = useState ( [ ] ) ;
/ / S e g m e n t r e l a t e d s t a t e
/ / S e g m e n t r e l a t e d s t a t e
const [ currentSegIndex , setCurrentSegIndex ] = useState ( 0 ) ;
const [ currentSegIndex , setCurrentSegIndex ] = useState ( 0 ) ;
@ -122,6 +126,10 @@ const App = memo(() => {
100 ,
100 ,
) ;
) ;
const [ , cancelCommandedTimeDebounce ] = useDebounce ( ( ) => {
setDebouncedCommandedTime ( commandedTime ) ;
} , 300 , [ commandedTime ] ) ;
/ / P r e f e r e n c e s
/ / P r e f e r e n c e s
const [ captureFormat , setCaptureFormat ] = useState ( configStore . get ( 'captureFormat' ) ) ;
const [ captureFormat , setCaptureFormat ] = useState ( configStore . get ( 'captureFormat' ) ) ;
@ -154,10 +162,11 @@ const App = memo(() => {
const timelineScrollerRef = useRef ( ) ;
const timelineScrollerRef = useRef ( ) ;
const timelineScrollerSkipEventRef = useRef ( ) ;
const timelineScrollerSkipEventRef = useRef ( ) ;
const lastSavedCutSegmentsRef = useRef ( ) ;
const lastSavedCutSegmentsRef = useRef ( ) ;
const readingKeyframesPromise = useRef ( ) ;
function appendFfmpegCommandLog ( command ) {
function appendFfmpegCommandLog ( command ) {
setFfmpegCommandLog ( old => [ ... old , command ] ) ;
setFfmpegCommandLog ( old => [ ... old , { command , time : new Date ( ) } ] ) ;
}
}
function setCopyStreamIdsForPath ( path , cb ) {
function setCopyStreamIdsForPath ( path , cb ) {
@ -178,21 +187,23 @@ const App = memo(() => {
} ) ;
} ) ;
}
}
function seekAbs ( val ) {
const seekAbs = useCallback ( ( val ) => {
const video = videoRef . current ;
const video = videoRef . current ;
if ( val == null || Number . isNaN ( val ) ) return ;
if ( val == null || Number . isNaN ( val ) ) return ;
let valRounded = val ;
if ( detectedFps ) valRounded = Math . round ( detectedFps * val ) / detectedFps ; / / R o u n d t o n e a r e s t f r a m e
let outVal = val ;
let outVal = val Rounded ;
if ( outVal < 0 ) outVal = 0 ;
if ( outVal < 0 ) outVal = 0 ;
if ( outVal > video . duration ) outVal = video . duration ;
if ( outVal > video . duration ) outVal = video . duration ;
video . currentTime = outVal ;
video . currentTime = outVal ;
setCommandedTime ( outVal ) ;
setCommandedTime ( outVal ) ;
}
} , [ detectedFps ] ) ;
const seekRel = useCallback ( ( val ) => {
const seekRel = useCallback ( ( val ) => {
seekAbs ( videoRef . current . currentTime + val ) ;
seekAbs ( videoRef . current . currentTime + val ) ;
} , [ ] ) ;
} , [ seekAbs ] ) ;
const shortStep = useCallback ( ( dir ) => {
const shortStep = useCallback ( ( dir ) => {
seekRel ( ( 1 / ( detectedFps || 60 ) ) * dir ) ;
seekRel ( ( 1 / ( detectedFps || 60 ) ) * dir ) ;
@ -200,6 +211,8 @@ const App = memo(() => {
const resetState = useCallback ( ( ) => {
const resetState = useCallback ( ( ) => {
const video = videoRef . current ;
const video = videoRef . current ;
cancelCommandedTimeDebounce ( ) ;
setDebouncedCommandedTime ( 0 ) ;
setCommandedTime ( 0 ) ;
setCommandedTime ( 0 ) ;
video . currentTime = 0 ;
video . currentTime = 0 ;
video . playbackRate = 1 ;
video . playbackRate = 1 ;
@ -224,11 +237,12 @@ const App = memo(() => {
setFilePath ( '' ) ; / / S e t t i n g v i d e o s r c = " " p r e v e n t s m e m o r y l e a k i n c h r o m i u m
setFilePath ( '' ) ; / / S e t t i n g v i d e o s r c = " " p r e v e n t s m e m o r y l e a k i n c h r o m i u m
setExternalStreamFiles ( [ ] ) ;
setExternalStreamFiles ( [ ] ) ;
setDetectedFps ( ) ;
setDetectedFps ( ) ;
set Streams( [ ] ) ;
set Main Streams( [ ] ) ;
setCopyStreamIdsByFile ( { } ) ;
setCopyStreamIdsByFile ( { } ) ;
setStreamsSelectorShown ( false ) ;
setStreamsSelectorShown ( false ) ;
setZoom ( 1 ) ;
setZoom ( 1 ) ;
} , [ cutSegmentsHistory , setCutSegments ] ) ;
setNeighbouringFrames ( [ ] ) ;
} , [ cutSegmentsHistory , setCutSegments , cancelCommandedTimeDebounce ] ) ;
useEffect ( ( ) => ( ) => {
useEffect ( ( ) => ( ) => {
if ( dummyVideoPath ) unlink ( dummyVideoPath ) . catch ( console . error ) ;
if ( dummyVideoPath ) unlink ( dummyVideoPath ) . catch ( console . error ) ;
@ -361,39 +375,58 @@ const App = memo(() => {
const getCurrentTime = useCallback ( ( ) => (
const getCurrentTime = useCallback ( ( ) => (
playing ? playerTime : commandedTime ) , [ commandedTime , playerTime , playing ] ) ;
playing ? playerTime : commandedTime ) , [ commandedTime , playerTime , playing ] ) ;
const addCutSegment = useCallback ( ( ) => {
/ / c o n s t g e t N e x t P r e v K e y f r a m e = u s e C a l l b a c k ( ( c u t T i m e , n e x t ) = > f f m p e g . g e t N e x t P r e v K e y f r a m e ( n e i g h b o u r i n g F r a m e s , c u t T i m e , n e x t ) , [ n e i g h b o u r i n g F r a m e s ] ) ;
const cutStartTime = currentCutSeg . start ;
const cutEndTime = currentCutSeg . end ;
if ( cutStartTime === undefined && cutEndTime === undefined ) return ;
const addCutSegment = useCallback ( ( ) => {
try {
const suggestedStart = getCurrentTime ( ) ;
/ / C a n n o t a d d i f p r e v s e g i s n o t f i n i s h e d
const suggestedEnd = suggestedStart + 10 ;
if ( currentCutSeg . start === undefined && currentCutSeg . end === undefined ) return ;
const cutSegmentsNew = [
const suggestedStart = getCurrentTime ( ) ;
... cutSegments ,
/ * i f ( k e y f r a m e C u t ) {
createSegment ( {
const keyframeAlignedStart = getNextPrevKeyframe ( suggestedStart , true ) ;
start : suggestedStart ,
if ( keyframeAlignedStart != null ) suggestedStart = keyframeAlignedStart ;
end : suggestedEnd <= duration ? suggestedEnd : undefined ,
} * /
} ) ,
] ;
let suggestedEnd = suggestedStart + 10 ;
if ( suggestedEnd >= duration ) {
setCutSegments ( cutSegmentsNew ) ;
suggestedEnd = undefined ;
setCurrentSegIndex ( cutSegmentsNew . length - 1 ) ;
} / * else if ( keyframeCut ) {
const keyframeAlignedEnd = getNextPrevKeyframe ( suggestedEnd , false ) ;
if ( keyframeAlignedEnd != null ) suggestedEnd = keyframeAlignedEnd ;
} * /
const cutSegmentsNew = [
... cutSegments ,
createSegment ( {
start : suggestedStart ,
end : suggestedEnd ,
} ) ,
] ;
setCutSegments ( cutSegmentsNew ) ;
setCurrentSegIndex ( cutSegmentsNew . length - 1 ) ;
} catch ( err ) {
console . error ( err ) ;
}
} , [
} , [
currentCutSeg , cutSegments , getCurrentTime , duration , setCutSegments ,
currentCutSeg .start , currentCutSeg . end , cutSegments , getCurrentTime , duration , setCutSegments ,
] ) ;
] ) ;
const setCutStart = useCallback ( ( ) => {
const setCutStart = useCallback ( ( ) => {
/ / h t t p s : / / g i t h u b . c o m / m i f i / l o s s l e s s - c u t / i s s u e s / 1 6 8
/ / h t t p s : / / g i t h u b . c o m / m i f i / l o s s l e s s - c u t / i s s u e s / 1 6 8
/ / I f w e a r e a f t e r t h e e n d o f t h e l a s t s e g m e n t i n t h e t i m e l i n e ,
/ / I f w e a r e a f t e r t h e e n d o f t h e l a s t s e g m e n t i n t h e t i m e l i n e ,
/ / a d d a n e w s e g m e n t t h a t s t a r t s a t p l a y e r T i m e
/ / a d d a n e w s e g m e n t t h a t s t a r t s a t p l a y e r T i m e
if ( currentCutSeg . end != null
if ( currentCutSeg . end != null && getCurrentTime ( ) > currentCutSeg . end ) {
&& getCurrentTime ( ) > currentCutSeg . end ) {
addCutSegment ( ) ;
addCutSegment ( ) ;
} else {
} else {
try {
try {
setCutTime ( 'start' , getCurrentTime ( ) ) ;
const startTime = getCurrentTime ( ) ;
/ * i f ( k e y f r a m e C u t ) {
const keyframeAlignedCutTo = getNextPrevKeyframe ( startTime , true ) ;
if ( keyframeAlignedCutTo != null ) startTime = keyframeAlignedCutTo ;
} * /
setCutTime ( 'start' , startTime ) ;
} catch ( err ) {
} catch ( err ) {
errorToast ( err . message ) ;
errorToast ( err . message ) ;
}
}
@ -402,7 +435,13 @@ const App = memo(() => {
const setCutEnd = useCallback ( ( ) => {
const setCutEnd = useCallback ( ( ) => {
try {
try {
setCutTime ( 'end' , getCurrentTime ( ) ) ;
const endTime = getCurrentTime ( ) ;
/ * i f ( k e y f r a m e C u t ) {
const keyframeAlignedCutTo = getNextPrevKeyframe ( endTime , false ) ;
if ( keyframeAlignedCutTo != null ) endTime = keyframeAlignedCutTo ;
} * /
setCutTime ( 'end' , endTime ) ;
} catch ( err ) {
} catch ( err ) {
errorToast ( err . message ) ;
errorToast ( err . message ) ;
}
}
@ -840,19 +879,17 @@ const App = memo(() => {
const { streams } = await ffmpeg . getAllStreams ( fp ) ;
const { streams } = await ffmpeg . getAllStreams ( fp ) ;
/ / c o n s o l e . l o g ( ' s t r e a m s ' , s t r e a m s N e w ) ;
/ / c o n s o l e . l o g ( ' s t r e a m s ' , s t r e a m s N e w ) ;
set Streams( streams ) ;
set Main Streams( streams ) ;
setCopyStreamIdsForPath ( fp , ( ) => fromPairs ( streams . map ( ( stream ) => [
setCopyStreamIdsForPath ( fp , ( ) => fromPairs ( streams . map ( ( stream ) => [
stream . index , defaultProcessedCodecTypes . includes ( stream . codec _type ) ,
stream . index , defaultProcessedCodecTypes . includes ( stream . codec _type ) ,
] ) ) ) ;
] ) ) ) ;
streams . find ( ( stream ) => {
const videoStream = streams . find ( stream => stream . codec _type === 'video' ) ;
const streamFps = getStreamFps ( stream ) ;
setMainVideoStream ( videoStream ) ;
if ( streamFps != null ) {
if ( videoStream ) {
setDetectedFps ( streamFps ) ;
const streamFps = getStreamFps ( videoStream ) ;
return true ;
if ( streamFps != null ) setDetectedFps ( streamFps ) ;
}
}
return false ;
} ) ;
setFileNameTitle ( fp ) ;
setFileNameTitle ( fp ) ;
setFilePath ( fp ) ;
setFilePath ( fp ) ;
@ -1099,6 +1136,10 @@ const App = memo(() => {
await loadEdlFile ( filePaths [ 0 ] ) ;
await loadEdlFile ( filePaths [ 0 ] ) ;
}
}
function openHelp ( ) {
toggleHelp ( ) ;
}
electron . ipcRenderer . on ( 'file-opened' , fileOpened ) ;
electron . ipcRenderer . on ( 'file-opened' , fileOpened ) ;
electron . ipcRenderer . on ( 'close-file' , closeFile ) ;
electron . ipcRenderer . on ( 'close-file' , closeFile ) ;
electron . ipcRenderer . on ( 'html5ify' , html5ify ) ;
electron . ipcRenderer . on ( 'html5ify' , html5ify ) ;
@ -1109,6 +1150,7 @@ const App = memo(() => {
electron . ipcRenderer . on ( 'redo' , redo ) ;
electron . ipcRenderer . on ( 'redo' , redo ) ;
electron . ipcRenderer . on ( 'importEdlFile' , importEdlFile ) ;
electron . ipcRenderer . on ( 'importEdlFile' , importEdlFile ) ;
electron . ipcRenderer . on ( 'exportEdlFile' , exportEdlFile ) ;
electron . ipcRenderer . on ( 'exportEdlFile' , exportEdlFile ) ;
electron . ipcRenderer . on ( 'openHelp' , openHelp ) ;
return ( ) => {
return ( ) => {
electron . ipcRenderer . removeListener ( 'file-opened' , fileOpened ) ;
electron . ipcRenderer . removeListener ( 'file-opened' , fileOpened ) ;
@ -1121,6 +1163,7 @@ const App = memo(() => {
electron . ipcRenderer . removeListener ( 'redo' , redo ) ;
electron . ipcRenderer . removeListener ( 'redo' , redo ) ;
electron . ipcRenderer . removeListener ( 'importEdlFile' , importEdlFile ) ;
electron . ipcRenderer . removeListener ( 'importEdlFile' , importEdlFile ) ;
electron . ipcRenderer . removeListener ( 'exportEdlFile' , exportEdlFile ) ;
electron . ipcRenderer . removeListener ( 'exportEdlFile' , exportEdlFile ) ;
electron . ipcRenderer . removeListener ( 'openHelp' , openHelp ) ;
} ;
} ;
} , [
} , [
load , mergeFiles , outputDir , filePath , customOutDir , startTimeOffset , getHtml5ifiedPath ,
load , mergeFiles , outputDir , filePath , customOutDir , startTimeOffset , getHtml5ifiedPath ,
@ -1215,9 +1258,10 @@ const App = memo(() => {
< option key = { f } value = { f } > { f } - { name } < / option >
< option key = { f } value = { f } > { f } - { name } < / option >
) ) ;
) ) ;
}
}
function renderOutFmt ( style = { } ) {
function renderOutFmt ( prop s) {
return (
return (
< select style = { style } defaultValue = "" value = { fileFormat } title = "Output format" onChange = { withBlur ( e => setFileFormat ( e . target . value ) ) } >
/ / e s l i n t - d i s a b l e - n e x t - l i n e r e a c t / j s x - p r o p s - n o - s p r e a d i n g
< Select defaultValue = "" value = { fileFormat } title = "Output format" onChange = { withBlur ( e => setFileFormat ( e . target . value ) ) } { ...props } >
< option key = "disabled1" value = "" disabled > Format < / option >
< option key = "disabled1" value = "" disabled > Format < / option >
{ detectedFileFormat && (
{ detectedFileFormat && (
@ -1231,19 +1275,20 @@ const App = memo(() => {
< option key = "disabled3" value = "" disabled > -- - All formats : -- - < / option >
< option key = "disabled3" value = "" disabled > -- - All formats : -- - < / option >
{ renderFormatOptions ( otherFormatsMap ) }
{ renderFormatOptions ( otherFormatsMap ) }
< / s elect>
< / S elect>
) ;
) ;
}
}
function renderCaptureFormatButton ( ) {
function renderCaptureFormatButton ( props ) {
return (
return (
< button
< Button
type = "button"
title = "Capture frame format"
title = "Capture frame format"
onClick = { withBlur ( toggleCaptureFormat ) }
onClick = { withBlur ( toggleCaptureFormat ) }
/ / e s l i n t - d i s a b l e - n e x t - l i n e r e a c t / j s x - p r o p s - n o - s p r e a d i n g
{ ... props }
>
>
{ captureFormat }
{ captureFormat }
< / b utton>
< / B utton>
) ;
) ;
}
}
@ -1266,12 +1311,9 @@ const App = memo(() => {
This is where working files , exported files , project files ( CSV ) are stored .
This is where working files , exported files , project files ( CSV ) are stored .
< / KeyCell >
< / KeyCell >
< Table.TextCell >
< Table.TextCell >
< button
< Button onClick = { setOutputDir } >
type = "button"
onClick = { setOutputDir }
>
{ customOutDir ? 'Custom working directory' : 'Same directory as input file' }
{ customOutDir ? 'Custom working directory' : 'Same directory as input file' }
< / b utton>
< / Button >
< div > { customOutDir } < / div >
< div > { customOutDir } < / div >
< / Table.TextCell >
< / Table.TextCell >
< / Row >
< / Row >
@ -1290,8 +1332,8 @@ const App = memo(() => {
< Row >
< Row >
< KeyCell >
< KeyCell >
Keyframe cut mode < br / >
Keyframe cut mode < br / >
< b > Nearest keyframe < / b > : Cut at the nearest keyframe ( not accurate time . ) < br / >
< b > Nearest keyframe < / b > : Cut at the nearest keyframe ( not accurate time . ) Equiv to < i > ffmpeg - ss - i ... < / i > < br / >
< b > Normal cut < / b > : Accurate time but could leave an empty portion at the beginning of the video . < br / >
< b > Normal cut < / b > : Accurate time but could leave an empty portion at the beginning of the video . Equiv to < i > ffmpeg - i - ss ... < / i > < br / >
< / KeyCell >
< / KeyCell >
< Table.TextCell >
< Table.TextCell >
< SegmentedControl
< SegmentedControl
@ -1389,6 +1431,24 @@ const App = memo(() => {
/ / e s l i n t - d i s a b l e - n e x t - l i n e r e a c t - h o o k s / e x h a u s t i v e - d e p s
/ / e s l i n t - d i s a b l e - n e x t - l i n e r e a c t - h o o k s / e x h a u s t i v e - d e p s
} , [ ] ) ;
} , [ ] ) ;
useEffect ( ( ) => {
async function run ( ) {
if ( ! filePath || debouncedCommandedTime == null || ! mainVideoStream || readingKeyframesPromise . current ) return ;
try {
const promise = ffmpeg . readFrames ( { filePath , aroundTime : debouncedCommandedTime , stream : mainVideoStream . index } ) ;
readingKeyframesPromise . current = promise ;
const newFrames = await promise ;
/ / c o n s o l e . l o g ( n e w F r a m e s ) ;
setNeighbouringFrames ( newFrames ) ;
} catch ( err ) {
console . error ( 'Failed to read keyframes' , err ) ;
} finally {
readingKeyframesPromise . current = undefined ;
}
}
run ( ) ;
} , [ filePath , debouncedCommandedTime , mainVideoStream ] ) ;
const topBarHeight = '2rem' ;
const topBarHeight = '2rem' ;
const bottomBarHeight = '6rem' ;
const bottomBarHeight = '6rem' ;
@ -1513,32 +1573,34 @@ const App = memo(() => {
{ filePath && (
{ filePath && (
< Fragment >
< Fragment >
< button
< Button
type = "button"
iconBefore = { customOutDir ? 'folder-open' : undefined }
height = { 20 }
onClick = { withBlur ( setOutputDir ) }
onClick = { withBlur ( setOutputDir ) }
title = { customOutDir }
title = { customOutDir }
>
>
{ ` Working dir ${ customOutDir ? 'set' : 'unset' } ` }
{ ` Working dir ${ customOutDir ? 'set' : 'unset' } ` }
< / b utton>
< / B utton>
{renderOutFmt ( { width : 60 } ) }
<div style = { { width : 60 } } > { renderOutFmt ( { height : 20 } ) } < / div >
< b utton
< B utton
style= { { opacity : cutSegments . length < 2 ? 0.4 : undefined } }
height= { 20 }
type= "button"
style= { { opacity : outSegments && outSegments . length < 2 ? 0.4 : undefined } }
title = { autoMerge ? 'Auto merge segments to one file after export' : 'Export to separate files' }
title = { autoMerge ? 'Auto merge segments to one file after export' : 'Export to separate files' }
onClick = { withBlur ( toggleAutoMerge ) }
onClick = { withBlur ( toggleAutoMerge ) }
>
>
< AutoMergeIcon / > { autoMerge ? 'Merge cuts' : 'Separate files' }
< AutoMergeIcon / > { autoMerge ? 'Merge cuts' : 'Separate files' }
< / b utton>
< / B utton>
< button
< Button
type = "button"
height = { 20 }
title = { ` Cut mode ${ keyframeCut ? 'nearest keyframe cut' : 'normal cut' } ` }
iconBefore = { keyframeCut ? 'key' : undefined }
title = { ` Cut mode is ${ keyframeCut ? 'keyframe cut' : 'normal cut' } ` }
onClick = { withBlur ( toggleKeyframeCut ) }
onClick = { withBlur ( toggleKeyframeCut ) }
>
>
{ keyframeCut ? 'Keyframe cut' : 'Normal cut' }
{ keyframeCut ? 'Keyframe cut' : 'Normal cut' }
< / b utton>
< / B utton>
< / Fragment >
< / Fragment >
) }
) }
@ -1718,6 +1780,10 @@ const App = memo(() => {
invertCutSegments = { invertCutSegments }
invertCutSegments = { invertCutSegments }
/ >
/ >
) ) }
) ) }
{ mainVideoStream && neighbouringFrames . filter ( f => f . keyframe ) . map ( ( f ) => (
< div key = { f . time } style = { { position : 'absolute' , top : 0 , bottom : 0 , left : ` ${ ( f . time / duration ) * 100 } % ` , marginLeft : - 1 , width : 1 , background : 'rgba(0,0,0,1)' } } / >
) ) }
< / div >
< / div >
< / div >
< / div >
@ -1802,14 +1868,14 @@ const App = memo(() => {
< div className = "left-menu no-user-select" style = { { position : 'absolute' , left : 0 , bottom : 0 , padding : '.3em' , display : 'flex' , alignItems : 'center' } } >
< div className = "left-menu no-user-select" style = { { position : 'absolute' , left : 0 , bottom : 0 , padding : '.3em' , display : 'flex' , alignItems : 'center' } } >
{ renderInvertCutButton ( ) }
{ renderInvertCutButton ( ) }
< select style = { { width : 80 , margin : '0 10px' } } value = { zoom . toString ( ) } title = "Zoom" onChange = { withBlur ( e => setZoom ( parseInt ( e . target . value , 10 ) ) ) } >
< Select height = { 20 } style = { { width : 80 , margin : '0 10px' } } value = { zoom . toString ( ) } title = "Zoom" onChange = { withBlur ( e => setZoom ( parseInt ( e . target . value , 10 ) ) ) } >
{ Array ( 10 ) . fill ( ) . map ( ( unused , z ) => {
{ Array ( 10 ) . fill ( ) . map ( ( unused , z ) => {
const val = 2 * * z ;
const val = 2 * * z ;
return (
return (
< option key = { val } value = { String ( val ) } > Zoom { val } x < / option >
< option key = { val } value = { String ( val ) } > Zoom { val } x < / option >
) ;
) ;
} ) }
} ) }
< / s elect>
< / S elect>
< / div >
< / div >
< div className = "right-menu no-user-select" style = { { position : 'absolute' , right : 0 , bottom : 0 , padding : '.3em' , display : 'flex' , alignItems : 'center' } } >
< div className = "right-menu no-user-select" style = { { position : 'absolute' , right : 0 , bottom : 0 , padding : '.3em' , display : 'flex' , alignItems : 'center' } } >
@ -1832,7 +1898,7 @@ const App = memo(() => {
role = "button"
role = "button"
/ >
/ >
{ renderCaptureFormatButton ( )}
{ renderCaptureFormatButton ( { height : 20 } )}
< IoIosCamera
< IoIosCamera
style = { { paddingLeft : 5 , paddingRight : 15 } }
style = { { paddingLeft : 5 , paddingRight : 15 } }