@ -46,7 +46,7 @@ import configStore from './store';
import { save as edlStoreSave , load as edlStoreLoad } from './edlStore' ;
import { save as edlStoreSave , load as edlStoreLoad } from './edlStore' ;
import {
import {
getOutPath , formatDuration , toast , errorToast , showFfmpegFail , setFileNameTitle ,
getOutPath , formatDuration , toast , errorToast , showFfmpegFail , setFileNameTitle ,
promptTimeOffset , generateColor , getOutDir , withBlur , checkDirWriteAccess ,
promptTimeOffset , generateColor , getOutDir , withBlur , checkDirWriteAccess , dirExists ,
} from './util' ;
} from './util' ;
@ -474,17 +474,31 @@ const App = memo(() => {
}
}
} , [ setCutTime ] ) ;
} , [ setCutTime ] ) ;
const setOutputDir = useCallback ( async ( ) => {
const outputDir = getOutDir ( customOutDir , filePath ) ;
const { filePaths } = await dialog . showOpenDialog ( { properties : [ 'openDirectory' ] } ) ;
setCustomOutDir ( ( filePaths && filePaths . length === 1 ) ? filePaths [ 0 ] : undefined ) ;
const askForOutDir = useCallback ( async ( defaultPath ) => {
const { filePaths } = await dialog . showOpenDialog ( {
properties : [ 'openDirectory' ] ,
title : i18n . t ( 'Where do you want to save output files?' ) ,
message : i18n . t ( 'Where do you want to save output files? Make sure there is enough free space in this folder' ) ,
defaultPath ,
buttonLabel : i18n . t ( 'Select output folder' ) ,
} ) ;
return ( filePaths && filePaths . length === 1 ) ? filePaths [ 0 ] : undefined ;
} , [ ] ) ;
} , [ ] ) ;
const changeOutDir = useCallback ( async ( ) => {
const newOutDir = await askForOutDir ( outputDir ) ;
/ / W e c a n n o t a l l o w e x p o r t i n g t o a d i r e c t o r y w h i c h h a s n o t y e t b e e n c o n f i r m e d b y a n o p e n d i a l o g
/ / b e c a u s e o f s a n d o x r e s t r i c t i o n s
if ( isMasBuild && ! newOutDir ) return ;
/ / E l s e i t ' s O K , w e a l l o w c l e a r i n g t h e d i r t o o
setCustomOutDir ( newOutDir ) ;
} , [ askForOutDir , outputDir ] ) ;
const effectiveFilePath = dummyVideoPath || html5FriendlyPath || filePath ;
const effectiveFilePath = dummyVideoPath || html5FriendlyPath || filePath ;
const fileUri = effectiveFilePath ? filePathToUrl ( effectiveFilePath ) : '' ;
const fileUri = effectiveFilePath ? filePathToUrl ( effectiveFilePath ) : '' ;
const outputDir = getOutDir ( customOutDir , filePath ) ;
const getEdlFilePath = useCallback ( ( fp ) => getOutPath ( customOutDir , fp , 'llc-edl.csv' ) , [ customOutDir ] ) ;
const getEdlFilePath = useCallback ( ( fp ) => getOutPath ( customOutDir , fp , 'llc-edl.csv' ) , [ customOutDir ] ) ;
const edlFilePath = getEdlFilePath ( filePath ) ;
const edlFilePath = getEdlFilePath ( filePath ) ;
@ -774,26 +788,26 @@ const App = memo(() => {
toast . fire ( { timer : 10000 , icon : 'warning' , title : i18n . t ( 'This video is not natively supported' ) , text : i18n . t ( 'This means that there is no audio in the preview and it has low quality. The final export operation will however be lossless and contains audio!' ) } ) ;
toast . fire ( { timer : 10000 , icon : 'warning' , title : i18n . t ( 'This video is not natively supported' ) , text : i18n . t ( 'This means that there is no audio in the preview and it has low quality. The final export operation will however be lossless and contains audio!' ) } ) ;
}
}
const createDummyVideo = useCallback ( async ( fp) => {
const createDummyVideo = useCallback ( async ( cod, fp) => {
const html5ifiedDummyPathDummy = getOutPath ( c ustomOutDir , fp , 'html5ified-dummy.mkv' ) ;
const html5ifiedDummyPathDummy = getOutPath ( c od , fp , 'html5ified-dummy.mkv' ) ;
await html5ifyDummy ( fp , html5ifiedDummyPathDummy ) ;
await html5ifyDummy ( fp , html5ifiedDummyPathDummy ) ;
setDummyVideoPath ( html5ifiedDummyPathDummy ) ;
setDummyVideoPath ( html5ifiedDummyPathDummy ) ;
setHtml5FriendlyPath ( ) ;
setHtml5FriendlyPath ( ) ;
showUnsupportedFileMessage ( ) ;
showUnsupportedFileMessage ( ) ;
} , [ customOutDir ] ) ;
} , [ ] ) ;
const tryCreateDummyVideo = useCallback ( async ( ) => {
const tryCreateDummyVideo = useCallback ( async ( ) => {
try {
try {
if ( working ) return ;
if ( working ) return ;
setWorking ( true ) ;
setWorking ( true ) ;
await createDummyVideo ( filePath) ;
await createDummyVideo ( customOutDir, filePath) ;
} catch ( err ) {
} catch ( err ) {
console . error ( err ) ;
console . error ( err ) ;
errorToast ( i18n . t ( 'Failed to playback this file. Try to convert to friendly format from the menu' ) ) ;
errorToast ( i18n . t ( 'Failed to playback this file. Try to convert to friendly format from the menu' ) ) ;
} finally {
} finally {
setWorking ( false ) ;
setWorking ( false ) ;
}
}
} , [ createDummyVideo , filePath , working ]) ;
} , [ createDummyVideo , filePath , working , customOutDir ]) ;
const togglePlay = useCallback ( ( resetPlaybackRate ) => {
const togglePlay = useCallback ( ( resetPlaybackRate ) => {
if ( ! filePath ) return ;
if ( ! filePath ) return ;
@ -950,17 +964,7 @@ const App = memo(() => {
}
}
} , [ playing ] ) ;
} , [ playing ] ) ;
const getHtml5ifiedPath = useCallback ( ( fp , type ) => getOutPath ( customOutDir , fp , ` html5ified- ${ type } .mp4 ` ) , [ customOutDir ] ) ;
const getHtml5ifiedPath = useCallback ( ( cod , fp , type ) => getOutPath ( cod , fp , ` html5ified- ${ type } .mp4 ` ) , [ ] ) ;
const checkExistingHtml5FriendlyFile = useCallback ( async ( fp , speed ) => {
const existing = getHtml5ifiedPath ( fp , speed ) ;
const ret = existing && await exists ( existing ) ;
if ( ret ) {
setHtml5FriendlyPath ( existing ) ;
showUnsupportedFileMessage ( ) ;
}
return ret ;
} , [ getHtml5ifiedPath ] ) ;
const loadEdlFile = useCallback ( async ( edlPath ) => {
const loadEdlFile = useCallback ( async ( edlPath ) => {
try {
try {
@ -982,8 +986,8 @@ const App = memo(() => {
}
}
} , [ cutSegmentsHistory , setCutSegments ] ) ;
} , [ cutSegmentsHistory , setCutSegments ] ) ;
const load = useCallback ( async ( fp , html5FriendlyPathRequested) => {
const load = useCallback ( async ( { filePath : fp , customOutDir: cod , html5FriendlyPathRequested } ) => {
console . log ( 'Load' , { fp , html5FriendlyPathRequested } ) ;
console . log ( 'Load' , { fp , cod, html5FriendlyPathRequested } ) ;
if ( working ) {
if ( working ) {
errorToast ( i18n . t ( 'Tried to load file while busy' ) ) ;
errorToast ( i18n . t ( 'Tried to load file while busy' ) ) ;
return ;
return ;
@ -993,6 +997,16 @@ const App = memo(() => {
setWorking ( true ) ;
setWorking ( true ) ;
async function checkExistingHtml5FriendlyFile ( speed ) {
const existing = getHtml5ifiedPath ( cod , fp , speed ) ;
const ret = existing && await exists ( existing ) ;
if ( ret ) {
setHtml5FriendlyPath ( existing ) ;
showUnsupportedFileMessage ( ) ;
}
return ret ;
}
try {
try {
const fd = await getFormatData ( fp ) ;
const fd = await getFormatData ( fp ) ;
@ -1028,10 +1042,10 @@ const App = memo(() => {
setHtml5FriendlyPath ( html5FriendlyPathRequested ) ;
setHtml5FriendlyPath ( html5FriendlyPathRequested ) ;
showUnsupportedFileMessage ( ) ;
showUnsupportedFileMessage ( ) ;
} else if (
} else if (
! ( await checkExistingHtml5FriendlyFile ( fp , 'slow-audio' ) || await checkExistingHtml5FriendlyFile ( fp , 'slow' ) || await checkExistingHtml5FriendlyFile ( fp , 'fast' ) )
! ( await checkExistingHtml5FriendlyFile ( 'slow-audio' ) || await checkExistingHtml5FriendlyFile ( 'slow' ) || await checkExistingHtml5FriendlyFile ( 'fast' ) )
&& ! doesPlayerSupportFile ( streams )
&& ! doesPlayerSupportFile ( streams )
) {
) {
await createDummyVideo ( fp) ;
await createDummyVideo ( cod, fp) ;
}
}
await loadEdlFile ( getEdlFilePath ( fp ) ) ;
await loadEdlFile ( getEdlFilePath ( fp ) ) ;
@ -1045,10 +1059,7 @@ const App = memo(() => {
} finally {
} finally {
setWorking ( false ) ;
setWorking ( false ) ;
}
}
} , [
} , [ resetState , working , createDummyVideo , loadEdlFile , getEdlFilePath , getHtml5ifiedPath ] ) ;
resetState , working , createDummyVideo , checkExistingHtml5FriendlyFile , loadEdlFile ,
getEdlFilePath ,
] ) ;
const toggleHelp = useCallback ( ( ) => setHelpVisible ( val => ! val ) , [ ] ) ;
const toggleHelp = useCallback ( ( ) => setHelpVisible ( val => ! val ) , [ ] ) ;
const toggleSettings = useCallback ( ( ) => setSettingsVisible ( val => ! val ) , [ ] ) ;
const toggleSettings = useCallback ( ( ) => setSettingsVisible ( val => ! val ) , [ ] ) ;
@ -1169,34 +1180,28 @@ const App = memo(() => {
const firstFile = filePaths [ 0 ] ;
const firstFile = filePaths [ 0 ] ;
const outDirPath = getOutDir ( customOutDir , firstFile ) ;
const customOutDirExists = await dirExists ( customOutDir ) ;
if ( ! customOutDirExists ) setCustomOutDir ( undefined ) ;
const newCustomOutDir = customOutDirExists ? customOutDir : undefined ;
const outDirPath = getOutDir ( newCustomOutDir , firstFile ) ;
const hasDirWriteAccess = await checkDirWriteAccess ( outDirPath ) ;
const hasDirWriteAccess = await checkDirWriteAccess ( outDirPath ) ;
if ( ! hasDirWriteAccess ) {
if ( ! hasDirWriteAccess ) {
if ( isMasBuild ) {
if ( isMasBuild ) {
await Swal . fire ( {
const newOutDir = await askForOutDir ( outDirPath ) ;
title : i18n . t ( 'Mac OS file security' ) ,
/ / U s e r c a n c e l l e d o p e n d i a l o g , r e f u s e t o o p e n f i l e , b e c a u s e w e w i l l g e t p e r m i s s i o n d e n i e d f r o m s a n d b o x
icon : 'info' ,
if ( ! newOutDir ) return ;
text : i18n . t ( 'Mac OS requires you to choose the folder where the output files should be saved. This is only required the first time for each folder. Simply press "Open" in the next dialog to allow access to the default folder.' ) ,
setCustomOutDir ( newOutDir ) ;
} ) ;
/ / T O D O c h e c k t h a t c o r r e c t d i r i s s e l e c t e d
/ / T O D O a l s o w h e n c u s t o m o u t d i r c h a n g e s
/ / e s l i n t - d i s a b l e - n e x t - l i n e n o - u n u s e d - v a r s
const { canceled , filePaths : filePaths2 } = await dialog . showOpenDialog ( {
title : 'Select file to open' ,
defaultPath : outDirPath ,
properties : [ 'openDirectory' ] ,
} ) ;
if ( canceled ) return ;
} else {
} else {
errorToast ( i18n . t ( 'You have no write access to the directory of this file, please select a custom working dir' ) ) ;
errorToast ( i18n . t ( 'You have no write access to the directory of this file, please select a custom working dir' ) ) ;
}
}
}
}
if ( ! isFileOpened ) {
if ( ! isFileOpened ) {
load ( firstFile ) ;
load ( { filePath : firstFile , customOutDir : newCustomOutDir } ) ;
return ;
return ;
}
}
const { value } = await Swal . fire ( {
const { value } = await Swal . fire ( {
title : i18n . t ( 'You opened a new file. What do you want to do?' ) ,
title : i18n . t ( 'You opened a new file. What do you want to do?' ) ,
icon : 'question' ,
icon : 'question' ,
@ -1211,12 +1216,12 @@ const App = memo(() => {
} ) ;
} ) ;
if ( value === 'open' ) {
if ( value === 'open' ) {
load ( firstFile ) ;
load ( { filePath : firstFile , customOutDir : newCustomOutDir } ) ;
} else if ( value === 'add' ) {
} else if ( value === 'add' ) {
addStreamSourceFile ( firstFile ) ;
addStreamSourceFile ( firstFile ) ;
setStreamsSelectorShown ( true ) ;
setStreamsSelectorShown ( true ) ;
}
}
} , [ addStreamSourceFile , isFileOpened , load , mergeFiles , customOutDir ]) ;
} , [ addStreamSourceFile , isFileOpened , load , mergeFiles , customOutDir , askForOutDir ]) ;
const onDrop = useCallback ( async ( ev ) => {
const onDrop = useCallback ( async ( ev ) => {
ev . preventDefault ( ) ;
ev . preventDefault ( ) ;
@ -1249,13 +1254,13 @@ const App = memo(() => {
try {
try {
setWorking ( true ) ;
setWorking ( true ) ;
if ( [ 'fast' , 'slow' , 'slow-audio' ] . includes ( speed ) ) {
if ( [ 'fast' , 'slow' , 'slow-audio' ] . includes ( speed ) ) {
const html5FriendlyPath New = getHtml5ifiedPath ( filePath , speed ) ;
const html5FriendlyPath Requested = getHtml5ifiedPath ( customOutDir , filePath , speed ) ;
const encodeVideo = [ 'slow' , 'slow-audio' ] . includes ( speed ) ;
const encodeVideo = [ 'slow' , 'slow-audio' ] . includes ( speed ) ;
const encodeAudio = speed === 'slow-audio' ;
const encodeAudio = speed === 'slow-audio' ;
await ffmpegHtml5ify ( filePath , html5FriendlyPath New , encodeVideo , encodeAudio ) ;
await ffmpegHtml5ify ( filePath , html5FriendlyPath Requested , encodeVideo , encodeAudio ) ;
load ( filePath , html5FriendlyPathNew ) ;
load ( { filePath , html5FriendlyPathRequested , customOutDir } ) ;
} else {
} else {
await createDummyVideo ( filePath) ;
await createDummyVideo ( customOutDir, filePath) ;
}
}
} catch ( err ) {
} catch ( err ) {
errorToast ( i18n . t ( 'Failed to html5ify file' ) ) ;
errorToast ( i18n . t ( 'Failed to html5ify file' ) ) ;
@ -1373,7 +1378,7 @@ const App = memo(() => {
useEffect ( ( ) => {
useEffect ( ( ) => {
document . body . addEventListener ( 'drop' , onDrop ) ;
document . body . addEventListener ( 'drop' , onDrop ) ;
return ( ) => document . body . removeEventListener ( 'drop' , onDrop ) ;
return ( ) => document . body . removeEventListener ( 'drop' , onDrop ) ;
} , [ load, mergeFiles , onDrop] ) ;
} , [ onDrop] ) ;
const commonFormatsMap = useMemo ( ( ) => fromPairs ( commonFormats . map ( format => [ format , allOutFormats [ format ] ] )
const commonFormatsMap = useMemo ( ( ) => fromPairs ( commonFormats . map ( format => [ format , allOutFormats [ format ] ] )
@ -1433,7 +1438,7 @@ const App = memo(() => {
const renderSettings = useCallback ( ( ) => (
const renderSettings = useCallback ( ( ) => (
< Settings
< Settings
setOutputDir= { setOutp utDir}
changeOutDir= { changeO utDir}
customOutDir = { customOutDir }
customOutDir = { customOutDir }
autoMerge = { autoMerge }
autoMerge = { autoMerge }
setAutoMerge = { setAutoMerge }
setAutoMerge = { setAutoMerge }
@ -1455,7 +1460,7 @@ const App = memo(() => {
renderCaptureFormatButton = { renderCaptureFormatButton }
renderCaptureFormatButton = { renderCaptureFormatButton }
onWheelTunerRequested = { onWheelTunerRequested }
onWheelTunerRequested = { onWheelTunerRequested }
/ >
/ >
) , [ AutoExportToggler , askBeforeClose , autoMerge , autoSaveProjectFile , customOutDir , invertCutSegments , keyframeCut , renderCaptureFormatButton , renderOutFmt , timecodeShowFrames , setOutp utDir, onWheelTunerRequested , language ] ) ;
) , [ AutoExportToggler , askBeforeClose , autoMerge , autoSaveProjectFile , customOutDir , invertCutSegments , keyframeCut , renderCaptureFormatButton , renderOutFmt , timecodeShowFrames , changeO utDir, onWheelTunerRequested , language ] ) ;
useEffect ( ( ) => {
useEffect ( ( ) => {
if ( ! isStoreBuild ) loadMifiLink ( ) . then ( setMifiLink ) ;
if ( ! isStoreBuild ) loadMifiLink ( ) . then ( setMifiLink ) ;
@ -1463,7 +1468,7 @@ const App = memo(() => {
useEffect ( ( ) => {
useEffect ( ( ) => {
/ / T e s t i n g :
/ / T e s t i n g :
/ / i f ( i s D e v ) l o a d ( '/ U s e r s / m i f i / D o w n l o a d s / i n p . M O V ' ) ;
/ / i f ( i s D e v ) l o a d ( { f i l e P a t h : '/ U s e r s / m i f i / D o w n l o a d s / i n p . M O V ' , c u s t o m O u t D i r } ) ;
/ / 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
} , [ ] ) ;
} , [ ] ) ;
@ -1527,7 +1532,7 @@ const App = memo(() => {
copyAnyAudioTrack = { copyAnyAudioTrack }
copyAnyAudioTrack = { copyAnyAudioTrack }
toggleStripAudio = { toggleStripAudio }
toggleStripAudio = { toggleStripAudio }
customOutDir = { customOutDir }
customOutDir = { customOutDir }
setOutputDir= { setOutp utDir}
changeOutDir= { changeO utDir}
renderOutFmt = { renderOutFmt }
renderOutFmt = { renderOutFmt }
outSegments = { outSegments }
outSegments = { outSegments }
autoMerge = { autoMerge }
autoMerge = { autoMerge }