diff --git a/src/main/configStore.ts b/src/main/configStore.ts index b07ed641..c8bba638 100644 --- a/src/main/configStore.ts +++ b/src/main/configStore.ts @@ -149,6 +149,7 @@ const defaults: Config = { preferStrongColors: false, outputFileNameMinZeroPadding: 1, cutFromAdjustmentFrames: 0, + cutToAdjustmentFrames: 0, invertTimelineScroll: undefined, storeWindowBounds: true, }; diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 359898b9..4ad39eac 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -172,7 +172,7 @@ function App() { const allUserSettings = useUserSettingsRoot(); const { - captureFormat, setCaptureFormat, customOutDir, setCustomOutDir, keyframeCut, setKeyframeCut, preserveMetadata, preserveChapters, preserveMovData, movFastStart, avoidNegativeTs, autoMerge, timecodeFormat, invertCutSegments, setInvertCutSegments, autoExportExtraStreams, askBeforeClose, enableAskForImportChapters, enableAskForFileOpenAction, playbackVolume, setPlaybackVolume, autoSaveProjectFile, wheelSensitivity, invertTimelineScroll, language, ffmpegExperimental, hideNotifications, hideOsNotifications, autoLoadTimecode, autoDeleteMergedSegments, exportConfirmEnabled, setExportConfirmEnabled, segmentsToChapters, preserveMetadataOnMerge, simpleMode, setSimpleMode, outSegTemplate, setOutSegTemplate, mergedFileTemplate, setMergedFileTemplate, keyboardSeekAccFactor, keyboardNormalSeekSpeed, keyboardSeekSpeed2, keyboardSeekSpeed3, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, outFormatLocked, setOutFormatLocked, safeOutputFileName, setSafeOutputFileName, enableAutoHtml5ify, segmentsToChaptersOnly, keyBindings, setKeyBindings, resetKeyBindings, enableSmartCut, customFfPath, storeProjectInWorkingDir, setStoreProjectInWorkingDir, enableOverwriteOutput, mouseWheelZoomModifierKey, mouseWheelFrameSeekModifierKey, mouseWheelKeyframeSeekModifierKey, captureFrameMethod, captureFrameQuality, captureFrameFileNameFormat, enableNativeHevc, cleanupChoices, setCleanupChoices, darkMode, setDarkMode, preferStrongColors, outputFileNameMinZeroPadding, cutFromAdjustmentFrames, + captureFormat, setCaptureFormat, customOutDir, setCustomOutDir, keyframeCut, setKeyframeCut, preserveMetadata, preserveChapters, preserveMovData, movFastStart, avoidNegativeTs, autoMerge, timecodeFormat, invertCutSegments, setInvertCutSegments, autoExportExtraStreams, askBeforeClose, enableAskForImportChapters, enableAskForFileOpenAction, playbackVolume, setPlaybackVolume, autoSaveProjectFile, wheelSensitivity, invertTimelineScroll, language, ffmpegExperimental, hideNotifications, hideOsNotifications, autoLoadTimecode, autoDeleteMergedSegments, exportConfirmEnabled, setExportConfirmEnabled, segmentsToChapters, preserveMetadataOnMerge, simpleMode, setSimpleMode, outSegTemplate, setOutSegTemplate, mergedFileTemplate, setMergedFileTemplate, keyboardSeekAccFactor, keyboardNormalSeekSpeed, keyboardSeekSpeed2, keyboardSeekSpeed3, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, outFormatLocked, setOutFormatLocked, safeOutputFileName, setSafeOutputFileName, enableAutoHtml5ify, segmentsToChaptersOnly, keyBindings, setKeyBindings, resetKeyBindings, enableSmartCut, customFfPath, storeProjectInWorkingDir, setStoreProjectInWorkingDir, enableOverwriteOutput, mouseWheelZoomModifierKey, mouseWheelFrameSeekModifierKey, mouseWheelKeyframeSeekModifierKey, captureFrameMethod, captureFrameQuality, captureFrameFileNameFormat, enableNativeHevc, cleanupChoices, setCleanupChoices, darkMode, setDarkMode, preferStrongColors, outputFileNameMinZeroPadding, cutFromAdjustmentFrames, cutToAdjustmentFrames, } = allUserSettings; const { working, setWorking, workingRef, abortWorking } = useLoading(); @@ -597,7 +597,7 @@ function App() { const { concatFiles, html5ifyDummy, cutMultiple, autoConcatCutSegments, html5ify, fixInvalidDuration, extractStreams, - } = useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, needSmartCut, enableOverwriteOutput, outputPlaybackRate, cutFromAdjustmentFrames, appendLastCommandsLog, smartCutCustomBitrate: smartCutBitrate, appendFfmpegCommandLog }); + } = useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, needSmartCut, enableOverwriteOutput, outputPlaybackRate, cutFromAdjustmentFrames, cutToAdjustmentFrames, appendLastCommandsLog, smartCutCustomBitrate: smartCutBitrate, appendFfmpegCommandLog }); const { captureFrameFromTag, captureFrameFromFfmpeg, captureFramesRange } = useFrameCapture({ appendFfmpegCommandLog, formatTimecode, treatOutputFileModifiedTimeAsStart }); diff --git a/src/renderer/src/components/ExportConfirm.tsx b/src/renderer/src/components/ExportConfirm.tsx index 05265c18..def22d01 100644 --- a/src/renderer/src/components/ExportConfirm.tsx +++ b/src/renderer/src/components/ExportConfirm.tsx @@ -34,8 +34,20 @@ const outDirStyle: CSSProperties = { ...highlightedTextStyle, wordBreak: 'break- const warningStyle: CSSProperties = { color: 'var(--orange8)', fontSize: '80%', marginBottom: '.5em' }; +const adjustCutFromValues = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; +const adjustCutToValues = [-10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + const HelpIcon = ({ onClick, style }: { onClick: () => void, style?: CSSProperties }) => ; +function ShiftTimes({ values, num, setNum }: { values: number[], num: number, setNum: (n: number) => void }) { + const { t } = useTranslation(); + return ( + + ); +} + function ExportConfirm({ areWeCutting, selectedSegments, @@ -95,7 +107,7 @@ function ExportConfirm({ }) { const { t } = useTranslation(); - const { changeOutDir, keyframeCut, toggleKeyframeCut, preserveMovData, setPreserveMovData, preserveMetadata, setPreserveMetadata, preserveChapters, setPreserveChapters, movFastStart, setMovFastStart, avoidNegativeTs, setAvoidNegativeTs, autoDeleteMergedSegments, exportConfirmEnabled, toggleExportConfirmEnabled, segmentsToChapters, setSegmentsToChapters, preserveMetadataOnMerge, setPreserveMetadataOnMerge, enableSmartCut, setEnableSmartCut, effectiveExportMode, enableOverwriteOutput, setEnableOverwriteOutput, ffmpegExperimental, setFfmpegExperimental, cutFromAdjustmentFrames, setCutFromAdjustmentFrames } = useUserSettings(); + const { changeOutDir, keyframeCut, toggleKeyframeCut, preserveMovData, setPreserveMovData, preserveMetadata, setPreserveMetadata, preserveChapters, setPreserveChapters, movFastStart, setMovFastStart, avoidNegativeTs, setAvoidNegativeTs, autoDeleteMergedSegments, exportConfirmEnabled, toggleExportConfirmEnabled, segmentsToChapters, setSegmentsToChapters, preserveMetadataOnMerge, setPreserveMetadataOnMerge, enableSmartCut, setEnableSmartCut, effectiveExportMode, enableOverwriteOutput, setEnableOverwriteOutput, ffmpegExperimental, setFfmpegExperimental, cutFromAdjustmentFrames, setCutFromAdjustmentFrames, cutToAdjustmentFrames, setCutToAdjustmentFrames } = useUserSettings(); const togglePreserveChapters = useCallback(() => setPreserveChapters((val) => !val), [setPreserveChapters]); const togglePreserveMovData = useCallback(() => setPreserveMovData((val) => !val), [setPreserveMovData]); @@ -350,19 +362,28 @@ function ExportConfirm({ {areWeCutting && ( - - - {t('Shift all start times')} - - - - - - - - + <> + + + {t('Shift all start times')} + + + + + + + + + + + {t('Shift all end times')} + + + + + + + )} {isMov && ( diff --git a/src/renderer/src/hooks/useFfmpegOperations.ts b/src/renderer/src/hooks/useFfmpegOperations.ts index 73ab27e6..ed9fd0ac 100644 --- a/src/renderer/src/hooks/useFfmpegOperations.ts +++ b/src/renderer/src/hooks/useFfmpegOperations.ts @@ -76,7 +76,7 @@ async function pathExists(path: string) { } } -function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, needSmartCut, enableOverwriteOutput, outputPlaybackRate, cutFromAdjustmentFrames, appendLastCommandsLog, smartCutCustomBitrate, appendFfmpegCommandLog }: { +function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, needSmartCut, enableOverwriteOutput, outputPlaybackRate, cutFromAdjustmentFrames, cutToAdjustmentFrames, appendLastCommandsLog, smartCutCustomBitrate, appendFfmpegCommandLog }: { filePath: string | undefined, treatInputFileModifiedTimeAsStart: boolean | null | undefined, treatOutputFileModifiedTimeAsStart: boolean | null | undefined, @@ -84,6 +84,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea needSmartCut: boolean, outputPlaybackRate: number, cutFromAdjustmentFrames: number, + cutToAdjustmentFrames: number, appendLastCommandsLog: (a: string) => void, smartCutCustomBitrate: number | undefined, appendFfmpegCommandLog: (args: string[]) => void, @@ -263,11 +264,12 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea const cuttingStart = isCuttingStart(cutFrom); const cutFromWithAdjustment = cutFrom + cutFromAdjustmentFrames * frameDuration; + const cutToWithAdjustment = cutTo + cutToAdjustmentFrames * frameDuration; const cuttingEnd = isCuttingEnd(cutTo, videoDuration); const areWeCutting = cuttingStart || cuttingEnd; - if (areWeCutting) console.log('Cutting from', cuttingStart ? `${cutFrom} (${cutFromWithAdjustment} adjusted ${cutFromAdjustmentFrames} frames)` : 'start', 'to', cuttingEnd ? cutTo : 'end'); + if (areWeCutting) console.log('Cutting from', cuttingStart ? `${cutFrom} (${cutFromWithAdjustment} adjusted ${cutFromAdjustmentFrames} frames)` : 'start', 'to', cuttingEnd ? `${cutTo} (adjusted ${cutToAdjustmentFrames} frames)` : 'end'); - let cutDuration = cutTo - cutFromWithAdjustment; + let cutDuration = cutToWithAdjustment - cutFromWithAdjustment; if (detectedFps != null) cutDuration = Math.max(cutDuration, frameDuration); // ensure at least one frame duration // Don't cut if no need: https://github.com/mifi/lossless-cut/issues/50 @@ -415,7 +417,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea logStdoutStderr(result); await transferTimestamps({ inPath: filePath, outPath, cutFrom, cutTo, treatInputFileModifiedTimeAsStart, duration: isDurationValid(videoDuration) ? videoDuration : undefined, treatOutputFileModifiedTimeAsStart }); - }, [appendFfmpegCommandLog, cutFromAdjustmentFrames, filePath, getOutputPlaybackRateArgs, shouldSkipExistingFile, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart]); + }, [appendFfmpegCommandLog, cutFromAdjustmentFrames, cutToAdjustmentFrames, filePath, getOutputPlaybackRateArgs, shouldSkipExistingFile, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart]); // inspired by https://gist.github.com/fernandoherreradelasheras/5eca67f4200f1a7cc8281747da08496e const cutEncodeSmartPart = useCallback(async ({ cutFrom, cutTo, outPath, outFormat, videoCodec, videoBitrate, videoTimebase, allFilesMeta, copyFileStreams, videoStreamIndex, ffmpegExperimental }: { diff --git a/src/renderer/src/hooks/useUserSettingsRoot.ts b/src/renderer/src/hooks/useUserSettingsRoot.ts index b7cc288b..d9542667 100644 --- a/src/renderer/src/hooks/useUserSettingsRoot.ts +++ b/src/renderer/src/hooks/useUserSettingsRoot.ts @@ -163,6 +163,8 @@ export default () => { useEffect(() => safeSetConfig({ outputFileNameMinZeroPadding }), [outputFileNameMinZeroPadding]); const [cutFromAdjustmentFrames, setCutFromAdjustmentFrames] = useState(safeGetConfigInitial('cutFromAdjustmentFrames')); useEffect(() => safeSetConfig({ cutFromAdjustmentFrames }), [cutFromAdjustmentFrames]); + const [cutToAdjustmentFrames, setCutToAdjustmentFrames] = useState(safeGetConfigInitial('cutToAdjustmentFrames')); + useEffect(() => safeSetConfig({ cutToAdjustmentFrames }), [cutToAdjustmentFrames]); const [storeWindowBounds, setStoreWindowBounds] = useState(safeGetConfigInitial('storeWindowBounds')); useEffect(() => safeSetConfig({ storeWindowBounds }), [storeWindowBounds]); @@ -301,6 +303,8 @@ export default () => { setOutputFileNameMinZeroPadding, cutFromAdjustmentFrames, setCutFromAdjustmentFrames, + cutToAdjustmentFrames, + setCutToAdjustmentFrames, storeWindowBounds, setStoreWindowBounds, }; diff --git a/types.ts b/types.ts index b08ad2a6..34828ac2 100644 --- a/types.ts +++ b/types.ts @@ -111,6 +111,7 @@ export interface Config { preferStrongColors: boolean, outputFileNameMinZeroPadding: number, cutFromAdjustmentFrames: number, + cutToAdjustmentFrames: number, invertTimelineScroll: boolean | undefined, }