diff --git a/package.json b/package.json index cb03dc1..88e61e6 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "@tsconfig/vite-react": "^3.0.0", "@types/eslint": "^8", "@types/lodash": "^4.14.202", + "@types/node": "18", "@types/sortablejs": "^1.15.0", "@typescript-eslint/eslint-plugin": "^6.12.0", "@typescript-eslint/parser": "^6.12.0", diff --git a/src/App.tsx b/src/App.tsx index e5563cc..574c96a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -85,7 +85,7 @@ import { rightBarWidth, leftBarWidth, ffmpegExtractWindow, zoomMax } from './uti import BigWaveform from './components/BigWaveform'; import isDev from './isDev'; -import { EdlFileType, FfmpegCommandLog, FfprobeChapter, FfprobeFormat, FfprobeStream, Html5ifyMode, PlaybackMode, StateSegment, Thumbnail, TunerType } from './types'; +import { EdlFileType, FfmpegCommandLog, FfprobeChapter, FfprobeFormat, FfprobeStream, Html5ifyMode, PlaybackMode, SegmentToExport, StateSegment, Thumbnail, TunerType } from './types'; const electron = window.require('electron'); const { exists } = window.require('fs-extra'); @@ -121,7 +121,7 @@ function App() { const [rotation, setRotation] = useState(360); const [cutProgress, setCutProgress] = useState(); const [startTimeOffset, setStartTimeOffset] = useState(0); - const [filePath, setFilePath] = useState(''); + const [filePath, setFilePath] = useState(); const [externalFilesMeta, setExternalFilesMeta] = useState({}); const [customTagsByFile, setCustomTagsByFile] = useState({}); const [paramsByStreamId, setParamsByStreamId] = useState(new Map()); @@ -444,7 +444,7 @@ function App() { const usingPreviewFile = !!previewFilePath; const effectiveFilePath = previewFilePath || filePath; const fileUri = useMemo(() => { - if (!effectiveFilePath) return ''; + if (!effectiveFilePath) return ''; // Setting video src="" prevents memory leak in chromium const uri = filePathToUrl(effectiveFilePath); // https://github.com/mifi/lossless-cut/issues/1674 if (cacheBuster !== 0) { @@ -458,10 +458,10 @@ function App() { const projectSuffix = 'proj.llc'; const oldProjectSuffix = 'llc-edl.csv'; // New LLC format can be stored along with input file or in working dir (customOutDir) - const getEdlFilePath = useCallback((fp: string, cod?: string) => getSuffixedOutPath({ customOutDir: cod, filePath: fp, nameSuffix: projectSuffix }), []); + const getEdlFilePath = useCallback((fp?: string, cod?: string) => getSuffixedOutPath({ customOutDir: cod, filePath: fp, nameSuffix: projectSuffix }), []); // Old versions of LosslessCut used CSV files and stored them always in customOutDir: const getEdlFilePathOld = useCallback((fp, cod) => getSuffixedOutPath({ customOutDir: cod, filePath: fp, nameSuffix: oldProjectSuffix }), []); - const getProjectFileSavePath = useCallback((storeProjectInWorkingDirIn) => getEdlFilePath(filePath, storeProjectInWorkingDirIn ? customOutDir : undefined), [getEdlFilePath, filePath, customOutDir]); + const getProjectFileSavePath = useCallback((storeProjectInWorkingDirIn: boolean) => getEdlFilePath(filePath, storeProjectInWorkingDirIn ? customOutDir : undefined), [getEdlFilePath, filePath, customOutDir]); const projectFileSavePath = useMemo(() => getProjectFileSavePath(storeProjectInWorkingDir), [getProjectFileSavePath, storeProjectInWorkingDir]); const currentSaveOperation = useMemo(() => { @@ -664,7 +664,7 @@ function App() { const allFilesMeta = useMemo(() => ({ ...externalFilesMeta, - [filePath]: mainFileMeta, + ...(filePath ? { [filePath]: mainFileMeta } : {}), }), [externalFilesMeta, filePath, mainFileMeta]); // total number of streams for ALL files @@ -742,6 +742,7 @@ function App() { const { waveforms } = useWaveform({ darkMode, filePath, relevantTime, waveformEnabled, audioStream: activeAudioStream, shouldShowWaveform, ffmpegExtractWindow, durationSafe }); const resetMergedOutFileName = useCallback(() => { + if (fileFormat == null || filePath == null) return; const ext = getOutFileExtension({ isCustomFormatSelected, outFormat: fileFormat, filePath }); const outFileName = getSuffixedFileName(filePath, `cut-merged-${Date.now()}${ext}`); setMergedOutFileName(outFileName); @@ -770,7 +771,7 @@ function App() { setRotation(360); setCutProgress(undefined); setStartTimeOffset(0); - setFilePath(''); // Setting video src="" prevents memory leak in chromium + setFilePath(undefined); setExternalFilesMeta({}); setCustomTagsByFile({}); setParamsByStreamId(new Map()); @@ -1173,16 +1174,17 @@ function App() { } }, [cleanupFilesWithDialog, isFileOpened, setWorking]); - const generateOutSegFileNames = useCallback(({ segments = segmentsToExport, template }) => ( - generateOutSegFileNamesRaw({ segments, template, formatTimecode, isCustomFormatSelected, fileFormat, filePath, outputDir, safeOutputFileName, maxLabelLength, outputFileNameMinZeroPadding }) - ), [fileFormat, filePath, formatTimecode, isCustomFormatSelected, maxLabelLength, outputDir, outputFileNameMinZeroPadding, safeOutputFileName, segmentsToExport]); + const generateOutSegFileNames = useCallback(({ segments = segmentsToExport, template }: { segments?: SegmentToExport[], template: string }) => { + if (fileFormat == null || outputDir == null || filePath == null) throw new Error(); + return generateOutSegFileNamesRaw({ segments, template, formatTimecode, isCustomFormatSelected, fileFormat, filePath, outputDir, safeOutputFileName, maxLabelLength, outputFileNameMinZeroPadding }); + }, [fileFormat, filePath, formatTimecode, isCustomFormatSelected, maxLabelLength, outputDir, outputFileNameMinZeroPadding, safeOutputFileName, segmentsToExport]); const closeExportConfirm = useCallback(() => setExportConfirmVisible(false), []); const willMerge = segmentsToExport.length > 1 && autoMerge; const mergedOutFilePath = useMemo(() => ( - getOutPath({ customOutDir, filePath, fileName: mergedOutFileName }) + mergedOutFileName != null ? getOutPath({ customOutDir, filePath, fileName: mergedOutFileName }) : undefined ), [customOutDir, filePath, mergedOutFileName]); const onExportConfirm = useCallback(async () => { @@ -1351,6 +1353,7 @@ function App() { try { const currentTime = getRelevantTime(); const video = videoRef.current; + if (video == null) throw new Error(); const useFffmpeg = usingPreviewFile || captureFrameMethod === 'ffmpeg'; const outPath = useFffmpeg ? await captureFrameFromFfmpeg({ customOutDir, filePath, fromTime: currentTime, captureFormat, quality: captureFrameQuality }) @@ -1376,10 +1379,10 @@ function App() { setCutProgress(0); - let lastOutPath; + let lastOutPath: string | undefined; let totalProgress = 0; - const onProgress = (progress) => { + const onProgress = (progress: number) => { totalProgress += progress; setCutProgress(totalProgress / segments.length); }; @@ -1387,10 +1390,11 @@ function App() { // eslint-disable-next-line no-restricted-syntax for (const segment of segments) { const { start, end } = segment; + if (filePath == null) throw new Error(); // eslint-disable-next-line no-await-in-loop lastOutPath = await captureFramesRange({ customOutDir, filePath, fps: detectedFps, fromTime: start, toTime: end, estimatedMaxNumFiles: captureFramesResponse.estimatedMaxNumFiles, captureFormat, quality: captureFrameQuality, filter: captureFramesResponse.filter, outputTimestamps: captureFrameFileNameFormat === 'timestamp', onProgress }); } - if (!hideAllNotifications) openDirToast({ icon: 'success', filePath: lastOutPath, text: i18n.t('Frames extracted to: {{path}}', { path: outputDir }) }); + if (!hideAllNotifications && lastOutPath != null) openDirToast({ icon: 'success', filePath: lastOutPath, text: i18n.t('Frames extracted to: {{path}}', { path: outputDir }) }); } catch (err) { handleError(err); } finally { @@ -1693,8 +1697,8 @@ function App() { try { setWorking({ text: i18n.t('Extracting all streams') }); setStreamsSelectorShown(false); - const extractedPaths = await extractStreams({ customOutDir, filePath, streams: mainCopiedStreams, enableOverwriteOutput }); - if (!hideAllNotifications) openDirToast({ icon: 'success', filePath: extractedPaths[0], text: i18n.t('All streams have been extracted as separate files') }); + const [firstExtractedPath] = await extractStreams({ customOutDir, filePath, streams: mainCopiedStreams, enableOverwriteOutput }); + if (!hideAllNotifications && firstExtractedPath != null) openDirToast({ icon: 'success', filePath: firstExtractedPath, text: i18n.t('All streams have been extracted as separate files') }); } catch (err) { if (err instanceof RefuseOverwriteError) { showRefuseToOverwrite(); @@ -1982,8 +1986,9 @@ function App() { const showIncludeExternalStreamsDialog = useCallback(async () => { try { const { canceled, filePaths } = await showOpenDialog({ properties: ['openFile'] }); - if (canceled || filePaths.length === 0) return; - await addStreamSourceFile(filePaths[0]); + const [firstFilePath] = filePaths; + if (canceled || firstFilePath == null) return; + await addStreamSourceFile(firstFilePath); } catch (err) { handleError(err); } @@ -2008,7 +2013,9 @@ function App() { const onEditSegmentTags = useCallback((index: number) => { setEditingSegmentTagsSegmentIndex(index); - setEditingSegmentTags(getSegmentTags(apparentCutSegments[index])); + const seg = apparentCutSegments[index]; + if (seg == null) throw new Error(); + setEditingSegmentTags(getSegmentTags(seg)); }, [apparentCutSegments]); const editCurrentSegmentTags = useCallback(() => { @@ -2219,8 +2226,8 @@ function App() { try { setWorking({ text: i18n.t('Extracting track') }); // setStreamsSelectorShown(false); - const extractedPaths = await extractStreams({ customOutDir, filePath, streams: mainStreams.filter((s) => s.index === index), enableOverwriteOutput }); - if (!hideAllNotifications) openDirToast({ icon: 'success', filePath: extractedPaths[0], text: i18n.t('Track has been extracted') }); + const [firstExtractedPath] = await extractStreams({ customOutDir, filePath, streams: mainStreams.filter((s) => s.index === index), enableOverwriteOutput }); + if (!hideAllNotifications && firstExtractedPath != null) openDirToast({ icon: 'success', filePath: firstExtractedPath, text: i18n.t('Track has been extracted') }); } catch (err) { if (err instanceof RefuseOverwriteError) { showRefuseToOverwrite(); diff --git a/src/components/BatchFile.jsx b/src/components/BatchFile.tsx similarity index 85% rename from src/components/BatchFile.jsx rename to src/components/BatchFile.tsx index 4366ce4..0b951aa 100644 --- a/src/components/BatchFile.jsx +++ b/src/components/BatchFile.tsx @@ -5,8 +5,10 @@ import { FaAngleRight, FaFile } from 'react-icons/fa'; import useContextMenu from '../hooks/useContextMenu'; import { primaryTextColor } from '../colors'; -const BatchFile = memo(({ path, isOpen, isSelected, name, onSelect, onDelete }) => { - const ref = useRef(); +const BatchFile = memo(({ path, isOpen, isSelected, name, onSelect, onDelete }: { + path: string, isOpen: boolean, isSelected: boolean, name: string, onSelect: (a: string) => void, onDelete: (a: string) => void +}) => { + const ref = useRef(null); const { t } = useTranslation(); const contextMenuTemplate = useMemo(() => [ diff --git a/src/components/ConcatDialog.tsx b/src/components/ConcatDialog.tsx index c1fa633..cd2c87f 100644 --- a/src/components/ConcatDialog.tsx +++ b/src/components/ConcatDialog.tsx @@ -225,7 +225,7 @@ const ConcatDialog = memo(({ isShown, onHide, paths, onConcat, alwaysConcatMulti setPreserveMetadataOnMerge(e.target.checked)} label={t('Preserve original metadata when merging? (slow)')} /> - {isMov(fileFormat) && setPreserveMovData(e.target.checked)} label={t('Preserve all MP4/MOV metadata?')} />} + {fileFormat != null && isMov(fileFormat) && setPreserveMovData(e.target.checked)} label={t('Preserve all MP4/MOV metadata?')} />} setSegmentsToChapters(e.target.checked)} label={t('Create chapters from merged segments? (slow)')} /> diff --git a/src/components/HighlightedText.jsx b/src/components/HighlightedText.jsx deleted file mode 100644 index 8dafc79..0000000 --- a/src/components/HighlightedText.jsx +++ /dev/null @@ -1,10 +0,0 @@ -import { memo } from 'react'; - -import { primaryTextColor } from '../colors'; - -export const highlightedTextStyle = { textDecoration: 'underline', textUnderlineOffset: '.2em', textDecorationColor: primaryTextColor, color: 'var(--gray12)', borderRadius: '.4em' }; - -// eslint-disable-next-line react/jsx-props-no-spreading -const HighlightedText = memo(({ children, style, ...props }) => {children}); - -export default HighlightedText; diff --git a/src/components/HighlightedText.tsx b/src/components/HighlightedText.tsx new file mode 100644 index 0000000..b27363d --- /dev/null +++ b/src/components/HighlightedText.tsx @@ -0,0 +1,12 @@ +import { CSSProperties, HTMLAttributes, memo } from 'react'; + +import { primaryTextColor } from '../colors'; + +export const highlightedTextStyle: CSSProperties = { textDecoration: 'underline', textUnderlineOffset: '.2em', textDecorationColor: primaryTextColor, color: 'var(--gray12)', borderRadius: '.4em' }; + +function HighlightedText({ children, style, ...props }: HTMLAttributes) { + // eslint-disable-next-line react/jsx-props-no-spreading + return {children}; +} + +export default memo(HighlightedText); diff --git a/src/components/OutSegTemplateEditor.jsx b/src/components/OutSegTemplateEditor.tsx similarity index 78% rename from src/components/OutSegTemplateEditor.jsx rename to src/components/OutSegTemplateEditor.tsx index c1820e3..e2aed2d 100644 --- a/src/components/OutSegTemplateEditor.jsx +++ b/src/components/OutSegTemplateEditor.tsx @@ -9,11 +9,12 @@ import { motion, AnimatePresence } from 'framer-motion'; import Swal from '../swal'; import HighlightedText from './HighlightedText'; -import { defaultOutSegTemplate, segNumVariable, segSuffixVariable } from '../util/outputNameTemplate'; +import { defaultOutSegTemplate, segNumVariable, segSuffixVariable, generateOutSegFileNames as generateOutSegFileNamesRaw } from '../util/outputNameTemplate'; import useUserSettings from '../hooks/useUserSettings'; import Switch from './Switch'; import Select from './Select'; import TextInput from './TextInput'; +import { SegmentToExport } from '../types'; const ReactSwal = withReactContent(Swal); @@ -23,16 +24,18 @@ const formatVariable = (variable) => `\${${variable}}`; const extVar = formatVariable('EXT'); -const OutSegTemplateEditor = memo(({ outSegTemplate, setOutSegTemplate, generateOutSegFileNames, currentSegIndexSafe }) => { +const OutSegTemplateEditor = memo(({ outSegTemplate, setOutSegTemplate, generateOutSegFileNames, currentSegIndexSafe }: { + outSegTemplate: string, setOutSegTemplate: (text: string) => void, generateOutSegFileNames: (a: { segments?: SegmentToExport[], template: string }) => ReturnType, currentSegIndexSafe: number, +}) => { const { safeOutputFileName, toggleSafeOutputFileName, outputFileNameMinZeroPadding, setOutputFileNameMinZeroPadding } = useUserSettings(); const [text, setText] = useState(outSegTemplate); const [debouncedText] = useDebounce(text, 500); - const [validText, setValidText] = useState(); - const [outSegProblems, setOutSegProblems] = useState({ error: undefined, sameAsInputFileNameWarning: false }); - const [outSegFileNames, setOutSegFileNames] = useState(); - const [shown, setShown] = useState(); - const inputRef = useRef(); + const [validText, setValidText] = useState(); + const [outSegProblems, setOutSegProblems] = useState<{ error?: string, sameAsInputFileNameWarning?: boolean }>({ error: undefined, sameAsInputFileNameWarning: false }); + const [outSegFileNames, setOutSegFileNames] = useState(); + const [shown, setShown] = useState(); + const inputRef = useRef(null); const { t } = useTranslation(); @@ -48,21 +51,25 @@ const OutSegTemplateEditor = memo(({ outSegTemplate, setOutSegTemplate, generate setValidText(outSegs.outSegProblems.error == null ? debouncedText : undefined); } catch (err) { console.error(err); - setValidText(); - setOutSegProblems({ error: err.message }); + setValidText(undefined); + setOutSegProblems({ error: err instanceof Error ? err.message : String(err) }); } }, [debouncedText, generateOutSegFileNames, t]); // eslint-disable-next-line no-template-curly-in-string const isMissingExtension = validText != null && !validText.endsWith(extVar); - const onAllSegmentsPreviewPress = () => ReactSwal.fire({ - title: t('Resulting segment file names', { count: outSegFileNames.length }), - html: ( -
- {outSegFileNames.map((f) =>
{f}
)} -
- ) }); + const onAllSegmentsPreviewPress = useCallback(() => { + if (outSegFileNames == null) return; + ReactSwal.fire({ + title: t('Resulting segment file names', { count: outSegFileNames.length }), + html: ( +
+ {outSegFileNames.map((f) =>
{f}
)} +
+ ), + }); + }, [outSegFileNames, t]); useEffect(() => { if (validText != null) setOutSegTemplate(validText); @@ -83,12 +90,14 @@ const OutSegTemplateEditor = memo(({ outSegTemplate, setOutSegTemplate, generate const onTextChange = useCallback((e) => setText(e.target.value), []); - const needToShow = shown || outSegProblems.error != null || outSegProblems.sameAsInputFileNameWarning; + const gotImportantMessage = outSegProblems.error != null || outSegProblems.sameAsInputFileNameWarning; + const needToShow = shown || gotImportantMessage; const onVariableClick = useCallback((variable) => { const input = inputRef.current; - const startPos = input.selectionStart; - const endPos = input.selectionEnd; + const startPos = input!.selectionStart; + const endPos = input!.selectionEnd; + if (startPos == null || endPos == null) return; const newValue = `${text.slice(0, startPos)}${`${formatVariable(variable)}${text.slice(endPos)}`}`; setText(newValue); @@ -114,7 +123,7 @@ const OutSegTemplateEditor = memo(({ outSegTemplate, setOutSegTemplate, generate {outSegFileNames != null && } - + {!gotImportantMessage && }
@@ -149,7 +158,7 @@ const OutSegTemplateEditor = memo(({ outSegTemplate, setOutSegTemplate, generate {hasTextNumericPaddedValue && (
Minimum numeric padded length
diff --git a/src/components/TextInput.jsx b/src/components/TextInput.jsx deleted file mode 100644 index a9eeda5..0000000 --- a/src/components/TextInput.jsx +++ /dev/null @@ -1,10 +0,0 @@ -import { forwardRef } from 'react'; - -const inputStyle = { borderRadius: '.4em', flexGrow: 1, fontFamily: 'inherit', fontSize: '.8em', backgroundColor: 'var(--gray3)', color: 'var(--gray12)', border: '1px solid var(--gray7)', appearance: 'none' }; - -const TextInput = forwardRef(({ style, ...props }, forwardedRef) => ( - // eslint-disable-next-line react/jsx-props-no-spreading - -)); - -export default TextInput; diff --git a/src/components/TextInput.tsx b/src/components/TextInput.tsx new file mode 100644 index 0000000..46f495d --- /dev/null +++ b/src/components/TextInput.tsx @@ -0,0 +1,10 @@ +import { CSSProperties, forwardRef } from 'react'; + +const inputStyle: CSSProperties = { borderRadius: '.4em', flexGrow: 1, fontFamily: 'inherit', fontSize: '.8em', backgroundColor: 'var(--gray3)', color: 'var(--gray12)', border: '1px solid var(--gray7)', appearance: 'none' }; + +const TextInput = forwardRef(({ style, ...props }, forwardedRef) => ( + // eslint-disable-next-line react/jsx-props-no-spreading + +)); + +export default TextInput; diff --git a/src/edlStore.ts b/src/edlStore.ts index bbe3b82..df5f438 100644 --- a/src/edlStore.ts +++ b/src/edlStore.ts @@ -115,8 +115,9 @@ export async function askForEdlImport({ type, fps }: { type: EdlImportType, fps? else if (type === 'llc') filters = [{ name: i18n.t('LosslessCut project'), extensions: ['llc'] }]; const { canceled, filePaths } = await showOpenDialog({ properties: ['openFile'], filters }); - if (canceled || filePaths.length === 0) return []; - return readEdlFile({ type, path: filePaths[0], fps }); + const [firstFilePath] = filePaths; + if (canceled || firstFilePath == null) return []; + return readEdlFile({ type, path: firstFilePath, fps }); } export async function exportEdlFile({ type, cutSegments, customOutDir, filePath, getFrameCount }: { diff --git a/src/ffmpeg.ts b/src/ffmpeg.ts index a71ae02..1dacffc 100644 --- a/src/ffmpeg.ts +++ b/src/ffmpeg.ts @@ -432,6 +432,7 @@ async function extractAttachmentStreams({ customOutDir, filePath, streams, enabl const outPaths = await pMap(streams, async ({ index, codec_name: codec, codec_type: type }) => { const ext = codec || 'bin'; const outPath = getSuffixedOutPath({ customOutDir, filePath, nameSuffix: `stream-${index}-${type}-${codec}.${ext}` }); + if (outPath == null) throw new Error(); if (!enableOverwriteOutput && await pathExists(outPath)) throw new RefuseOverwriteError(); streamArgs = [ diff --git a/src/hooks/useContextMenu.js b/src/hooks/useContextMenu.ts similarity index 61% rename from src/hooks/useContextMenu.js rename to src/hooks/useContextMenu.ts index 1f8b163..968f535 100644 --- a/src/hooks/useContextMenu.js +++ b/src/hooks/useContextMenu.ts @@ -1,14 +1,14 @@ -import { useEffect } from 'react'; +import { RefObject, useEffect } from 'react'; +import type { MenuItem, MenuItemConstructorOptions } from 'electron'; import useNativeMenu from './useNativeMenu'; // https://github.com/transflow/use-electron-context-menu export default function useContextMenu( - ref, - template, - options = {}, + ref: RefObject, + template: (MenuItemConstructorOptions | MenuItem)[], ) { - const { openMenu, closeMenu } = useNativeMenu(template, options); + const { openMenu, closeMenu } = useNativeMenu(template); useEffect(() => { const el = ref.current; diff --git a/src/hooks/useFfmpegOperations.js b/src/hooks/useFfmpegOperations.ts similarity index 97% rename from src/hooks/useFfmpegOperations.js rename to src/hooks/useFfmpegOperations.ts index d7452eb..72cbbbb 100644 --- a/src/hooks/useFfmpegOperations.js +++ b/src/hooks/useFfmpegOperations.ts @@ -13,7 +13,7 @@ const { join, resolve, dirname } = window.require('path'); const { pathExists } = window.require('fs-extra'); const { writeFile, mkdir } = window.require('fs/promises'); -async function writeChaptersFfmetadata(outDir, chapters) { +async function writeChaptersFfmetadata(outDir: string, chapters: { start: number, end: number, name?: string }[]) { if (!chapters || chapters.length === 0) return undefined; const path = join(outDir, `ffmetadata-${Date.now()}.txt`); @@ -26,8 +26,8 @@ async function writeChaptersFfmetadata(outDir, chapters) { return path; } -function getMovFlags({ preserveMovData, movFastStart }) { - const flags = []; +function getMovFlags({ preserveMovData, movFastStart }: { preserveMovData: boolean, movFastStart: boolean }) { + const flags: string[] = []; // https://video.stackexchange.com/a/26084/29486 // https://github.com/mifi/lossless-cut/issues/331#issuecomment-623401794 @@ -52,7 +52,7 @@ function getMatroskaFlags() { const getChaptersInputArgs = (ffmetadataPath) => (ffmetadataPath ? ['-f', 'ffmetadata', '-i', ffmetadataPath] : []); -async function tryDeleteFiles(paths) { +async function tryDeleteFiles(paths: string[]) { return pMap(paths, (path) => unlinkWithRetry(path).catch((err) => console.error('Failed to delete', path, err)), { concurrency: 5 }); } @@ -80,12 +80,12 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea } try { - let inputArgs = []; + let inputArgs: string[] = []; let inputIndex = 0; // Keep track of input index to be used later // eslint-disable-next-line no-inner-declarations - function addInput(args) { + function addInput(args: string[]) { inputArgs = [...inputArgs, ...args]; const retIndex = inputIndex; inputIndex += 1; @@ -245,10 +245,8 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea const mapStreamsArgs = getMapStreamsArgs({ copyFileStreams: copyFileStreamsFiltered, allFilesMeta, outFormat }); const customParamsArgs = (() => { - const ret = []; - // eslint-disable-next-line no-restricted-syntax + const ret: string[] = []; for (const [fileId, fileParams] of paramsByStreamId.entries()) { - // eslint-disable-next-line no-restricted-syntax for (const [streamId, streamParams] of fileParams.entries()) { const outputIndex = mapInputStreamIndexToOutputIndex(fileId, parseInt(streamId, 10)); if (outputIndex != null) { @@ -269,7 +267,6 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea // custom stream metadata tags const customTags = streamParams.get('customTags'); if (customTags != null) { - // eslint-disable-next-line no-restricted-syntax for (const [tag, value] of Object.entries(customTags)) { ret.push(`-metadata:s:${outputIndex}`, `${tag}=${value}`); } @@ -285,7 +282,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea // No progress if we set loglevel warning :( // '-loglevel', 'warning', - ...getOutputPlaybackRateArgs(outputPlaybackRate), + ...getOutputPlaybackRateArgs(), ...inputArgs, @@ -325,7 +322,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea logStdoutStderr(result); await transferTimestamps({ inPath: filePath, outPath, cutFrom, cutTo, treatInputFileModifiedTimeAsStart, duration: isDurationValid(videoDuration) ? videoDuration : undefined, treatOutputFileModifiedTimeAsStart }); - }, [cutFromAdjustmentFrames, filePath, getOutputPlaybackRateArgs, outputPlaybackRate, shouldSkipExistingFile, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart]); + }, [cutFromAdjustmentFrames, filePath, getOutputPlaybackRateArgs, shouldSkipExistingFile, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart]); const cutMultiple = useCallback(async ({ outputDir, customOutDir, segments, outSegFileNames, videoDuration, rotation, detectedFps, @@ -388,6 +385,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea // eslint-disable-next-line no-shadow async function cutEncodeSmartPartWrapper({ cutFrom, cutTo, outPath }) { if (await shouldSkipExistingFile(outPath)) return; + if (videoCodec == null || videoBitrate == null || videoTimebase == null) throw new Error(); await cutEncodeSmartPart({ filePath, cutFrom, cutTo, outPath, outFormat, videoCodec, videoBitrate, videoStreamIndex, videoTimebase, allFilesMeta, copyFileStreams: copyFileStreamsFiltered, ffmpegExperimental }); } diff --git a/src/hooks/useFrameCapture.js b/src/hooks/useFrameCapture.ts similarity index 77% rename from src/hooks/useFrameCapture.js rename to src/hooks/useFrameCapture.ts index 01005f1..f43e01e 100644 --- a/src/hooks/useFrameCapture.js +++ b/src/hooks/useFrameCapture.ts @@ -1,6 +1,7 @@ import dataUriToBuffer from 'data-uri-to-buffer'; import pMap from 'p-map'; import { useCallback } from 'react'; +import type * as FsPromises from 'fs/promises'; import { getSuffixedOutPath, getOutDir, transferTimestamps, getSuffixedFileName, getOutPath, escapeRegExp, fsOperationWithRetry } from '../util'; import { getNumDigits } from '../segments'; @@ -8,7 +9,7 @@ import { getNumDigits } from '../segments'; import { captureFrame as ffmpegCaptureFrame, captureFrames as ffmpegCaptureFrames } from '../ffmpeg'; const mime = window.require('mime-types'); -const { rename, readdir, writeFile } = window.require('fs/promises'); +const { rename, readdir, writeFile }: typeof FsPromises = window.require('fs/promises'); function getFrameFromVideo(video, format, quality) { @@ -16,7 +17,7 @@ function getFrameFromVideo(video, format, quality) { canvas.width = video.videoWidth; canvas.height = video.videoHeight; - canvas.getContext('2d').drawImage(video, 0, 0); + canvas.getContext('2d')!.drawImage(video, 0, 0); const dataUri = canvas.toDataURL(`image/${format}`, quality); @@ -24,8 +25,10 @@ function getFrameFromVideo(video, format, quality) { } export default ({ formatTimecode, treatOutputFileModifiedTimeAsStart }) => { - const captureFramesRange = useCallback(async ({ customOutDir, filePath, fps, fromTime, toTime, estimatedMaxNumFiles, captureFormat, quality, filter, onProgress, outputTimestamps }) => { - const getSuffix = (prefix) => `${prefix}.${captureFormat}`; + const captureFramesRange = useCallback(async ({ customOutDir, filePath, fps, fromTime, toTime, estimatedMaxNumFiles, captureFormat, quality, filter, onProgress, outputTimestamps }: { + customOutDir, filePath: string, fps: number, fromTime: number, toTime: number, estimatedMaxNumFiles: number, captureFormat: string, quality: number, filter?: string, onProgress: (a: number) => void, outputTimestamps: boolean + }) => { + const getSuffix = (prefix: string) => `${prefix}.${captureFormat}`; if (!outputTimestamps) { const numDigits = getNumDigits(estimatedMaxNumFiles); @@ -48,21 +51,21 @@ export default ({ formatTimecode, treatOutputFileModifiedTimeAsStart }) => { const files = await readdir(outDir); // https://github.com/mifi/lossless-cut/issues/1139 - const matches = files.map((fileName) => { + const matches = files.flatMap((fileName) => { const escapedRegexp = escapeRegExp(getSuffixedFileName(filePath, tmpSuffix)); const regexp = `^${escapedRegexp}(\\d+)`; const match = fileName.match(new RegExp(regexp)); - if (!match) return undefined; - const frameNum = parseInt(match[1], 10); - if (Number.isNaN(frameNum) || frameNum < 0) return undefined; - return { fileName, frameNum }; - }).filter((it) => it != null); + if (!match) return []; + const frameNum = parseInt(match[1]!, 10); + if (Number.isNaN(frameNum) || frameNum < 0) return []; + return [{ fileName, frameNum }]; + }); console.log('Renaming temp files...'); const outPaths = await pMap(matches, async ({ fileName, frameNum }) => { const duration = formatTimecode({ seconds: fromTime + (frameNum / fps), fileNameFriendly: true }); const renameFromPath = getOutPath({ customOutDir, filePath, fileName }); - const renameToPath = getOutPath({ customOutDir, filePath, fileName: getSuffixedFileName(filePath, getSuffix(duration, captureFormat)) }); + const renameToPath = getOutPath({ customOutDir, filePath, fileName: getSuffixedFileName(filePath, getSuffix(duration)) }); await fsOperationWithRetry(async () => rename(renameFromPath, renameToPath)); return renameToPath; }, { concurrency: 1 }); @@ -70,7 +73,9 @@ export default ({ formatTimecode, treatOutputFileModifiedTimeAsStart }) => { return outPaths[0]; }, [formatTimecode]); - const captureFrameFromFfmpeg = useCallback(async ({ customOutDir, filePath, fromTime, captureFormat, quality }) => { + const captureFrameFromFfmpeg = useCallback(async ({ customOutDir, filePath, fromTime, captureFormat, quality }: { + customOutDir?: string, filePath?: string, fromTime: number, captureFormat: string, quality: number, + }) => { const time = formatTimecode({ seconds: fromTime, fileNameFriendly: true }); const nameSuffix = `${time}.${captureFormat}`; const outPath = getSuffixedOutPath({ customOutDir, filePath, nameSuffix }); @@ -80,7 +85,9 @@ export default ({ formatTimecode, treatOutputFileModifiedTimeAsStart }) => { return outPath; }, [formatTimecode, treatOutputFileModifiedTimeAsStart]); - const captureFrameFromTag = useCallback(async ({ customOutDir, filePath, currentTime, captureFormat, video, quality }) => { + const captureFrameFromTag = useCallback(async ({ customOutDir, filePath, currentTime, captureFormat, video, quality }: { + customOutDir?: string, filePath?: string, currentTime: number, captureFormat: string, video: HTMLVideoElement, quality: number, + }) => { const buf = getFrameFromVideo(video, captureFormat, quality); const ext = mime.extension(buf.type); diff --git a/src/hooks/useNativeMenu.js b/src/hooks/useNativeMenu.ts similarity index 63% rename from src/hooks/useNativeMenu.js rename to src/hooks/useNativeMenu.ts index 3ae178d..7f0c827 100644 --- a/src/hooks/useNativeMenu.js +++ b/src/hooks/useNativeMenu.ts @@ -1,20 +1,22 @@ +import type { Menu as MenuType, MenuItemConstructorOptions, MenuItem } from 'electron'; import { useCallback, useMemo } from 'react'; // TODO pull out? const remote = window.require('@electron/remote'); -const { Menu } = remote; +// eslint-disable-next-line prefer-destructuring +const Menu: typeof MenuType = remote.Menu; // https://github.com/transflow/use-electron-context-menu // https://www.electronjs.org/docs/latest/api/menu-item export default function useNativeMenu( - template, - options = {}, + template: (MenuItemConstructorOptions | MenuItem)[], + options: { x?: number, y?: number, onContext?: (e: MouseEvent) => void, onClose?: () => void } = {}, ) { const menu = useMemo(() => Menu.buildFromTemplate(template), [template]); const { x, y, onContext, onClose } = options; - const openMenu = useCallback((e) => { + const openMenu = useCallback((e: MouseEvent) => { menu.popup({ window: remote.getCurrentWindow(), x, diff --git a/src/hooks/useSegments.ts b/src/hooks/useSegments.ts index 67948d7..7941366 100644 --- a/src/hooks/useSegments.ts +++ b/src/hooks/useSegments.ts @@ -21,7 +21,7 @@ const { blackDetect, silenceDetect } = remote.require('./ffmpeg'); export default ({ filePath, workingRef, setWorking, setCutProgress, videoStream, duration, getRelevantTime, maxLabelLength, checkFileOpened, invertCutSegments, segmentsToChaptersOnly }: { - filePath: string, workingRef: MutableRefObject, setWorking: (w: { text: string, abortController?: AbortController } | undefined) => void, setCutProgress: (a: number | undefined) => void, videoStream, duration?: number, getRelevantTime: () => number, maxLabelLength: number, checkFileOpened: () => boolean, invertCutSegments: boolean, segmentsToChaptersOnly: boolean, + filePath?: string, workingRef: MutableRefObject, setWorking: (w: { text: string, abortController?: AbortController } | undefined) => void, setCutProgress: (a: number | undefined) => void, videoStream, duration?: number, getRelevantTime: () => number, maxLabelLength: number, checkFileOpened: () => boolean, invertCutSegments: boolean, segmentsToChaptersOnly: boolean, }) => { // Segment related state const segCounterRef = useRef(0); @@ -266,6 +266,7 @@ export default ({ filePath, workingRef, setWorking, setCutProgress, videoStream, async function align(key) { const time = newSegment[key]; + if (filePath == null) throw new Error(); const keyframe = await findKeyframeNearTime({ filePath, streamIndex: videoStream.index, time, mode }); if (keyframe == null) throw new Error(`Cannot find any keyframe within 60 seconds of frame ${time}`); newSegment[key] = keyframe; diff --git a/src/index.tsx b/src/index.tsx index 02cfd12..77a8291 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,6 +3,7 @@ import { createRoot } from 'react-dom/client'; import { MotionConfig } from 'framer-motion'; import { enableMapSet } from 'immer'; import * as Electron from 'electron'; +import Remote from '@electron/remote'; import 'sweetalert2/dist/sweetalert2.css'; @@ -28,7 +29,10 @@ import './main.css'; declare global { interface Window { - require: (module: 'electron') => typeof Electron; + require: (module: T) => T extends '@electron/remote' ? typeof Remote : + T extends 'electron' ? typeof Electron : + // eslint-disable-next-line @typescript-eslint/no-explicit-any + any; } } diff --git a/src/segments.ts b/src/segments.ts index 453a1f7..502dff2 100644 --- a/src/segments.ts +++ b/src/segments.ts @@ -2,7 +2,7 @@ import { nanoid } from 'nanoid'; import sortBy from 'lodash/sortBy'; import minBy from 'lodash/minBy'; import maxBy from 'lodash/maxBy'; -import { ApparentSegmentBase, InverseSegment, PlaybackMode, SegmentBase } from './types'; +import { ApparentCutSegment, ApparentSegmentBase, InverseSegment, PlaybackMode, SegmentBase, SegmentTags } from './types'; export const isDurationValid = (duration?: number): duration is number => duration != null && Number.isFinite(duration) && duration > 0; @@ -50,7 +50,7 @@ export function findSegmentsAtCursor(apparentSegments, currentTime) { return indexes; } -export const getSegmentTags = (segment) => (segment.tags || {}); +export const getSegmentTags = (segment: { tags?: SegmentTags | undefined }) => (segment.tags || {}); export const sortSegments = (segments: T[]) => sortBy(segments, 'start'); @@ -129,7 +129,7 @@ export function hasAnySegmentOverlap(sortedSegments) { return overlappingGroups.length > 0; } -export function invertSegments(sortedCutSegments, includeFirstSegment: boolean, includeLastSegment: boolean, duration?: number) { +export function invertSegments(sortedCutSegments: ApparentCutSegment[], includeFirstSegment: boolean, includeLastSegment: boolean, duration?: number) { if (sortedCutSegments.length === 0) return undefined; if (hasAnySegmentOverlap(sortedCutSegments)) return undefined; @@ -137,7 +137,7 @@ export function invertSegments(sortedCutSegments, includeFirstSegment: boolean, const ret: InverseSegment[] = []; if (includeFirstSegment) { - const firstSeg = sortedCutSegments[0]; + const firstSeg = sortedCutSegments[0]!; if (firstSeg.start > 0) { ret.push({ start: 0, @@ -149,7 +149,7 @@ export function invertSegments(sortedCutSegments, includeFirstSegment: boolean, sortedCutSegments.forEach((cutSegment, i) => { if (i === 0) return; - const previousSeg = sortedCutSegments[i - 1]; + const previousSeg = sortedCutSegments[i - 1]!; const inverted: InverseSegment = { start: previousSeg.end, end: cutSegment.start, diff --git a/src/types.ts b/src/types.ts index 8d7ac0e..e26f84b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -9,21 +9,34 @@ export interface ApparentSegmentBase { } +export type SegmentTags = Record; + export interface StateSegment extends SegmentBase { name: string; segId: string; segColorIndex?: number | undefined; - tags?: Record | undefined; + tags?: SegmentTags | undefined; } export interface Segment extends SegmentBase { name?: string, } -export interface InverseSegment extends SegmentBase { +export interface ApparentCutSegment extends ApparentSegmentBase { + segId?: string | undefined, + tags?: SegmentTags | undefined; +} + +export interface InverseSegment extends ApparentSegmentBase { segId?: string, } +export interface SegmentToExport extends ApparentSegmentBase { + name?: string | undefined; + segId?: string | undefined; + tags?: SegmentTags | undefined; +} + export type PlaybackMode = 'loop-segment-start-end' | 'loop-segment' | 'play-segment-once' | 'loop-selected-segments'; export type Html5ifyMode = 'fastest' | 'fast-audio-remux' | 'fast-audio' | 'fast' | 'slow' | 'slow-audio' | 'slowest'; diff --git a/src/util.ts b/src/util.ts index 77c4124..16caf9d 100644 --- a/src/util.ts +++ b/src/util.ts @@ -3,17 +3,21 @@ import pMap from 'p-map'; import ky from 'ky'; import prettyBytes from 'pretty-bytes'; import sortBy from 'lodash/sortBy'; -import pRetry from 'p-retry'; +import pRetry, { Options } from 'p-retry'; import { ExecaError } from 'execa'; +import type * as FsPromises from 'fs/promises'; +import type * as Os from 'os'; +import type * as FsExtra from 'fs-extra'; +import type { PlatformPath } from 'path'; import isDev from './isDev'; import Swal, { toast } from './swal'; import { ffmpegExtractWindow } from './util/constants'; -const { dirname, parse: parsePath, join, extname, isAbsolute, resolve, basename } = window.require('path'); -const fsExtra = window.require('fs-extra'); -const { stat, lstat, readdir, utimes, unlink } = window.require('fs/promises'); -const os = window.require('os'); +const { dirname, parse: parsePath, join, extname, isAbsolute, resolve, basename }: PlatformPath = window.require('path'); +const fsExtra: typeof FsExtra = window.require('fs-extra'); +const { stat, lstat, readdir, utimes, unlink }: typeof FsPromises = window.require('fs/promises'); +const os: typeof Os = window.require('os'); const { ipcRenderer } = window.require('electron'); const remote = window.require('@electron/remote'); @@ -27,9 +31,10 @@ export function getFileDir(filePath?: string) { return filePath ? dirname(filePath) : undefined; } -export function getOutDir(customOutDir?: string, filePath?: string) { - if (customOutDir) return customOutDir; - if (filePath) return getFileDir(filePath); +export function getOutDir(customOutDir?: T1, filePath?: T2): T1 extends string ? string : T2 extends string ? string : undefined; +export function getOutDir(customOutDir?: string | undefined, filePath?: string | undefined) { + if (customOutDir != null) return customOutDir; + if (filePath != null) return getFileDir(filePath); return undefined; } @@ -39,15 +44,17 @@ function getFileBaseName(filePath?: string) { return parsed.name; } -export function getOutPath({ customOutDir, filePath, fileName }: { customOutDir?: string, filePath?: string, fileName?: string }) { - if (!filePath) return undefined; +export function getOutPath(a: { customOutDir?: string, filePath?: T, fileName: string }): T extends string ? string : undefined; +export function getOutPath({ customOutDir, filePath, fileName }: { customOutDir?: string, filePath?: string | undefined, fileName: string }) { + if (filePath == null) return undefined; return join(getOutDir(customOutDir, filePath), fileName); } export const getSuffixedFileName = (filePath: string | undefined, nameSuffix: string) => `${getFileBaseName(filePath)}-${nameSuffix}`; -export function getSuffixedOutPath({ customOutDir, filePath, nameSuffix }: { customOutDir?: string, filePath?: string, nameSuffix: string }) { - if (!filePath) return undefined; +export function getSuffixedOutPath(a: { customOutDir?: string, filePath?: T, nameSuffix: string }): T extends string ? string : undefined; +export function getSuffixedOutPath({ customOutDir, filePath, nameSuffix }: { customOutDir?: string, filePath?: string | undefined, nameSuffix: string }) { + if (filePath == null) return undefined; return getOutPath({ customOutDir, filePath, fileName: getSuffixedFileName(filePath, nameSuffix) }); } @@ -100,7 +107,7 @@ export async function dirExists(dirPath) { const testFailFsOperation = false; // Retry because sometimes write operations fail on windows due to the file being locked for various reasons (often anti-virus) #272 #1797 #1704 -export async function fsOperationWithRetry(operation, { signal, retries = 10, minTimeout = 100, maxTimeout = 2000, ...opts }) { +export async function fsOperationWithRetry(operation, { signal, retries = 10, minTimeout = 100, maxTimeout = 2000, ...opts }: Options & { retries?: number, minTimeout?: number, maxTimeout?: number } = {}) { return pRetry(async () => { if (testFailFsOperation && Math.random() > 0.3) throw Object.assign(new Error('test delete failure'), { code: 'EPERM' }); await operation(); @@ -116,10 +123,9 @@ export async function fsOperationWithRetry(operation, { signal, retries = 10, mi } // example error: index-18074aaf.js:166 Failed to delete C:\Users\USERNAME\Desktop\RC\New folder\2023-12-27 21-45-22 (GMT p5)-merged-1703933052361-00.01.04.915-00.01.07.424-seg1.mp4 Error: EPERM: operation not permitted, unlink 'C:\Users\USERNAME\Desktop\RC\New folder\2023-12-27 21-45-22 (GMT p5)-merged-1703933052361-00.01.04.915-00.01.07.424-seg1.mp4' -export const unlinkWithRetry = async (path, options) => fsOperationWithRetry(async () => unlink(path), { ...options, onFailedAttempt: (error) => console.warn('Retrying delete', path, error.attemptNumber) }); +export const unlinkWithRetry = async (path: string, options?: Options) => fsOperationWithRetry(async () => unlink(path), { ...options, onFailedAttempt: (error) => console.warn('Retrying delete', path, error.attemptNumber) }); // example error: index-18074aaf.js:160 Error: EPERM: operation not permitted, utime 'C:\Users\USERNAME\Desktop\RC\New folder\2023-12-27 21-45-22 (GMT p5)-merged-1703933052361-cut-merged-1703933070237.mp4' -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const utimesWithRetry = async (path: string, atime: number, mtime: number, options?: any) => fsOperationWithRetry(async () => utimes(path, atime, mtime), { ...options, onFailedAttempt: (error) => console.warn('Retrying utimes', path, error.attemptNumber) }); +export const utimesWithRetry = async (path: string, atime: number, mtime: number, options?: Options) => fsOperationWithRetry(async () => utimes(path, atime, mtime), { ...options, onFailedAttempt: (error) => console.warn('Retrying utimes', path, error.attemptNumber) }); export const getFrameDuration = (fps?: number) => 1 / (fps ?? 30); @@ -192,7 +198,7 @@ export const arch = os.arch(); export const isWindows = platform === 'win32'; export const isMac = platform === 'darwin'; -export function getExtensionForFormat(format) { +export function getExtensionForFormat(format: string) { const ext = { matroska: 'mkv', ipod: 'm4a', @@ -203,7 +209,9 @@ export function getExtensionForFormat(format) { return ext || format; } -export function getOutFileExtension({ isCustomFormatSelected, outFormat, filePath }) { +export function getOutFileExtension({ isCustomFormatSelected, outFormat, filePath }: { + isCustomFormatSelected?: boolean, outFormat: string, filePath: string, +}) { if (!isCustomFormatSelected) { const ext = extname(filePath); // QuickTime is quirky about the file extension of mov files (has to be .mov) @@ -232,11 +240,12 @@ export async function findExistingHtml5FriendlyFile(fp, cod) { const prefix = getSuffixedFileName(fp, html5ifiedPrefix); const outDir = getOutDir(cod, fp); + if (outDir == null) throw new Error(); const dirEntries = await readdir(outDir); const html5ifiedDirEntries = dirEntries.filter((entry) => entry.startsWith(prefix)); - let matches: { entry: string, suffix: string }[] = []; + let matches: { entry: string, suffix?: string }[] = []; suffixes.forEach((suffix) => { const entryWithSuffix = html5ifiedDirEntries.find((entry) => new RegExp(`${suffix}\\..*$`).test(entry.replace(prefix, ''))); if (entryWithSuffix) matches = [...matches, { entry: entryWithSuffix, suffix }]; @@ -325,6 +334,7 @@ export async function checkAppPath() { return; } const pathSeg = pathMatch[1]; + if (pathSeg == null) return; if (pathSeg.startsWith(`57275${mf}.${llc}_`)) return; // this will report the path and may return a msg const url = `https://losslesscut-analytics.mifi.no/${pathSeg.length}/${encodeURIComponent(btoa(pathSeg))}`; @@ -381,7 +391,7 @@ function setDocumentExtraTitle(extra) { document.title = extra != null ? `${baseTitle} - ${extra}` : baseTitle; } -export function setDocumentTitle({ filePath, working, cutProgress }: { filePath: string, working?: string, cutProgress?: number }) { +export function setDocumentTitle({ filePath, working, cutProgress }: { filePath?: string, working?: string, cutProgress?: number }) { const parts: string[] = []; if (filePath) parts.push(basename(filePath)); if (working) { diff --git a/src/util/outputNameTemplate.ts b/src/util/outputNameTemplate.ts index 1966784..3555170 100644 --- a/src/util/outputNameTemplate.ts +++ b/src/util/outputNameTemplate.ts @@ -1,22 +1,25 @@ import i18n from 'i18next'; import lodashTemplate from 'lodash/template'; +import { PlatformPath } from 'path'; import { isMac, isWindows, hasDuplicates, filenamify, getOutFileExtension } from '../util'; import isDev from '../isDev'; import { getSegmentTags, formatSegNum } from '../segments'; -import { Segment } from '../types'; +import { SegmentToExport } from '../types'; export const segNumVariable = 'SEG_NUM'; export const segSuffixVariable = 'SEG_SUFFIX'; -const { parse: parsePath, sep: pathSep, join: pathJoin, normalize: pathNormalize, basename } = window.require('path'); +const { parse: parsePath, sep: pathSep, join: pathJoin, normalize: pathNormalize, basename }: PlatformPath = window.require('path'); -function getOutSegProblems({ fileNames, filePath, outputDir, safeOutputFileName }) { - let error; + +function getOutSegProblems({ fileNames, filePath, outputDir, safeOutputFileName }: { + fileNames: string[], filePath: string, outputDir: string, safeOutputFileName: boolean +}) { + let error: string | undefined; let sameAsInputFileNameWarning = false; - // eslint-disable-next-line no-restricted-syntax for (const fileName of fileNames) { if (!filePath) { error = i18n.t('No file is loaded'); @@ -115,7 +118,7 @@ function interpolateSegmentFileName({ template, epochMs, inputFileNameWithoutExt } export function generateOutSegFileNames({ segments, template: desiredTemplate, formatTimecode, isCustomFormatSelected, fileFormat, filePath, outputDir, safeOutputFileName, maxLabelLength, outputFileNameMinZeroPadding }: { - segments: Segment[], template: string, formatTimecode: (a: { seconds?: number, shorten?: boolean, fileNameFriendly?: boolean }) => string, isCustomFormatSelected: boolean, fileFormat?: string, filePath: string, outputDir: string, safeOutputFileName: string, maxLabelLength: number, outputFileNameMinZeroPadding: number, + segments: SegmentToExport[], template: string, formatTimecode: (a: { seconds?: number, shorten?: boolean, fileNameFriendly?: boolean }) => string, isCustomFormatSelected: boolean, fileFormat: string, filePath: string, outputDir: string, safeOutputFileName: boolean, maxLabelLength: number, outputFileNameMinZeroPadding: number, }) { function generate({ template, forceSafeOutputFileName }) { const epochMs = Date.now(); @@ -126,7 +129,7 @@ export function generateOutSegFileNames({ segments, template: desiredTemplate, f // Fields that did not come from the source file's name must be sanitized, because they may contain characters that are not supported by the target operating/file system // however we disable this when the user has chosen to (safeOutputFileName === false) - const filenamifyOrNot = (fileName) => (safeOutputFileName || forceSafeOutputFileName ? filenamify(fileName) : fileName).slice(0, Math.max(0, maxLabelLength)); + const filenamifyOrNot = (fileName: string) => (safeOutputFileName || forceSafeOutputFileName ? filenamify(fileName) : fileName).slice(0, Math.max(0, maxLabelLength)); function getSegSuffix() { if (name) return `-${filenamifyOrNot(name)}`; diff --git a/src/util/streams.ts b/src/util/streams.ts index 6828d69..ac8906d 100644 --- a/src/util/streams.ts +++ b/src/util/streams.ts @@ -106,7 +106,7 @@ export function getActiveDisposition(disposition) { return existingActiveDispositionEntry[0]; // return the key } -export const isMov = (format) => ['ismv', 'ipod', 'mp4', 'mov'].includes(format); +export const isMov = (format: string) => ['ismv', 'ipod', 'mp4', 'mov'].includes(format); type GetVideoArgsFn = (a: { streamIndex: number, outputIndex: number }) => string[] | undefined; diff --git a/yarn.lock b/yarn.lock index e4d99cc..98c6342 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1809,6 +1809,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:18": + version: 18.19.21 + resolution: "@types/node@npm:18.19.21" + dependencies: + undici-types: "npm:~5.26.4" + checksum: 3a5c5841f294bc35b5b416a32764b5c0c2f22f4cef48cb7d2e3b4e068a52d5857e50da8e6e0685e743127c70344301c833849a3904ce3bd3f67448da5e85487a + languageName: node + linkType: hard + "@types/node@npm:^18.11.18": version: 18.17.6 resolution: "@types/node@npm:18.17.6" @@ -7699,6 +7708,7 @@ __metadata: "@tsconfig/vite-react": "npm:^3.0.0" "@types/eslint": "npm:^8" "@types/lodash": "npm:^4.14.202" + "@types/node": "npm:18" "@types/sortablejs": "npm:^1.15.0" "@typescript-eslint/eslint-plugin": "npm:^6.12.0" "@typescript-eslint/parser": "npm:^6.12.0" @@ -11748,6 +11758,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~5.26.4": + version: 5.26.5 + resolution: "undici-types@npm:5.26.5" + checksum: 0097779d94bc0fd26f0418b3a05472410408877279141ded2bd449167be1aed7ea5b76f756562cb3586a07f251b90799bab22d9019ceba49c037c76445f7cddd + languageName: node + linkType: hard + "unique-filename@npm:^2.0.0": version: 2.0.1 resolution: "unique-filename@npm:2.0.1"