@ -24,7 +24,7 @@ import useKeyboard from './hooks/useKeyboard';
import useFileFormatState from './hooks/useFileFormatState' ;
import useFrameCapture from './hooks/useFrameCapture' ;
import useSegments from './hooks/useSegments' ;
import useDirectoryAccess from './hooks/useDirectoryAccess' ;
import useDirectoryAccess , { DirectoryAccessDeclinedError } from './hooks/useDirectoryAccess' ;
import UserSettingsContext from './contexts/UserSettingsContext' ;
@ -387,10 +387,10 @@ const App = memo(() => {
const projectSuffix = 'proj.llc' ;
const oldProjectSuffix = 'llc-edl.csv' ;
/ / N e w L L C f o r m a t c a n b e s t o r e d a l o n g w i t h i n p u t f i l e o r i n w o r k i n g d i r ( c u s t o m O u t D i r )
const getEdlFilePath = useCallback ( ( fp , storeProjectInWorkingDir2 = false ) => getSuffixedOutPath ( { customOutDir : storeProje ctInW orkingDir2 ? customOutDir : undefine d, filePath : fp , nameSuffix : projectSuffix } ) , [ customOutDir ] ) ;
/ / O l d v e r s i o n s o f L o s s l e s s C u t u s e d C S V f i l e s a n d s t o r e d t h e m in c u s t o m O u t D i r :
const getEdlFilePathOld = useCallback ( ( fp ) => getSuffixedOutPath ( { customOutDir , filePath : fp , nameSuffix : oldProjectSuffix } ) , [ customOutDir ] ) ;
const projectFileSavePath = useMemo ( ( ) => getEdlFilePath ( filePath , storeProjectInWorkingDir ) , [ getEdlFilePath , filePath , storeProjectInWorking Dir] ) ;
const getEdlFilePath = useCallback ( ( fp , cod ) => getSuffixedOutPath ( { customOutDir : cod, filePath : fp , nameSuffix : projectSuffix } ) , [ ] ) ;
/ / O l d v e r s i o n s o f L o s s l e s s C u t u s e d C S V f i l e s a n d s t o r e d t h e m al w a y s in c u s t o m O u t D i r :
const getEdlFilePathOld = useCallback ( ( fp , cod ) => getSuffixedOutPath ( { customOutDir : cod , filePath : fp , nameSuffix : oldProjectSuffix } ) , [ ] ) ;
const projectFileSavePath = useMemo ( ( ) => getEdlFilePath ( filePath , storeProjectInWorkingDir ? customOutDir : undefined ) , [ getEdlFilePath , filePath , storeProjectInWorking Dir, customOut Dir] ) ;
const currentSaveOperation = useMemo ( ( ) => {
if ( ! projectFileSavePath ) return undefined ;
@ -452,7 +452,7 @@ const App = memo(() => {
if ( ! supportsRotation && ! hideAllNotifications ) toast . fire ( { text : i18n . t ( 'Lossless rotation might not work with this file format. You may try changing to MP4' ) } ) ;
} , [ hideAllNotifications , fileFormat ] ) ;
const { ensureWritable Dirs } = useDirectoryAccess ( { customOutDir , setCustomOutDir } ) ;
const { ensureWritable Out Dir, en sureAccessToSourceDir } = useDirectoryAccess ( { customOutDir , setCustomOutDir } ) ;
const toggleCaptureFormat = useCallback ( ( ) => setCaptureFormat ( f => ( f === 'png' ? 'jpeg' : 'png' ) ) , [ setCaptureFormat ] ) ;
@ -723,12 +723,13 @@ const App = memo(() => {
for ( const path of filePaths ) {
try {
/ / e s l i n t - d i s a b l e - n e x t - l i n e n o - a w a i t - i n - l o o p
const { newCustomOutDir , cancel } = await ensureWritableDirs ( { inputPath : path } ) ;
if ( cancel ) return ;
const newCustomOutDir = await ensureWritableOutDir ( path ) ;
/ / e s l i n t - d i s a b l e - n e x t - l i n e n o - a w a i t - i n - l o o p
await html5ify ( { customOutDir : newCustomOutDir , filePath : path , speed , hasAudio : true , hasVideo : true , onProgress : setTotalProgress } ) ;
} catch ( err2 ) {
if ( err2 instanceof DirectoryAccessDeclinedError ) return ;
console . error ( 'Failed to html5ify' , path , err2 ) ;
failedFiles . push ( path ) ;
}
@ -745,7 +746,7 @@ const App = memo(() => {
setWorking ( ) ;
setCutProgress ( ) ;
}
} , [ batchFiles , ensureWritable Dirs , html5ify , setWorking ] ) ;
} , [ batchFiles , ensureWritable Out Dir, html5ify , setWorking ] ) ;
const getConvertToSupportedFormat = useCallback ( ( fallback ) => rememberConvertToSupportedFormat || fallback , [ rememberConvertToSupportedFormat ] ) ;
@ -887,8 +888,7 @@ const App = memo(() => {
const firstPath = paths [ 0 ] ;
if ( ! firstPath ) return ;
const { newCustomOutDir , cancel } = await ensureWritableDirs ( { inputPath : firstPath } ) ;
if ( cancel ) return ;
const newCustomOutDir = await ensureWritableOutDir ( firstPath ) ;
const outDir = getOutDir ( newCustomOutDir , firstPath ) ;
@ -917,6 +917,8 @@ const App = memo(() => {
if ( ! includeAllStreams && haveExcludedStreams ) notices . push ( i18n . t ( 'Some extra tracks have been discarded. You can change this option before merging.' ) ) ;
if ( ! hideAllNotifications ) openConcatFinishedToast ( { filePath : outPath , notices , warnings } ) ;
} catch ( err ) {
if ( err instanceof DirectoryAccessDeclinedError ) return ;
if ( err . killed === true ) {
/ / a s s u m e e x e c a k i l l e d ( a b o r t e d b y u s e r )
return ;
@ -940,7 +942,7 @@ const App = memo(() => {
setWorking ( ) ;
setCutProgress ( ) ;
}
} , [ setWorking , ensureWritable Dirs , segmentsToChapters , concatFiles , ffmpegExperimental , preserveMovData , movFastStart , preserveMetadataOnMerge , closeBatch , hideAllNotifications , handleConcatFailed ] ) ;
} , [ setWorking , ensureWritable Out Dir, segmentsToChapters , concatFiles , ffmpegExperimental , preserveMovData , movFastStart , preserveMetadataOnMerge , closeBatch , hideAllNotifications , handleConcatFailed ] ) ;
const cleanupFiles = useCallback ( async ( cleanupChoices2 ) => {
/ / S t o r e p a t h s b e f o r e w e r e s e t s t a t e
@ -1274,24 +1276,35 @@ const App = memo(() => {
return true ;
}
async function tryOpenProject ( { chapters } ) {
const storeProjectInSourceDir = ! storeProjectInWorkingDir ;
async function tryOpenProject ( { chapters , cod } ) {
try {
if ( projectPath ) {
await loadEdlFile ( { path : projectPath , type : 'llc' } ) ;
return ;
/ / F i r s t t r y t o o p e n f r o m f r o m w o r k i n g d i r
if ( await tryOpenProjectPath ( getEdlFilePath ( fp , cod ) , 'llc' ) ) return ;
/ / t h e n t r y t o o p e n p r o j e c t f r o m s o u r c e f i l e d i r
const sameDirEdlFilePath = getEdlFilePath ( fp ) ;
/ / M A S o n l y a l l o w s f s . s t a t ( f s - e x t r a . e x i s t s ) i f w e d o n ' t h a v e a c c e s s t o i n p u t d i r y e t , s o c h e c k f i r s t i f t h e f i l e e x i s t s ,
/ / s o w e d o n ' t n e e d t o a n n o y t h e u s e r b y a s k i n g f o r p e r m i s s i o n i f t h e p r o j e c t f i l e d o e s n ' t e x i s t
if ( await exists ( sameDirEdlFilePath ) ) {
/ / O k , t h e f i l e e x i s t s . n o w w e h a v e t o a s k t h e u s e r , b e c a u s e w e n e e d t o r e a d t h a t f i l e
await ensureAccessToSourceDir ( fp ) ;
/ / O k , w e g o t a c c e s s f r o m t h e u s e r ( o r a l r e a d y h a v e a c c e s s ) , n o w r e a d t h e p r o j e c t f i l e
await loadEdlFile ( { path : sameDirEdlFilePath , type : 'llc' } ) ;
}
/ / F i r s t t r y t o o p e n f r o m s o u r c e f i l e d i r , t h e n f r o m w o r k i n g d i r , t h e n f i n a l l y o l d c s v s t y l e p r o j e c t
if ( await tryOpenProjectPath ( getEdlFilePath ( fp , true ) , 'llc' ) ) return ;
if ( await tryOpenProjectPath ( getEdlFilePath ( fp , false ) , 'llc' ) ) return ;
if ( await tryOpenProjectPath ( getEdlFilePathOld ( fp ) , 'csv' ) ) return ;
/ / t h e n f i n a l l y o l d c s v s t y l e p r o j e c t
if ( await tryOpenProjectPath ( getEdlFilePathOld ( fp , cod ) , 'csv' ) ) return ;
/ / O K , w e d i d n ' t f i n d a p r o j e c t f i l e , i n s t e a d m a y b e t r y t o c r e a t e p r o j e c t ( s e g m e n t s ) f r o m c h a p t e r s
const edl = await tryMapChaptersToEdl ( chapters ) ;
if ( edl . length > 0 && enableAskForImportChapters && ( await askForImportChapters ( ) ) ) {
console . log ( 'Convert chapters to segments' , edl ) ;
loadCutSegments ( edl ) ;
}
} catch ( err ) {
if ( err instanceof DirectoryAccessDeclinedError ) throw err ;
console . error ( 'EDL load failed, but continuing' , err ) ;
errorToast ( ` ${ i18n . t ( 'Failed to load segments' ) } ( ${ err . message } ) ` ) ;
}
@ -1342,15 +1355,15 @@ const App = memo(() => {
const hevcPlaybackSupported = enableNativeHevc && await hevcPlaybackSupportedPromise ;
const mightNeedAutoHtml5ify = ! willPlayerProperlyHandleVideo ( { streams : fileMeta . streams , hevcPlaybackSupported } ) && validDuration ;
/ / n e e d t o e n s u r e w e h a v e a c c e s s t o w r i t e t o w o r k i n g d i r e c t o r y
const cod = await ensureWritableOutDir ( fp ) ;
/ / W e m a y b e b e w r i t i n g p r o j e c t f i l e t o i n p u t p a t h ' s d i r ( i f s t o r e P r o j e c t I n W o r k i n g D i r i s t r u e ) , o r w r i t e h t m l 5 i f i e d f i l e t o i n p u t d i r
const { newCustomOutDir : cod , canceled } = await ensureWritableDirs ( { inputPath : fp , checkInputDir : ! storeProjectInWorkingDir || mightNeedAutoHtml5ify } ) ;
if ( canceled ) return ;
/ / i f s t o r e P r o j e c t I n S o u r c e D i r i s t r u e , w e w i l l b e w r i t i n g p r o j e c t f i l e t o i n p u t p a t h ' s d i r , s o e n s u r e t h a t o n e t o o
if ( storeProjectInSourceDir ) await ensureAccessToSourceDir ( fp ) ;
const existingHtml5FriendlyFile = await findExistingHtml5FriendlyFile ( fp , cod ) ;
const needsAutoHtml5ify = ! existingHtml5FriendlyFile && mightNeedAutoHtml5ify ;
const needsAutoHtml5ify = ! existingHtml5FriendlyFile && ! willPlayerProperlyHandleVideo ( { streams : fileMeta . streams , hevcPlaybackSupported } ) && validDuration ;
/ / B E G I N S T A T E U P D A T E S :
@ -1370,7 +1383,11 @@ const App = memo(() => {
await html5ifyAndLoadWithPreferences ( cod , fp , 'fastest' , haveVideoStream , haveAudioStream ) ;
}
await tryOpenProject ( { chapters : fileMeta . chapters } ) ;
if ( projectPath ) {
await loadEdlFile ( { path : projectPath , type : 'llc' } ) ;
} else {
await tryOpenProject ( { chapters : fileMeta . chapters , cod } ) ;
}
/ / t h r o w n e w E r r o r ( ' t e s t ' ) ;
@ -1399,10 +1416,13 @@ const App = memo(() => {
/ / 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 / 5 1 5
setFilePath ( fp ) ;
} catch ( err ) {
if ( err ) {
if ( err instanceof DirectoryAccessDeclinedError ) return ;
}
resetState ( ) ;
throw err ;
}
} , [ setWorking , loadEdlFile , getEdlFilePath , getEdlFilePathOld , enableAskForImportChapters , loadCutSegments , autoLoadTimecode , enableNativeHevc , ensureWritable Dirs , storeProjectInWorking Dir, resetState , setCopyStreamIdsForPath , setFileFormat , outFormatLocked , setDetectedFileFormat , html5ifyAndLoadWithPreferences , showPreviewFileLoadedMessage , showUnsupportedFileMessage , hideAllNotifications ] ) ;
} , [ setWorking , loadEdlFile , getEdlFilePath , getEdlFilePathOld , enableAskForImportChapters , loadCutSegments , autoLoadTimecode , enableNativeHevc , ensureWritable Out Dir, storeProjectInWorking Dir, ensureAccessToSource Dir, resetState , setCopyStreamIdsForPath , setFileFormat , outFormatLocked , setDetectedFileFormat , html5ifyAndLoadWithPreferences , showPreviewFileLoadedMessage , showUnsupportedFileMessage , hideAllNotifications ] ) ;
const toggleLastCommands = useCallback ( ( ) => setLastCommandsVisible ( val => ! val ) , [ ] ) ;
const toggleSettings = useCallback ( ( ) => setSettingsVisible ( val => ! val ) , [ ] ) ;
@ -1429,7 +1449,16 @@ const App = memo(() => {
console . log ( { mediaFileName } ) ;
if ( ! mediaFileName ) return ;
projectPath = path ;
path = pathJoin ( dirname ( path ) , mediaFileName ) ;
const mediaFilePath = pathJoin ( dirname ( path ) , mediaFileName ) ;
/ / W e m i g h t n e e d t o g e t u s e r ' s a c c e s s t o t h e p r o j e c t f i l e ' s d i r e c t o r y , i n o r d e r t o r e a d t h e m e d i a f i l e
try {
await ensureAccessToSourceDir ( mediaFilePath ) ;
} catch ( err ) {
if ( err instanceof DirectoryAccessDeclinedError ) return ;
}
path = mediaFilePath ;
}
/ / B e c a u s e A p p l e i s b e i n g n a z i a b o u t t h e a b i l i t y t o o p e n " c o p y p r o t e c t e d D V D f i l e s "
const disallowVob = isMasBuild ;
@ -1439,7 +1468,7 @@ const App = memo(() => {
}
await loadMedia ( { filePath : path , projectPath } ) ;
} , [ loadMedia] ) ;
} , [ ensureAccessToSourceDir, loadMedia] ) ;
/ / t o d o m e r g e w i t h u s e r O p e n F i l e s ?
const batchOpenSingleFile = useCallback ( async ( path ) => {