diff --git a/public/configStore.js b/public/configStore.js index 40384185..06424a2f 100644 --- a/public/configStore.js +++ b/public/configStore.js @@ -128,6 +128,7 @@ const defaults = { allowMultipleInstances: false, darkMode: true, preferStrongColors: false, + outputFileNameMinZeroPadding: 1, }; // For portable app: https://github.com/mifi/lossless-cut/issues/645 diff --git a/src/App.jsx b/src/App.jsx index a6b2a2dd..bc74c106 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -185,7 +185,7 @@ const App = memo(() => { const allUserSettings = useUserSettingsRoot(); const { - captureFormat, setCaptureFormat, customOutDir, setCustomOutDir, keyframeCut, setKeyframeCut, preserveMovData, setPreserveMovData, movFastStart, setMovFastStart, avoidNegativeTs, autoMerge, timecodeFormat, invertCutSegments, setInvertCutSegments, autoExportExtraStreams, askBeforeClose, enableAskForImportChapters, enableAskForFileOpenAction, playbackVolume, setPlaybackVolume, autoSaveProjectFile, wheelSensitivity, invertTimelineScroll, language, ffmpegExperimental, hideNotifications, autoLoadTimecode, autoDeleteMergedSegments, exportConfirmEnabled, setExportConfirmEnabled, segmentsToChapters, setSegmentsToChapters, preserveMetadataOnMerge, setPreserveMetadataOnMerge, simpleMode, setSimpleMode, outSegTemplate, setOutSegTemplate, keyboardSeekAccFactor, keyboardNormalSeekSpeed, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, outFormatLocked, setOutFormatLocked, safeOutputFileName, setSafeOutputFileName, enableAutoHtml5ify, segmentsToChaptersOnly, keyBindings, setKeyBindings, resetKeyBindings, enableSmartCut, customFfPath, storeProjectInWorkingDir, setStoreProjectInWorkingDir, enableOverwriteOutput, mouseWheelZoomModifierKey, captureFrameMethod, captureFrameQuality, captureFrameFileNameFormat, enableNativeHevc, cleanupChoices, setCleanupChoices, darkMode, setDarkMode, preferStrongColors, + captureFormat, setCaptureFormat, customOutDir, setCustomOutDir, keyframeCut, setKeyframeCut, preserveMovData, setPreserveMovData, movFastStart, setMovFastStart, avoidNegativeTs, autoMerge, timecodeFormat, invertCutSegments, setInvertCutSegments, autoExportExtraStreams, askBeforeClose, enableAskForImportChapters, enableAskForFileOpenAction, playbackVolume, setPlaybackVolume, autoSaveProjectFile, wheelSensitivity, invertTimelineScroll, language, ffmpegExperimental, hideNotifications, autoLoadTimecode, autoDeleteMergedSegments, exportConfirmEnabled, setExportConfirmEnabled, segmentsToChapters, setSegmentsToChapters, preserveMetadataOnMerge, setPreserveMetadataOnMerge, simpleMode, setSimpleMode, outSegTemplate, setOutSegTemplate, keyboardSeekAccFactor, keyboardNormalSeekSpeed, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, outFormatLocked, setOutFormatLocked, safeOutputFileName, setSafeOutputFileName, enableAutoHtml5ify, segmentsToChaptersOnly, keyBindings, setKeyBindings, resetKeyBindings, enableSmartCut, customFfPath, storeProjectInWorkingDir, setStoreProjectInWorkingDir, enableOverwriteOutput, mouseWheelZoomModifierKey, captureFrameMethod, captureFrameQuality, captureFrameFileNameFormat, enableNativeHevc, cleanupChoices, setCleanupChoices, darkMode, setDarkMode, preferStrongColors, outputFileNameMinZeroPadding, } = allUserSettings; useEffect(() => { @@ -1100,8 +1100,8 @@ const App = memo(() => { }, [isFileOpened, cleanupChoices, askForCleanupChoices, cleanupFiles, setWorking]); const generateOutSegFileNames = useCallback(({ segments = segmentsToExport, template, forceSafeOutputFileName }) => ( - generateOutSegFileNamesRaw({ segments, template, forceSafeOutputFileName, formatTimecode, isCustomFormatSelected, fileFormat, filePath, safeOutputFileName, maxLabelLength }) - ), [fileFormat, filePath, formatTimecode, isCustomFormatSelected, maxLabelLength, safeOutputFileName, segmentsToExport]); + generateOutSegFileNamesRaw({ segments, template, forceSafeOutputFileName, formatTimecode, isCustomFormatSelected, fileFormat, filePath, safeOutputFileName, maxLabelLength, outputFileNameMinZeroPadding }) + ), [fileFormat, filePath, formatTimecode, isCustomFormatSelected, maxLabelLength, outputFileNameMinZeroPadding, safeOutputFileName, segmentsToExport]); const getOutSegError = useCallback((fileNames) => getOutSegErrorRaw({ fileNames, filePath, outputDir, safeOutputFileName }), [filePath, outputDir, safeOutputFileName]); diff --git a/src/components/OutSegTemplateEditor.jsx b/src/components/OutSegTemplateEditor.jsx index 02dc8cee..0554d655 100644 --- a/src/components/OutSegTemplateEditor.jsx +++ b/src/components/OutSegTemplateEditor.jsx @@ -1,27 +1,31 @@ -import React, { memo, useState, useEffect, useCallback, useRef } from 'react'; +import React, { memo, useState, useEffect, useCallback, useRef, useMemo } from 'react'; import { useDebounce } from 'use-debounce'; import i18n from 'i18next'; import { useTranslation } from 'react-i18next'; import { WarningSignIcon, ErrorIcon, Button, IconButton, TickIcon, ResetIcon } from 'evergreen-ui'; import withReactContent from 'sweetalert2-react-content'; import { IoIosHelpCircle } from 'react-icons/io'; +import { motion, AnimatePresence } from 'framer-motion'; import Swal from '../swal'; import HighlightedText from './HighlightedText'; -import { defaultOutSegTemplate, segNumVariable } from '../util/outputNameTemplate'; +import { defaultOutSegTemplate, segNumVariable, segSuffixVariable } from '../util/outputNameTemplate'; import useUserSettings from '../hooks/useUserSettings'; +import Switch from './Switch'; +import Select from './Select'; const ReactSwal = withReactContent(Swal); const electron = window.require('electron'); -// eslint-disable-next-line no-template-curly-in-string -const extVar = '${EXT}'; +const formatVariable = (variable) => `\${${variable}}`; + +const extVar = formatVariable('EXT'); const inputStyle = { flexGrow: 1, fontFamily: 'inherit', fontSize: '.8em', backgroundColor: 'var(--gray3)', color: 'var(--gray12)', border: '1px solid var(--gray7)', appearance: 'none' }; const OutSegTemplateEditor = memo(({ outSegTemplate, setOutSegTemplate, generateOutSegFileNames, currentSegIndexSafe, getOutSegError }) => { - const { safeOutputFileName, toggleSafeOutputFileName } = useUserSettings(); + const { safeOutputFileName, toggleSafeOutputFileName, outputFileNameMinZeroPadding, setOutputFileNameMinZeroPadding } = useUserSettings(); const [text, setText] = useState(outSegTemplate); const [debouncedText] = useDebounce(text, 500); @@ -33,6 +37,8 @@ const OutSegTemplateEditor = memo(({ outSegTemplate, setOutSegTemplate, generate const { t } = useTranslation(); + const hasTextNumericPaddedValue = useMemo(() => [segNumVariable, segSuffixVariable].some((v) => debouncedText.includes(formatVariable(v))), [debouncedText]); + useEffect(() => { if (debouncedText == null) return; @@ -92,44 +98,63 @@ const OutSegTemplateEditor = memo(({ outSegTemplate, setOutSegTemplate, generate const startPos = input.selectionStart; const endPos = input.selectionEnd; - const newValue = `${text.slice(0, startPos)}${`\${${variable}}${text.slice(endPos)}`}`; + const newValue = `${text.slice(0, startPos)}${`${formatVariable(variable)}${text.slice(endPos)}`}`; setText(newValue); }, [text]); return ( - <> -