@ -1,4 +1,4 @@
import React , { memo , useCallback , useMemo } from 'react' ;
import React , { memo , useCallback , useMemo , useState } from 'react' ;
import { FaYinYang , FaKeyboard } from 'react-icons/fa' ;
import { GlobeIcon , CleanIcon , CogIcon , Button , NumericalIcon , FolderCloseIcon , DocumentIcon , TimeIcon } from 'evergreen-ui' ;
import { useTranslation } from 'react-i18next' ;
@ -35,8 +35,10 @@ const Settings = memo(({
onKeyboardShortcutsDialogRequested ,
askForCleanupChoices ,
toggleStoreProjectInWorkingDir ,
simpleMode ,
} ) => {
const { t } = useTranslation ( ) ;
const [ showAdvanced , setShowAdvanced ] = useState ( ! simpleMode ) ;
const { customOutDir , changeOutDir , keyframeCut , toggleKeyframeCut , timecodeFormat , setTimecodeFormat , invertCutSegments , setInvertCutSegments , askBeforeClose , setAskBeforeClose , enableAskForImportChapters , setEnableAskForImportChapters , enableAskForFileOpenAction , setEnableAskForFileOpenAction , autoSaveProjectFile , setAutoSaveProjectFile , invertTimelineScroll , setInvertTimelineScroll , language , setLanguage , ffmpegExperimental , setFfmpegExperimental , hideNotifications , setHideNotifications , autoLoadTimecode , setAutoLoadTimecode , enableAutoHtml5ify , setEnableAutoHtml5ify , customFfPath , setCustomFfPath , storeProjectInWorkingDir , enableOverwriteOutput , setEnableOverwriteOutput , mouseWheelZoomModifierKey , setMouseWheelZoomModifierKey , captureFrameMethod , setCaptureFrameMethod , captureFrameQuality , setCaptureFrameQuality , captureFrameFileNameFormat , setCaptureFrameFileNameFormat , enableNativeHevc , setEnableNativeHevc , enableUpdateCheck , setEnableUpdateCheck , allowMultipleInstances , setAllowMultipleInstances , preferStrongColors , setPreferStrongColors , treatInputFileModifiedTimeAsStart , setTreatInputFileModifiedTimeAsStart , treatOutputFileModifiedTimeAsStart , setTreatOutputFileModifiedTimeAsStart } = useUserSettings ( ) ;
@ -78,7 +80,15 @@ const Settings = memo(({
< th style = { { width : 300 } } > { t ( 'Current setting' ) } < / th >
< / tr >
< / thead >
< tbody >
< Row >
< KeyCell > { t ( 'Show advanced settings' ) } < / KeyCell >
< td >
< Switch checked = { showAdvanced } onCheckedChange = { setShowAdvanced } / >
< / td >
< / Row >
< Row >
< KeyCell > < GlobeIcon style = { { verticalAlign : 'middle' , marginRight : '.5em' } } / > App language < / KeyCell >
< td >
@ -89,6 +99,67 @@ const Settings = memo(({
< / td >
< / Row >
{ showAdvanced && (
< Row >
< KeyCell >
{ t ( 'Auto save project file?' ) } < br / >
< / KeyCell >
< td >
< Switch checked = { autoSaveProjectFile } onCheckedChange = { setAutoSaveProjectFile } / >
< / td >
< / Row >
) }
{ showAdvanced && (
< Row >
< KeyCell > { t ( 'Store project file (.llc) in the working directory or next to loaded media file?' ) } < / KeyCell >
< td >
< Button iconBefore = { storeProjectInWorkingDir ? FolderCloseIcon : DocumentIcon } disabled = { ! autoSaveProjectFile } onClick = { toggleStoreProjectInWorkingDir } >
{ storeProjectInWorkingDir ? t ( 'Store in working directory' ) : t ( 'Store next to media file' ) }
< / Button >
< / td >
< / Row >
) }
{ showAdvanced && ! isMasBuild && (
< Row >
< KeyCell >
{ t ( 'Custom FFmpeg directory (experimental)' ) } < br / >
< div style = { detailsStyle } >
{ t ( 'This allows you to specify custom FFmpeg and FFprobe binaries to use. Make sure the "ffmpeg" and "ffprobe" executables exist in the same directory, and then select the directory.' ) }
< / div >
< / KeyCell >
< td >
< Button iconBefore = { CogIcon } onClick = { changeCustomFfPath } >
{ customFfPath ? t ( 'Using external ffmpeg' ) : t ( 'Using built-in ffmpeg' ) }
< / Button >
< div > { customFfPath } < / div >
< / td >
< / Row >
) }
{ showAdvanced && ! isStoreBuild && (
< Row >
< KeyCell > { t ( 'Check for updates on startup?' ) } < / KeyCell >
< td >
< Switch checked = { enableUpdateCheck } onCheckedChange = { setEnableUpdateCheck } / >
< / td >
< / Row >
) }
{ showAdvanced && (
< Row >
< KeyCell > { t ( 'Allow multiple instances of LosslessCut to run concurrently? (experimental)' ) } < / KeyCell >
< td >
< Switch checked = { allowMultipleInstances } onCheckedChange = { setAllowMultipleInstances } / >
< / td >
< / Row >
) }
< Header title = { t ( 'Options affecting exported files' ) } / >
< Row >
< KeyCell >
{ t ( 'Choose cutting mode: Remove or keep selected segments from video when exporting?' ) } < br / >
@ -119,94 +190,30 @@ const Settings = memo(({
< / td >
< / Row >
< Row >
< KeyCell >
{ t ( 'Auto save project file?' ) } < br / >
< / KeyCell >
< td >
< Switch checked = { autoSaveProjectFile } onCheckedChange = { setAutoSaveProjectFile } / >
< / td >
< / Row >
< Row >
< KeyCell > { t ( 'Store project file (.llc) in the working directory or next to loaded media file?' ) } < / KeyCell >
< td >
< Button iconBefore = { storeProjectInWorkingDir ? FolderCloseIcon : DocumentIcon } disabled = { ! autoSaveProjectFile } onClick = { toggleStoreProjectInWorkingDir } >
{ storeProjectInWorkingDir ? t ( 'Store in working directory' ) : t ( 'Store next to media file' ) }
< / Button >
< / td >
< / Row >
< Header title = { t ( 'Keyboard, mouse and input' ) } / >
< Row >
< KeyCell > { t ( 'Keyboard & mouse shortcuts' ) } < / KeyCell >
< td >
< Button iconBefore = { < FaKeyboard / > } onClick = { onKeyboardShortcutsDialogRequested } > { t ( 'Keyboard & mouse shortcuts' ) } < / Button >
< / td >
< / Row >
< Row >
< KeyCell > { t ( 'Mouse wheel zoom modifier key' ) } < / KeyCell >
< td >
< Select value = { mouseWheelZoomModifierKey } onChange = { ( e ) => setMouseWheelZoomModifierKey ( e . target . value ) } >
{ Object . entries ( getModifierKeyNames ( ) ) . map ( ( [ key , value ] ) => (
< option key = { key } value = { key } > { value } < / option >
) ) }
< / Select >
< / td >
< / Row >
< Row >
< KeyCell > { t ( 'Timeline trackpad/wheel sensitivity' ) } < / KeyCell >
< td >
< Button iconBefore = { CogIcon } onClick = { ( ) => onTunerRequested ( 'wheelSensitivity' ) } > { t ( 'Change value' ) } < / Button >
< / td >
< / Row >
< Row >
< KeyCell > { t ( 'Timeline keyboard seek speed' ) } < / KeyCell >
< td >
< Button iconBefore = { CogIcon } onClick = { ( ) => onTunerRequested ( 'keyboardNormalSeekSpeed' ) } > { t ( 'Change value' ) } < / Button >
< / td >
< / Row >
< Row >
< KeyCell > { t ( 'Timeline keyboard seek acceleration' ) } < / KeyCell >
< td >
< Button iconBefore = { CogIcon } onClick = { ( ) => onTunerRequested ( 'keyboardSeekAccFactor' ) } > { t ( 'Change value' ) } < / Button >
< / td >
< / Row >
< Row >
< KeyCell > { t ( 'Invert timeline trackpad/wheel direction?' ) } < / KeyCell >
< td >
< Switch checked = { invertTimelineScroll } onCheckedChange = { setInvertTimelineScroll } / >
< / td >
< / Row >
< Header title = { t ( 'Options affecting exported files' ) } / >
< Row >
< KeyCell > { t ( 'Set file modification date/time of output files to:' ) } < / KeyCell >
< td >
< Select value = { treatOutputFileModifiedTimeAsStart ? ? 'disabled' } onChange = { ( e ) => setTreatOutputFileModifiedTimeAsStart ( e . target . value === 'disabled' ? null : ( e . target . value === 'true' ) ) } >
< option value = "disabled" > { t ( 'Current time' ) } < / option >
< option value = "true" > { t ( 'Source file\'s time plus segment start cut time' ) } < / option >
< option value = "false" > { t ( 'Source file\'s time minus segment end cut time' ) } < / option >
< / Select >
< / td >
< / Row >
{ showAdvanced && (
< Row >
< KeyCell > { t ( 'Set file modification date/time of output files to:' ) } < / KeyCell >
< td >
< Select value = { treatOutputFileModifiedTimeAsStart ? ? 'disabled' } onChange = { ( e ) => setTreatOutputFileModifiedTimeAsStart ( e . target . value === 'disabled' ? null : ( e . target . value === 'true' ) ) } >
< option value = "disabled" > { t ( 'Current time' ) } < / option >
< option value = "true" > { t ( 'Source file\'s time plus segment start cut time' ) } < / option >
< option value = "false" > { t ( 'Source file\'s time minus segment end cut time' ) } < / option >
< / Select >
< / td >
< / Row >
) }
< Row >
< KeyCell > { t ( 'Treat source file modification date/time as:' ) } < / KeyCell >
< td >
< Select disabled = { treatOutputFileModifiedTimeAsStart == null } value = { treatInputFileModifiedTimeAsStart } onChange = { ( e ) => setTreatInputFileModifiedTimeAsStart ( ( e . target . value === 'true' ) ) } >
< option value = "true" > { t ( 'Start of video' ) } < / option >
< option value = "false" > { t ( 'End of video' ) } < / option >
< / Select >
< / td >
< / Row >
{ showAdvanced && (
< Row >
< KeyCell > { t ( 'Treat source file modification date/time as:' ) } < / KeyCell >
< td >
< Select disabled = { treatOutputFileModifiedTimeAsStart == null } value = { treatInputFileModifiedTimeAsStart } onChange = { ( e ) => setTreatInputFileModifiedTimeAsStart ( ( e . target . value === 'true' ) ) } >
< option value = "true" > { t ( 'Start of video' ) } < / option >
< option value = "false" > { t ( 'End of video' ) } < / option >
< / Select >
< / td >
< / Row >
) }
< Row >
< KeyCell >
@ -244,6 +251,30 @@ const Settings = memo(({
< / td >
< / Row >
{ showAdvanced && (
< Row >
< KeyCell > { t ( 'Enable experimental ffmpeg features flag?' ) } < / KeyCell >
< td >
< Switch checked = { ffmpegExperimental } onCheckedChange = { setFfmpegExperimental } / >
< / td >
< / Row >
) }
{ showAdvanced && (
< Row >
< KeyCell >
{ t ( 'Extract unprocessable tracks to separate files or discard them?' ) } < br / >
< div style = { detailsStyle } >
{ t ( '(data tracks such as GoPro GPS, telemetry etc. are not copied over by default because ffmpeg cannot cut them, thus they will cause the media duration to stay the same after cutting video/audio)' ) }
< / div >
< / KeyCell >
< td >
< AutoExportToggler / >
< / td >
< / Row >
) }
< Header title = { t ( 'Snapshots and frame extraction' ) } / >
< Row >
@ -284,126 +315,128 @@ const Settings = memo(({
< / td >
< / Row >
< Header title = { t ( 'Keyboard, mouse and input' ) } / >
< Row >
< KeyCell > { t ( 'In timecode show' ) } < / KeyCell >
< KeyCell > { t ( ' Keyboard & mouse shortcuts ') } < / KeyCell >
< td >
< Button iconBefore = { timecodeFormat === 'frameCount' ? NumericalIcon : TimeIcon } onClick = { onTimecodeFormatClick } >
{ timecodeFormatOptions [ timecodeFormat ] }
< / Button >
< Button iconBefore = { < FaKeyboard / > } onClick = { onKeyboardShortcutsDialogRequested } > { t ( 'Keyboard & mouse shortcuts' ) } < / Button >
< / td >
< / Row >
< Header title = { t ( 'Prompts and dialogs' ) } / >
< Row >
< KeyCell > { t ( ' Show informational notifications ') } < / KeyCell >
< KeyCell > { t ( 'Mouse wheel zoom modifier key' ) } < / KeyCell >
< td >
< Switch checked = { ! hideNotifications } onCheckedChange = { ( v ) => setHideNotifications ( v ? undefined : 'all' ) } / >
< Select value = { mouseWheelZoomModifierKey } onChange = { ( e ) => setMouseWheelZoomModifierKey ( e . target . value ) } >
{ Object . entries ( getModifierKeyNames ( ) ) . map ( ( [ key , value ] ) => (
< option key = { key } value = { key } > { value } < / option >
) ) }
< / Select >
< / td >
< / Row >
< Row >
< KeyCell > { t ( ' Ask about what to do when opening a new file when another file is already already open? ') } < / KeyCell >
< KeyCell > { t ( ' Timeline trackpad/wheel sensitivity ') } < / KeyCell >
< td >
< Switch checked = { enableAskForFileOpenAction } onCheckedChange = { setEnableAskForFileOpenAction } / >
< Button iconBefore = { CogIcon } onClick = { ( ) => onTunerRequested ( 'wheelSensitivity' ) } > { t ( 'Change value' ) } < / Button >
< / td >
< / Row >
< Row >
< KeyCell > { t ( ' Ask for confirmation when closing app or file? ') } < / KeyCell >
< KeyCell > { t ( ' Timeline keyboard seek speed ') } < / KeyCell >
< td >
< Switch checked = { askBeforeClose } onCheckedChange = { setAskBeforeClose } / >
< Button iconBefore = { CogIcon } onClick = { ( ) => onTunerRequested ( 'keyboardNormalSeekSpeed' ) } > { t ( 'Change value' ) } < / Button >
< / td >
< / Row >
< Row >
< KeyCell > { t ( ' Ask about importing chapters from opened file? ') } < / KeyCell >
< KeyCell > { t ( ' Timeline keyboard seek acceleration ') } < / KeyCell >
< td >
< Switch checked = { enableAskForImportChapters } onCheckedChange = { setEnableAskForImportChapters } / >
< Button iconBefore = { CogIcon } onClick = { ( ) => onTunerRequested ( 'keyboardSeekAccFactor' ) } > { t ( 'Change value' ) } < / Button >
< / td >
< / Row >
< Header title = { t ( 'User interface' ) } / >
< Row >
< KeyCell > { t ( ' Prefer strong colors ') } < / KeyCell >
< KeyCell > { t ( 'Invert timeline trackpad/wheel direction?' ) } < / KeyCell >
< td >
< Switch checked = { preferStrongColors} onCheckedChange = { setPreferStrongColors } / >
< Switch checked = { invertTimelineScroll} onCheckedChange = { setInvertTimelineScroll } / >
< / td >
< / Row >
< Header title = { t ( ' Advanced options ') } / >
< Header title = { t ( ' User interface ') } / >
{ ! isMasBuil d && (
{ showAdvance d && (
< Row >
< KeyCell >
{ t ( 'Custom FFmpeg directory (experimental)' ) } < br / >
< div style = { detailsStyle } >
{ t ( 'This allows you to specify custom FFmpeg and FFprobe binaries to use. Make sure the "ffmpeg" and "ffprobe" executables exist in the same directory, and then select the directory.' ) }
< / div >
< / KeyCell >
< KeyCell > { t ( 'Auto load timecode from file as an offset in the timeline?' ) } < / KeyCell >
< td >
< Button iconBefore = { CogIcon } onClick = { changeCustomFfPath } >
{ customFfPath ? t ( 'Using external ffmpeg' ) : t ( 'Using built-in ffmpeg' ) }
< / Button >
< div > { customFfPath } < / div >
< Switch checked = { autoLoadTimecode } onCheckedChange = { setAutoLoadTimecode } / >
< / td >
< / Row >
) }
{ ! isStoreBuil d && (
{ showAdvance d && (
< Row >
< KeyCell > { t ( ' Check for updates on startup? ') } < / KeyCell >
< KeyCell > { t ( ' Enable HEVC / H265 hardware decoding (you may need to turn this off if you have problems with HEVC files) ') } < / KeyCell >
< td >
< Switch checked = { enableUpdateCheck } onCheckedChange = { setEnableUpdateCheck } / >
< Switch checked = { enableNativeHevc } onCheckedChange = { setEnableNativeHevc } / >
< / td >
< / Row >
) }
{ showAdvanced && (
< Row >
< KeyCell > { t ( 'Try to automatically convert to supported format when opening unsupported file?' ) } < / KeyCell >
< td >
< Switch checked = { enableAutoHtml5ify } onCheckedChange = { setEnableAutoHtml5ify } / >
< / td >
< / Row >
) }
< Row >
< KeyCell > { t ( 'Allow multiple instances of LosslessCut to run concurrently? (experimental)' ) } < / KeyCell >
< KeyCell > { t ( ' In timecode show ') } < / KeyCell >
< td >
< Switch checked = { allowMultipleInstances } onCheckedChange = { setAllowMultipleInstances } / >
< Button iconBefore = { timecodeFormat === 'frameCount' ? NumericalIcon : TimeIcon } onClick = { onTimecodeFormatClick } >
{ timecodeFormatOptions [ timecodeFormat ] }
< / Button >
< / td >
< / Row >
< Row >
< KeyCell > { t ( ' Enable HEVC / H265 hardware decoding (you may need to turn this off if you have problems with HEVC files) ') } < / KeyCell >
< KeyCell > { t ( ' Prefer strong colors ') } < / KeyCell >
< td >
< Switch checked = { enableNativeHevc} onCheckedChange = { setEnableNativeHevc } / >
< Switch checked = { preferStrongColors} onCheckedChange = { setPreferStrongColors } / >
< / td >
< / Row >
< Header title = { t ( 'Prompts and dialogs' ) } / >
< Row >
< KeyCell > { t ( 'Enable experimental ffmpeg features flag?' ) } < / KeyCell >
< KeyCell > { t ( ' Show informational notifications ') } < / KeyCell >
< td >
< Switch checked = { ffmpegExperimental } onCheckedChange = { setFfmpegExperimental } / >
< Switch checked = { ! hideNotifications } onCheckedChange = { ( v ) => setHideNotifications ( v ? undefined : 'all' ) } / >
< / td >
< / Row >
< Row >
< KeyCell > { t ( 'A uto load timecode from file as an offset in the timeline ?') } < / KeyCell >
< KeyCell > { t ( 'A sk about what to do when opening a new file when another file is already already open ?') } < / KeyCell >
< td >
< Switch checked = { autoLoadTimecode} onCheckedChange = { setAutoLoadTimecode } / >
< Switch checked = { enableAskForFileOpenAction} onCheckedChange = { setEnableAskForFileOpenAction } / >
< / td >
< / Row >
< Row >
< KeyCell > { t ( ' Try to automatically convert to supported format when opening unsupported file?') } < / KeyCell >
< KeyCell > { t ( ' Ask for confirmation when closing app or file?') } < / KeyCell >
< td >
< Switch checked = { enableAutoHtml5ify} onCheckedChange = { setEnableAutoHtml5ify } / >
< Switch checked = { askBeforeClose} onCheckedChange = { setAskBeforeClose } / >
< / td >
< / Row >
< Row >
< KeyCell >
{ t ( 'Extract unprocessable tracks to separate files or discard them?' ) } < br / >
< div style = { detailsStyle } >
{ t ( '(data tracks such as GoPro GPS, telemetry etc. are not copied over by default because ffmpeg cannot cut them, thus they will cause the media duration to stay the same after cutting video/audio)' ) }
< / div >
< / KeyCell >
< KeyCell > { t ( 'Ask about importing chapters from opened file?' ) } < / KeyCell >
< td >
< AutoExportToggler / >
< Switch checked = { enableAskForImportChapters } onCheckedChange = { setEnableAskForImportChapters } / >
< / td >
< / Row >
< / tbody >