diff --git a/ffprobe.ts b/ffprobe.ts index 392fc9a6..13cd2a8d 100644 --- a/ffprobe.ts +++ b/ffprobe.ts @@ -287,7 +287,7 @@ export type FFprobeStreamTags = { // https://github.com/mifi/lossless-cut/issues/1530 title?: string, -} +} & Record /** * An FFprobe response stream object @@ -557,7 +557,7 @@ export interface FFprobeChapter { /** * The "tags" field on an FFprobe response format object */ -export interface FFprobeFormatTags { +export type FFprobeFormatTags = { /** * Not clear, probably the media type brand, but not sure */ @@ -682,7 +682,7 @@ export interface FFprobeFormatTags { * The song's publisher (only present in audio files) */ PUBLISHER?: string -} +} & Record /** * An FFprobe response format object diff --git a/package.json b/package.json index 7b7ab145..2fb90dc0 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,8 @@ "@types/node": "20", "@types/react": "^18.2.66", "@types/react-dom": "^18.2.22", + "@types/react-syntax-highlighter": "^15.5.13", + "@types/smpte-timecode": "^1.2.5", "@types/sortablejs": "^1.15.0", "@types/yargs-parser": "^21.0.3", "@typescript-eslint/eslint-plugin": "^6.12.0", diff --git a/src/main/compatPlayer.ts b/src/main/compatPlayer.ts index bc0c050e..16a3490e 100644 --- a/src/main/compatPlayer.ts +++ b/src/main/compatPlayer.ts @@ -23,7 +23,7 @@ export function createMediaSourceStream({ path, videoStreamIndex, audioStreamInd stdout.pause(); - const readChunk = async () => new Promise((resolve, reject) => { + const readChunk = async () => new Promise((resolve, reject) => { let cleanup: () => void; const onClose = () => { diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 4db0851e..d50b5f12 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -88,7 +88,7 @@ import { rightBarWidth, leftBarWidth, ffmpegExtractWindow, zoomMax } from './uti import BigWaveform from './components/BigWaveform'; import isDev from './isDev'; -import { Chapter, CustomTagsByFile, EdlExportType, EdlFileType, EdlImportType, FfmpegCommandLog, FilesMeta, goToTimecodeDirectArgsSchema, openFilesActionArgsSchema, ParamsByStreamId, PlaybackMode, SegmentColorIndex, SegmentTags, SegmentToExport, StateSegment, TunerType } from './types'; +import { BatchFile, Chapter, CustomTagsByFile, EdlExportType, EdlFileType, EdlImportType, FfmpegCommandLog, FilesMeta, goToTimecodeDirectArgsSchema, openFilesActionArgsSchema, ParamsByStreamId, PlaybackMode, SegmentColorIndex, SegmentTags, SegmentToExport, StateSegment, TunerType } from './types'; import { CaptureFormat, KeyboardAction, Html5ifyMode, WaveformMode, ApiActionRequest } from '../../../types'; import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../../ffprobe'; import useLoading from './hooks/useLoading'; @@ -168,7 +168,7 @@ function App() { const incrementMediaSourceQuality = useCallback(() => setMediaSourceQuality((v) => (v + 1) % mediaSourceQualities.length), []); // Batch state / concat files - const [batchFiles, setBatchFiles] = useState<{ path: string }[]>([]); + const [batchFiles, setBatchFiles] = useState([]); const [selectedBatchFiles, setSelectedBatchFiles] = useState([]); const allUserSettings = useUserSettingsRoot(); diff --git a/src/renderer/src/ErrorBoundary.tsx b/src/renderer/src/ErrorBoundary.tsx index d2fdf94a..badd5a08 100644 --- a/src/renderer/src/ErrorBoundary.tsx +++ b/src/renderer/src/ErrorBoundary.tsx @@ -13,7 +13,7 @@ class ErrorBoundary extends Component<{ children: ReactNode }> { this.state = { error: undefined }; } - static getDerivedStateFromError(error) { + static getDerivedStateFromError(error: unknown) { return { error }; } diff --git a/src/renderer/src/MediaSourcePlayer.tsx b/src/renderer/src/MediaSourcePlayer.tsx index 91b2020c..2262168f 100644 --- a/src/renderer/src/MediaSourcePlayer.tsx +++ b/src/renderer/src/MediaSourcePlayer.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState, useCallback, useMemo, memo, CSSProperties, RefObject } from 'react'; +import { useEffect, useRef, useState, useCallback, useMemo, memo, CSSProperties, RefObject, ReactEventHandler, FocusEventHandler } from 'react'; import { Spinner } from 'evergreen-ui'; import { useDebounce } from 'use-debounce'; @@ -25,7 +25,7 @@ async function startPlayback({ path, video, videoStreamIndex, audioStreamIndex, let canPlay = false; let bufferEndTime: number | undefined; let bufferStartTime = 0; - let stream; + let stream: ReturnType | undefined; let done = false; let interval: NodeJS.Timeout | undefined; let objectUrl: string | undefined; @@ -48,7 +48,7 @@ async function startPlayback({ path, video, videoStreamIndex, audioStreamIndex, const mediaSource = new MediaSource(); - let streamTimestamp; + let streamTimestamp: number | undefined; let lastRemoveTimestamp = 0; function setStandardPlaybackRate() { @@ -111,7 +111,7 @@ async function startPlayback({ path, video, videoStreamIndex, audioStreamIndex, const processChunk = async () => { try { - const chunk = await stream.readChunk(); + const chunk = await stream!.readChunk(); if (chunk == null) { console.log('End of stream'); return; @@ -264,7 +264,7 @@ function MediaSourcePlayer({ rotate, filePath, playerTime, videoStream, audioStr const canvasRef = useRef(null); const [loading, setLoading] = useState(true); - const onVideoError = useCallback((error) => { + const onVideoError = useCallback>((error) => { console.error('video error', error); }, []); @@ -348,7 +348,7 @@ function MediaSourcePlayer({ rotate, filePath, playerTime, videoStream, audioStr if (videoRef.current) videoRef.current.volume = playbackVolume; }, [playbackVolume]); - const onFocus = useCallback((e) => { + const onFocus = useCallback>((e) => { // prevent video element from stealing focus in fullscreen mode https://github.com/mifi/lossless-cut/issues/543#issuecomment-1868167775 e.target.blur(); }, []); diff --git a/src/renderer/src/SegmentList.tsx b/src/renderer/src/SegmentList.tsx index 87285350..089c066f 100644 --- a/src/renderer/src/SegmentList.tsx +++ b/src/renderer/src/SegmentList.tsx @@ -1,4 +1,4 @@ -import { memo, useMemo, useRef, useCallback, useState, SetStateAction, Dispatch, ReactNode } from 'react'; +import { memo, useMemo, useRef, useCallback, useState, SetStateAction, Dispatch, ReactNode, MouseEventHandler } from 'react'; import { FaYinYang, FaSave, FaPlus, FaMinus, FaTag, FaSortNumericDown, FaAngleRight, FaRegCheckCircle, FaRegCircle } from 'react-icons/fa'; import { AiOutlineSplitCells } from 'react-icons/ai'; import { MotionStyle, motion } from 'framer-motion'; @@ -168,7 +168,7 @@ const Segment = memo(({ const CheckIcon = selected ? FaRegCheckCircle : FaRegCircle; - const onToggleSegmentSelectedClick = useCallback((e) => { + const onToggleSegmentSelectedClick = useCallback((e) => { e.stopPropagation(); onToggleSegmentSelected(seg); }, [onToggleSegmentSelected, seg]); @@ -310,7 +310,7 @@ function SegmentList({ const sortableList = useMemo(() => segments.map((seg) => ({ id: seg.segId, seg })), [segments]); - const setSortableList = useCallback((newList) => { + const setSortableList = useCallback((newList: typeof sortableList) => { if (isEqual(segments.map((s) => s.segId), newList.map((l) => l.id))) return; // No change updateSegOrders(newList.map((list) => list.id)); }, [segments, updateSegOrders]); diff --git a/src/renderer/src/components/BatchFilesList.tsx b/src/renderer/src/components/BatchFilesList.tsx index 1642112e..caf719a3 100644 --- a/src/renderer/src/components/BatchFilesList.tsx +++ b/src/renderer/src/components/BatchFilesList.tsx @@ -9,6 +9,7 @@ import { SortAlphabeticalIcon, SortAlphabeticalDescIcon } from 'evergreen-ui'; import BatchFile from './BatchFile'; import { controlsBackground, darkModeTransition } from '../colors'; import { mySpring } from '../animations'; +import { BatchFile as BatchFileType } from '../types'; const iconStyle = { @@ -20,14 +21,25 @@ const iconStyle = { padding: '3px 5px', }; -function BatchFilesList({ selectedBatchFiles, filePath, width, batchFiles, setBatchFiles, onBatchFileSelect, batchListRemoveFile, closeBatch, onMergeFilesClick, onBatchConvertToSupportedFormatClick }) { +function BatchFilesList({ selectedBatchFiles, filePath, width, batchFiles, setBatchFiles, onBatchFileSelect, batchListRemoveFile, closeBatch, onMergeFilesClick, onBatchConvertToSupportedFormatClick }: { + selectedBatchFiles: string[], + filePath: string | undefined, + width: number, + batchFiles: BatchFileType[], + setBatchFiles: (f: BatchFileType[]) => void, + onBatchFileSelect: (f: string) => void, + batchListRemoveFile: (path: string | undefined) => void, + closeBatch: () => void, + onMergeFilesClick: () => void, + onBatchConvertToSupportedFormatClick: () => void, +}) { const { t } = useTranslation(); const [sortDesc, setSortDesc] = useState(); const sortableList = batchFiles.map((batchFile) => ({ id: batchFile.path, batchFile })); - const setSortableList = useCallback((newList) => { + const setSortableList = useCallback((newList: { batchFile: BatchFileType }[]) => { setBatchFiles(newList.map(({ batchFile }) => batchFile)); }, [setBatchFiles]); diff --git a/src/renderer/src/components/BigWaveform.tsx b/src/renderer/src/components/BigWaveform.tsx index f77e6e41..9b1e77bf 100644 --- a/src/renderer/src/components/BigWaveform.tsx +++ b/src/renderer/src/components/BigWaveform.tsx @@ -25,13 +25,13 @@ function BigWaveform({ waveforms, relevantTime, playing, durationSafe, zoom, see const smoothTime = smoothTimeRaw ?? relevantTime; - const mouseDownRef = useRef<{ relevantTime: number, x }>(); + const mouseDownRef = useRef<{ relevantTime: number, x: number }>(); const containerRef = useRef(null); const getRect = useCallback(() => containerRef.current!.getBoundingClientRect(), []); - const handleMouseDown = useCallback((e) => { - const rect = e.target.getBoundingClientRect(); + const handleMouseDown = useCallback>((e) => { + const rect = (e.target as HTMLDivElement).getBoundingClientRect(); const x = e.clientX - rect.left; mouseDownRef.current = { relevantTime, x }; diff --git a/src/renderer/src/components/ConcatDialog.tsx b/src/renderer/src/components/ConcatDialog.tsx index 51fcb7b7..0ba2d7a8 100644 --- a/src/renderer/src/components/ConcatDialog.tsx +++ b/src/renderer/src/components/ConcatDialog.tsx @@ -142,7 +142,7 @@ function ConcatDialog({ isShown, onHide, paths, onConcat, alwaysConcatMultipleFi return; } // check all these parameters - ['codec_name', 'width', 'height', 'fps', 'pix_fmt', 'level', 'profile', 'sample_fmt', 'r_frame_rate', 'time_base'].forEach((key) => { + (['codec_name', 'width', 'height', 'pix_fmt', 'level', 'profile', 'sample_fmt', 'avg_frame_rate', 'r_frame_rate', 'time_base'] as const).forEach((key) => { const val = stream[key]; const referenceVal = referenceStream[key]; if (val !== referenceVal) { @@ -187,7 +187,7 @@ function ConcatDialog({ isShown, onHide, paths, onConcat, alwaysConcatMultipleFi }; }, [allFilesMetaCache, enableReadFileMeta, isShown, paths]); - const onOutputFormatUserChange = useCallback((newFormat) => setFileFormat(newFormat), [setFileFormat]); + const onOutputFormatUserChange = useCallback((newFormat: string) => setFileFormat(newFormat), [setFileFormat]); const onConcatClick = useCallback(() => { if (outFileName == null) throw new Error(); diff --git a/src/renderer/src/components/KeyboardShortcuts.tsx b/src/renderer/src/components/KeyboardShortcuts.tsx index 083f05ec..22fcf5e9 100644 --- a/src/renderer/src/components/KeyboardShortcuts.tsx +++ b/src/renderer/src/components/KeyboardShortcuts.tsx @@ -698,7 +698,7 @@ const KeyboardShortcuts = memo(({ const categoriesWithActions = useMemo(() => Object.entries(groupBy(actionEntries, ([, { category }]) => category)), [actionEntries]); - const onDeleteBindingClick = useCallback(({ action, keys }) => { + const onDeleteBindingClick = useCallback(({ action, keys }: { action: KeyboardAction, keys: string }) => { // eslint-disable-next-line no-alert if (!window.confirm(t('Are you sure?'))) return; diff --git a/src/renderer/src/components/OutputFormatSelect.tsx b/src/renderer/src/components/OutputFormatSelect.tsx index 7b1464c0..6b1501dc 100644 --- a/src/renderer/src/components/OutputFormatSelect.tsx +++ b/src/renderer/src/components/OutputFormatSelect.tsx @@ -11,7 +11,7 @@ const commonSubtitleFormats = ['ass', 'srt', 'sup', 'webvtt']; function renderFormatOptions(formats: string[]) { return formats.map((format) => ( - + )); } @@ -32,7 +32,7 @@ function OutputFormatSelect({ style, detectedFileFormat, fileFormat, onOutputFor {detectedFileFormat && ( )} diff --git a/src/renderer/src/components/SegmentCutpointButton.tsx b/src/renderer/src/components/SegmentCutpointButton.tsx index 6d642738..87afdebd 100644 --- a/src/renderer/src/components/SegmentCutpointButton.tsx +++ b/src/renderer/src/components/SegmentCutpointButton.tsx @@ -1,11 +1,17 @@ import { CSSProperties, useMemo } from 'react'; +import { FaStepForward } from 'react-icons/fa'; import { useSegColors } from '../contexts'; import useUserSettings from '../hooks/useUserSettings'; import { SegmentBase, SegmentColorIndex } from '../types'; const SegmentCutpointButton = ({ currentCutSeg, side, Icon, onClick, title, style }: { - currentCutSeg: SegmentBase & SegmentColorIndex, side: 'start' | 'end', Icon, onClick?: (() => void) | undefined, title?: string | undefined, style?: CSSProperties | undefined + currentCutSeg: SegmentBase & SegmentColorIndex, + side: 'start' | 'end', + Icon: typeof FaStepForward, + onClick?: (() => void) | undefined, + title?: string | undefined, + style?: CSSProperties | undefined, }) => { const { darkMode } = useUserSettings(); const { getSegColor } = useSegColors(); @@ -18,9 +24,9 @@ const SegmentCutpointButton = ({ currentCutSeg, side, Icon, onClick, title, styl return ( ); diff --git a/src/renderer/src/components/SetCutpointButton.tsx b/src/renderer/src/components/SetCutpointButton.tsx index cacfb206..5343dc82 100644 --- a/src/renderer/src/components/SetCutpointButton.tsx +++ b/src/renderer/src/components/SetCutpointButton.tsx @@ -7,7 +7,11 @@ import { SegmentBase, SegmentColorIndex } from '../types'; // constant side because we are mirroring const SetCutpointButton = ({ currentCutSeg, side, title, onClick, style }: { - currentCutSeg: SegmentBase & SegmentColorIndex, side: 'start' | 'end', title?: string, onClick?: () => void, style?: CSSProperties + currentCutSeg: SegmentBase & SegmentColorIndex, + side: 'start' | 'end', + title?: string, + onClick?: () => void, + style?: CSSProperties, }) => ( ); diff --git a/src/renderer/src/components/Settings.tsx b/src/renderer/src/components/Settings.tsx index 6354211d..2cac009e 100644 --- a/src/renderer/src/components/Settings.tsx +++ b/src/renderer/src/components/Settings.tsx @@ -122,7 +122,7 @@ function Settings({ diff --git a/src/renderer/src/components/TagEditor.tsx b/src/renderer/src/components/TagEditor.tsx index baa6d55a..28ebc41f 100644 --- a/src/renderer/src/components/TagEditor.tsx +++ b/src/renderer/src/components/TagEditor.tsx @@ -1,4 +1,4 @@ -import { memo, useRef, useState, useMemo, useCallback, useEffect } from 'react'; +import { memo, useRef, useState, useMemo, useCallback, useEffect, FormEventHandler, MouseEventHandler } from 'react'; import { useTranslation } from 'react-i18next'; import { TextInput, TrashIcon, ResetIcon, TickIcon, EditIcon, PlusIcon, Button, IconButton } from 'evergreen-ui'; import invariant from 'tiny-invariant'; @@ -68,14 +68,14 @@ function TagEditor({ existingTags = emptyObject, customTags = emptyObject, editi } }, [editingTag, editingTagVal, existingTags, mergedTags, newTag, onResetClick, saveTag, setEditingTag]); - function onSubmit(e) { + const onSubmit = useCallback>((e) => { e.preventDefault(); onEditClick(); - } + }, [onEditClick]); - const onAddPress = useCallback(async (e) => { + const onAddPress = useCallback>(async (e) => { e.preventDefault(); - e.target.blur(); + (e.target as HTMLButtonElement).blur(); if (newTag || editingTag != null) { // save any unsaved edit diff --git a/src/renderer/src/components/ValueTuner.tsx b/src/renderer/src/components/ValueTuner.tsx index 4b9ffc45..19050b9a 100644 --- a/src/renderer/src/components/ValueTuner.tsx +++ b/src/renderer/src/components/ValueTuner.tsx @@ -1,4 +1,4 @@ -import { memo, useState, useCallback, CSSProperties } from 'react'; +import { memo, useState, useCallback, CSSProperties, ChangeEventHandler } from 'react'; import { Button } from 'evergreen-ui'; import { useTranslation } from 'react-i18next'; @@ -6,17 +6,25 @@ import Switch from './Switch'; function ValueTuner({ style, title, value, setValue, onFinished, resolution = 1000, min: minIn = 0, max: maxIn = 1, resetToDefault }: { - style?: CSSProperties, title: string, value: number, setValue: (string) => void, onFinished: () => void, resolution?: number, min?: number, max?: number, resetToDefault: () => void + style?: CSSProperties, + title: string, + value: number, + setValue: (v: number) => void, + onFinished: () => void, + resolution?: number, + min?: number, + max?: number, + resetToDefault: () => void, }) { const { t } = useTranslation(); const [min, setMin] = useState(minIn); const [max, setMax] = useState(maxIn); - function onChange(e) { + const onChange = useCallback>((e) => { e.target.blur(); - setValue(Math.min(Math.max(min, ((e.target.value / resolution) * (max - min)) + min), max)); - } + setValue(Math.min(Math.max(min, ((Number(e.target.value) / resolution) * (max - min)) + min), max)); + }, [max, min, resolution, setValue]); const isZoomed = !(min === minIn && max === maxIn); diff --git a/src/renderer/src/components/VolumeControl.tsx b/src/renderer/src/components/VolumeControl.tsx index 77658981..ed673fec 100644 --- a/src/renderer/src/components/VolumeControl.tsx +++ b/src/renderer/src/components/VolumeControl.tsx @@ -1,9 +1,13 @@ -import { memo, useState, useCallback, useRef, useEffect } from 'react'; +import { memo, useState, useCallback, useRef, useEffect, ChangeEventHandler } from 'react'; import { FaVolumeMute, FaVolumeUp } from 'react-icons/fa'; import { useTranslation } from 'react-i18next'; -function VolumeControl({ playbackVolume, setPlaybackVolume, onToggleMutedClick }: { playbackVolume: number, setPlaybackVolume: (a: number) => void, onToggleMutedClick: () => void }) { +function VolumeControl({ playbackVolume, setPlaybackVolume, onToggleMutedClick }: { + playbackVolume: number, + setPlaybackVolume: (a: number) => void, + onToggleMutedClick: () => void, +}) { const [volumeControlVisible, setVolumeControlVisible] = useState(false); const timeoutRef = useRef(); const { t } = useTranslation(); @@ -15,9 +19,9 @@ function VolumeControl({ playbackVolume, setPlaybackVolume, onToggleMutedClick } return () => clear(); }, [playbackVolume, volumeControlVisible]); - const onVolumeChange = useCallback((e) => { + const onVolumeChange = useCallback>((e) => { e.target.blur(); - setPlaybackVolume(e.target.value / 100); + setPlaybackVolume(Number(e.target.value) / 100); }, [setPlaybackVolume]); const onVolumeIconClick = useCallback(() => { diff --git a/src/renderer/src/dialogs/html5ify.tsx b/src/renderer/src/dialogs/html5ify.tsx index 19e79def..35deac44 100644 --- a/src/renderer/src/dialogs/html5ify.tsx +++ b/src/renderer/src/dialogs/html5ify.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback } from 'react'; +import { useState, useCallback, ChangeEventHandler } from 'react'; import i18n from 'i18next'; import { ReactSwal } from '../swal'; @@ -8,7 +8,9 @@ import Checkbox from '../components/Checkbox'; // eslint-disable-next-line import/prefer-default-export export async function askForHtml5ifySpeed({ allowedOptions, showRemember, initialOption }: { - allowedOptions: Html5ifyMode[], showRemember?: boolean | undefined, initialOption?: Html5ifyMode | undefined + allowedOptions: Html5ifyMode[], + showRemember?: boolean | undefined, + initialOption?: Html5ifyMode | undefined, }) { const availOptions: Record = { fastest: i18n.t('Fastest: FFmpeg-assisted playback'), @@ -31,12 +33,12 @@ export async function askForHtml5ifySpeed({ allowedOptions, showRemember, initia const [option, setOption] = useState(selectedOption); const [remember, setRemember] = useState(rememberChoice); - const onOptionChange = useCallback((e) => { - selectedOption = e.currentTarget.value; + const onOptionChange = useCallback>((e) => { + selectedOption = e.currentTarget.value as Html5ifyMode; setOption(selectedOption); }, []); - const onRememberChange = useCallback((checked) => { + const onRememberChange = useCallback((checked: boolean) => { rememberChoice = checked; setRemember(rememberChoice); }, []); diff --git a/src/renderer/src/dialogs/index.tsx b/src/renderer/src/dialogs/index.tsx index 3d07b5d8..8040fd31 100644 --- a/src/renderer/src/dialogs/index.tsx +++ b/src/renderer/src/dialogs/index.tsx @@ -6,6 +6,7 @@ import SyntaxHighlighter from 'react-syntax-highlighter'; import { tomorrow as lightSyntaxStyle, tomorrowNight as darkSyntaxStyle } from 'react-syntax-highlighter/dist/esm/styles/hljs'; import JSON5 from 'json5'; import type { SweetAlertOptions } from 'sweetalert2'; +import invariant from 'tiny-invariant'; import { formatDuration } from '../util/duration'; import Swal, { ReactSwal, swalToastOptions, toast } from '../swal'; @@ -221,7 +222,7 @@ async function askForNumSegments() { return parseInt(value, 10); } -export async function createNumSegments(fileDuration) { +export async function createNumSegments(fileDuration: number) { const numSegments = await askForNumSegments(); if (numSegments == null) return undefined; const edl: SegmentBase[] = []; @@ -259,17 +260,17 @@ async function askForSegmentDuration({ fileDuration, inputPlaceholder, parseTime // https://github.com/mifi/lossless-cut/issues/1153 async function askForSegmentsRandomDurationRange() { - function parse(str) { + function parse(str: string) { // eslint-disable-next-line unicorn/better-regex const match = str.replaceAll(/\s/g, '').match(/^duration([\d.]+)to([\d.]+),gap([-\d.]+)to([-\d.]+)$/i); if (!match) return undefined; const values = match.slice(1); const parsed = values.map((val) => parseFloat(val)); - const durationMin = parsed[0]; - const durationMax = parsed[1]; - const gapMin = parsed[2]; - const gapMax = parsed[3]; + const durationMin = parsed[0]!; + const durationMax = parsed[1]!; + const gapMin = parsed[2]!; + const gapMax = parsed[3]!; if (!(parsed.every((val) => !Number.isNaN(val)) && durationMin <= durationMax && gapMin <= gapMax && durationMin > 0)) return undefined; return { durationMin, durationMax, gapMin, gapMax }; @@ -306,7 +307,7 @@ async function askForSegmentsStartOrEnd(text: string) { }); if (!value) return undefined; - return value === 'both' ? ['start', 'end'] : [value]; + return value === 'both' ? ['start', 'end'] as const : [value as 'start' | 'end'] as const; } export async function askForShiftSegments({ inputPlaceholder, parseTimecode }: { inputPlaceholder: string, parseTimecode: ParseTimecode }) { @@ -338,6 +339,7 @@ export async function askForShiftSegments({ inputPlaceholder, parseTimecode }: { if (value == null) return undefined; const parsed = parseValue(value); + invariant(parsed != null); const startOrEnd = await askForSegmentsStartOrEnd(i18n.t('Do you want to shift the start or end timestamp by {{time}}?', { time: formatDuration({ seconds: parsed, shorten: true }) })); if (startOrEnd == null) return undefined; @@ -567,7 +569,7 @@ export async function labelSegmentDialog({ currentName, maxLength }: { currentNa title: i18n.t('Label current segment'), inputValue: currentName, input: currentName.includes('\n') ? 'textarea' : 'text', - inputValidator: (v) => (v.length > maxLength ? `${i18n.t('Max length')} ${maxLength}` : null), + inputValidator: (v: string) => (v.length > maxLength ? `${i18n.t('Max length')} ${maxLength}` : null), }); return value; } @@ -685,7 +687,7 @@ export async function openDirToast({ filePath, text, html, ...props }: SweetAler if (value) showItemInFolder(filePath); } -const UnorderedList = ({ children }) => ( +const UnorderedList = ({ children }: { children: ReactNode }) => (
    {children}
); const ListItem = ({ icon: Icon, iconColor, children, style }: { icon: IconComponent, iconColor?: string, children: ReactNode, style?: CSSProperties }) => ( @@ -736,7 +738,7 @@ export async function openConcatFinishedToast({ filePath, warnings, notices }: { await openDirToast({ filePath, html, width: 800, position: 'center', timer: 30000 }); } -export async function askForPlaybackRate({ detectedFps, outputPlaybackRate }) { +export async function askForPlaybackRate({ detectedFps, outputPlaybackRate }: { detectedFps: number | undefined, outputPlaybackRate: number }) { const fps = detectedFps || 1; const currentFps = fps * outputPlaybackRate; diff --git a/src/renderer/src/edlStore.ts b/src/renderer/src/edlStore.ts index c21a6a72..a9459dc7 100644 --- a/src/renderer/src/edlStore.ts +++ b/src/renderer/src/edlStore.ts @@ -141,7 +141,11 @@ export async function askForEdlImport({ type, fps }: { type: EdlImportType, fps? else if (type === 'srt') filters = [{ name: i18n.t('Subtitles (SRT)'), extensions: ['srt'] }]; else if (type === 'llc') filters = [{ name: i18n.t('LosslessCut project'), extensions: ['llc'] }]; - const { canceled, filePaths } = await showOpenDialog({ properties: ['openFile'], filters, title: i18n.t('Import project') }); + const { canceled, filePaths } = await showOpenDialog({ + properties: ['openFile'], + title: i18n.t('Import project'), + ...(filters && { filters }), + }); const [firstFilePath] = filePaths; if (canceled || firstFilePath == null) return []; return readEdlFile({ type, path: firstFilePath, fps }); diff --git a/src/renderer/src/ffmpeg.ts b/src/renderer/src/ffmpeg.ts index 4776cc67..95d8c4c3 100644 --- a/src/renderer/src/ffmpeg.ts +++ b/src/renderer/src/ffmpeg.ts @@ -1,7 +1,7 @@ import pMap from 'p-map'; import sortBy from 'lodash/sortBy'; import i18n from 'i18next'; -import Timecode from 'smpte-timecode'; +import Timecode, { FRAMERATE } from 'smpte-timecode'; import minBy from 'lodash/minBy'; import invariant from 'tiny-invariant'; @@ -88,13 +88,13 @@ export async function readFrames({ filePath, from, to, streamIndex }: { return sortBy(packetsFiltered, 'time'); } -export async function readFramesAroundTime({ filePath, streamIndex, aroundTime, window }) { +export async function readFramesAroundTime({ filePath, streamIndex, aroundTime, window }: { filePath: string, streamIndex: number, aroundTime: number, window: number }) { if (aroundTime == null) throw new Error('aroundTime was nullish'); const { from, to } = getIntervalAroundTime(aroundTime, window); return readFrames({ filePath, from, to, streamIndex }); } -export async function readKeyframesAroundTime({ filePath, streamIndex, aroundTime, window }) { +export async function readKeyframesAroundTime({ filePath, streamIndex, aroundTime, window }: { filePath: string, streamIndex: number, aroundTime: number, window: number }) { const frames = await readFramesAroundTime({ filePath, aroundTime, streamIndex, window }); return frames.filter((frame) => frame.keyframe); } @@ -158,7 +158,7 @@ export function getSafeCutTime(frames: (Frame & { time: number })[], cutTime: nu return time; } - const findReverseIndex = (arr, cb) => { + const findReverseIndex = (arr: T[], cb: (value: T, i: number, obj: T[]) => unknown) => { // eslint-disable-next-line unicorn/no-array-callback-reference const ret = [...arr].reverse().findIndex(cb); if (ret === -1) return -1; @@ -477,7 +477,7 @@ export function isIphoneHevc(format: FFprobeFormat, streams: FFprobeStream[]) { if (!streams.some((s) => s.codec_name === 'hevc')) return false; const makeTag = format.tags && format.tags['com.apple.quicktime.make']; const modelTag = format.tags && format.tags['com.apple.quicktime.model']; - return (makeTag === 'Apple' && modelTag.startsWith('iPhone')); + return (makeTag === 'Apple' && modelTag?.startsWith('iPhone')); } export function isProblematicAvc1(outFormat: string | undefined, streams: FFprobeStream[]) { @@ -524,7 +524,7 @@ export function getStreamFps(stream: FFprobeStream) { function parseTimecode(str: string, frameRate?: number | undefined) { // console.log(str, frameRate); - const t = Timecode(str, frameRate ? parseFloat(frameRate.toFixed(3)) : undefined); + const t = Timecode(str, frameRate ? parseFloat(frameRate.toFixed(3)) as FRAMERATE : undefined); if (!t) return undefined; const seconds = ((t.hours * 60) + t.minutes) * 60 + t.seconds + (t.frames / t.frameRate); return Number.isFinite(seconds) ? seconds : undefined; diff --git a/src/renderer/src/ffprobe.ts b/src/renderer/src/ffprobe.ts index 537ebf59..4a16f6d1 100644 --- a/src/renderer/src/ffprobe.ts +++ b/src/renderer/src/ffprobe.ts @@ -31,10 +31,10 @@ export function parseLevel(videoStream: FFprobeStream) { return undefined; } -export function parseProfile(videoStream) { +export function parseProfile(videoStream: FFprobeStream) { const { profile: ffprobeProfile, codec_name: videoCodec } = videoStream; - let map; + let map: Map; if (videoCodec === 'h264') { // List of profiles that x264 supports https://trac.ffmpeg.org/wiki/Encode/H.264 // baseline, main, high, high10 (first 10 bit compatible profile), high422 (supports yuv420p, yuv422p, yuv420p10le and yuv422p10le), high444 (supports as above as well as yuv444p and yuv444p10le) @@ -71,12 +71,13 @@ export function parseProfile(videoStream) { return undefined; } - if (!map.has(ffprobeProfile)) { + const match = map.get(ffprobeProfile); + + if (match == null) { console.warn('Unknown ffprobe profile', ffprobeProfile); return undefined; } - const match = map.get(ffprobeProfile); if (match.warn) console.warn('Possibly unknown ffprobe profile', ffprobeProfile); return match.profile; } diff --git a/src/renderer/src/hooks/useFfmpegOperations.ts b/src/renderer/src/hooks/useFfmpegOperations.ts index ed9fd0ac..f8867bad 100644 --- a/src/renderer/src/hooks/useFfmpegOperations.ts +++ b/src/renderer/src/hooks/useFfmpegOperations.ts @@ -559,7 +559,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea }]; // eslint-disable-next-line no-shadow - async function cutEncodeSmartPartWrapper({ cutFrom, cutTo, outPath }) { + async function cutEncodeSmartPartWrapper({ cutFrom, cutTo, outPath }: { cutFrom: number, cutTo: number, outPath: string }) { if (await shouldSkipExistingFile(outPath)) return; if (videoCodec == null || detectedVideoBitrate == null || videoTimebase == null) throw new Error(); invariant(filePath != null); @@ -851,16 +851,16 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea // TODO add more // TODO allow user to change? - }; + } as const; - const match = map[stream.codec_name]; + const match = map[stream.codec_name as keyof typeof map]; if (match) return match; // default fallbacks: - if (stream.codec_type === 'video') return { ext: 'mkv', format: 'matroska' }; - if (stream.codec_type === 'audio') return { ext: 'mka', format: 'matroska' }; - if (stream.codec_type === 'subtitle') return { ext: 'mks', format: 'matroska' }; - if (stream.codec_type === 'data') return { ext: 'bin', format: 'data' }; // https://superuser.com/questions/1243257/save-data-stream + if (stream.codec_type === 'video') return { ext: 'mkv', format: 'matroska' } as const; + if (stream.codec_type === 'audio') return { ext: 'mka', format: 'matroska' } as const; + if (stream.codec_type === 'subtitle') return { ext: 'mks', format: 'matroska' } as const; + if (stream.codec_type === 'data') return { ext: 'bin', format: 'data' } as const; // https://superuser.com/questions/1243257/save-data-stream return undefined; } @@ -871,13 +871,19 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea invariant(filePath != null); if (streams.length === 0) return []; - const outStreams = streams.map((s) => ({ - index: s.index, - codec: s.codec_name || s.codec_tag_string || s.codec_type, - type: s.codec_type, - format: getPreferredCodecFormat(s), - })) - .filter(({ format, index }) => format != null && index != null); + const outStreams = streams.flatMap((s) => { + const format = getPreferredCodecFormat(s); + const { index } = s; + + if (format == null || index == null) return []; + + return [{ + index, + codec: s.codec_name || s.codec_tag_string || s.codec_type, + type: s.codec_type, + format, + }]; + }); // console.log(outStreams); diff --git a/src/renderer/src/hooks/useKeyframes.ts b/src/renderer/src/hooks/useKeyframes.ts index 8e0819bb..deaf3f0f 100644 --- a/src/renderer/src/hooks/useKeyframes.ts +++ b/src/renderer/src/hooks/useKeyframes.ts @@ -20,7 +20,7 @@ function useKeyframes({ keyframesEnabled, filePath, commandedTime, videoStream, const [neighbouringKeyFramesMap, setNeighbouringKeyFrames] = useState>({}); const neighbouringKeyFrames = useMemo(() => Object.values(neighbouringKeyFramesMap), [neighbouringKeyFramesMap]); - const findNearestKeyFrameTime = useCallback(({ time, direction }) => ffmpegFindNearestKeyFrameTime({ frames: neighbouringKeyFrames, time, direction, fps: detectedFps }), [neighbouringKeyFrames, detectedFps]); + const findNearestKeyFrameTime = useCallback(({ time, direction }: { time: number, direction: number }) => ffmpegFindNearestKeyFrameTime({ frames: neighbouringKeyFrames, time, direction, fps: detectedFps }), [neighbouringKeyFrames, detectedFps]); useEffect(() => setNeighbouringKeyFrames({}), [filePath, videoStream]); diff --git a/src/renderer/src/hooks/useStreamsMeta.ts b/src/renderer/src/hooks/useStreamsMeta.ts index eb9bfbf0..1312aed1 100644 --- a/src/renderer/src/hooks/useStreamsMeta.ts +++ b/src/renderer/src/hooks/useStreamsMeta.ts @@ -45,7 +45,7 @@ export default ({ mainStreams, filePath, autoExportExtraStreams }: { const checkCopyingAnyTrackOfType = useCallback((filter: (s: FFprobeStream) => boolean) => mainStreams.some((stream) => isCopyingStreamId(filePath, stream.index) && filter(stream)), [filePath, isCopyingStreamId, mainStreams]); - const toggleStripStream = useCallback((filter) => { + const toggleStripStream = useCallback((filter: (s: FFprobeStream) => boolean) => { const copyingAnyTrackOfType = checkCopyingAnyTrackOfType(filter); invariant(filePath != null); setCopyStreamIdsForPath(filePath, (old) => { diff --git a/src/renderer/src/hooks/useWhatChanged.ts b/src/renderer/src/hooks/useWhatChanged.ts index 3cbd41d5..7abde41a 100644 --- a/src/renderer/src/hooks/useWhatChanged.ts +++ b/src/renderer/src/hooks/useWhatChanged.ts @@ -1,7 +1,7 @@ import React from 'react'; // https://stackoverflow.com/questions/64997362/how-do-i-see-what-props-have-changed-in-react -export default function useWhatChanged(props) { +export default function useWhatChanged(props: Record) { // cache the last set of props const prev = React.useRef(props); diff --git a/src/renderer/src/segments.ts b/src/renderer/src/segments.ts index f84f679f..4cb6aba9 100644 --- a/src/renderer/src/segments.ts +++ b/src/renderer/src/segments.ts @@ -106,27 +106,30 @@ export function partitionIntoOverlappingRanges group.length > 1).map((group) => sortBy(group, (seg) => getSegmentStart(seg))); } -export function combineOverlappingSegments(existingSegments, getSegApparentEnd2) { +export function combineOverlappingSegments(existingSegments: T[], getSegApparentEnd2: (seg: SegmentBase) => number) { const partitionedSegments = partitionIntoOverlappingRanges(existingSegments, getSegApparentStart, getSegApparentEnd2); - return existingSegments.map((existingSegment) => { + return existingSegments.flatMap((existingSegment) => { const partOfPartition = partitionedSegments.find((partition) => partition.includes(existingSegment)); - if (partOfPartition == null) return existingSegment; // this is not an overlapping segment, pass it through + if (partOfPartition == null) { + return [existingSegment]; // this is not an overlapping segment, pass it through + } const index = partOfPartition.indexOf(existingSegment); // The first segment is the one with the lowest "start" value, so we use its start value if (index === 0) { - return { + return [{ ...existingSegment, // but use the segment with the highest "end" value as the end value. end: sortBy(partOfPartition, (segment) => segment.end)[partOfPartition.length - 1]!.end, - }; + }]; } - return undefined; // then remove all other segments in this partition group - }).filter(Boolean); + + return []; // then remove all other segments in this partition group + }); } -export function combineSelectedSegments(existingSegments: T[], getSegApparentEnd2, isSegmentSelected) { +export function combineSelectedSegments(existingSegments: StateSegment[], getSegApparentEnd2: (seg: StateSegment) => number, isSegmentSelected: (seg: StateSegment) => boolean) { const selectedSegments = existingSegments.filter((segment) => isSegmentSelected(segment)); const firstSegment = minBy(selectedSegments, (seg) => getSegApparentStart(seg)); const lastSegment = maxBy(selectedSegments, (seg) => getSegApparentEnd2(seg)); diff --git a/src/renderer/src/theme.ts b/src/renderer/src/theme.ts index 177e5c96..800c81b3 100644 --- a/src/renderer/src/theme.ts +++ b/src/renderer/src/theme.ts @@ -37,9 +37,9 @@ const customTheme: ProviderProps['value'] = { // https://github.com/segmentio/evergreen/blob/master/src/themes/default/components/button.js // eslint-disable-next-line @typescript-eslint/no-explicit-any - border: ((_theme, props) => `1px solid ${borderColorForIntent(props.intent)}`) as any as string, // todo types + border: ((_theme: unknown, props: { intent: IntentTypes }) => `1px solid ${borderColorForIntent(props.intent)}`) as any as string, // todo types // eslint-disable-next-line @typescript-eslint/no-explicit-any - color: ((_theme, props) => props.color || colorKeyForIntent(props.intent)) as any as string, // todo types + color: ((_theme: unknown, props: { color: string, intent: IntentTypes }) => props.color || colorKeyForIntent(props.intent)) as any as string, // todo types _hover: { backgroundColor: 'var(--gray4)', @@ -61,7 +61,7 @@ const customTheme: ProviderProps['value'] = { // https://github.com/segmentio/evergreen/blob/master/src/themes/default/components/button.js // eslint-disable-next-line @typescript-eslint/no-explicit-any - color: ((_theme, props) => props.color || colorKeyForIntent(props.intent)) as any as string, // todo types + color: ((_theme: unknown, props: { color: string, intent: IntentTypes }) => props.color || colorKeyForIntent(props.intent)) as any as string, // todo types _hover: { backgroundColor: 'var(--gray4)', diff --git a/src/renderer/src/types.ts b/src/renderer/src/types.ts index 012c210f..c04142a4 100644 --- a/src/renderer/src/types.ts +++ b/src/renderer/src/types.ts @@ -136,3 +136,8 @@ export interface StreamParams { bsfHevcMp4toannexb?: boolean, } export type ParamsByStreamId = Map>; + +export interface BatchFile { + path: string, + name: string, +} diff --git a/src/renderer/src/util.ts b/src/renderer/src/util.ts index 89c49657..b5c75c7a 100644 --- a/src/renderer/src/util.ts +++ b/src/renderer/src/util.ts @@ -506,8 +506,8 @@ export async function readDirRecursively(dirPath: string) { export function getImportProjectType(filePath: string) { if (filePath.endsWith('Summary.txt')) return 'dv-analyzer-summary-txt'; - const edlFormatForExtension = { csv: 'csv', pbf: 'pbf', edl: 'edl', cue: 'cue', xml: 'xmeml', fcpxml: 'fcpxml' }; - const matchingExt = Object.keys(edlFormatForExtension).find((ext) => filePath.toLowerCase().endsWith(`.${ext}`)); + const edlFormatForExtension = { csv: 'csv', pbf: 'pbf', edl: 'edl', cue: 'cue', xml: 'xmeml', fcpxml: 'fcpxml' } as const; + const matchingExt = Object.keys(edlFormatForExtension).find((ext) => filePath.toLowerCase().endsWith(`.${ext}`)) as keyof typeof edlFormatForExtension | undefined; if (!matchingExt) return undefined; return edlFormatForExtension[matchingExt]; } diff --git a/src/renderer/src/worker/evalWorker.ts b/src/renderer/src/worker/evalWorker.ts index b426ee9b..5cf2cbb2 100644 --- a/src/renderer/src/worker/evalWorker.ts +++ b/src/renderer/src/worker/evalWorker.ts @@ -67,10 +67,12 @@ Object.getOwnPropertyNames(myGlobal).forEach(function (prop) { } }); +// @ts-expect-error dunno // eslint-disable-next-line no-proto, prefer-arrow-callback, func-names Object.getOwnPropertyNames(myGlobal.__proto__).forEach(function (prop) { // eslint-disable-next-line no-prototype-builtins if (!wl.hasOwnProperty(prop)) { + // @ts-expect-error dunno // eslint-disable-next-line no-proto Object.defineProperty(myGlobal.__proto__, prop, { // eslint-disable-next-line func-names, object-shorthand @@ -92,7 +94,7 @@ Object.defineProperty(Array.prototype, 'join', { // eslint-disable-next-line wrap-iife, func-names value: function (old) { // eslint-disable-next-line func-names - return function (arg) { + return function (arg: unknown[]) { // @ts-expect-error dunno how to fix if (this.length > 500 || (arg && arg.length > 500)) { // eslint-disable-next-line no-throw-literal diff --git a/tsconfig.web.json b/tsconfig.web.json index 3eb0d857..4badbdd4 100644 --- a/tsconfig.web.json +++ b/tsconfig.web.json @@ -4,8 +4,6 @@ "lib": ["ES2023", "DOM", "DOM.Iterable"], "noEmit": true, - "noImplicitAny": false, // todo - "types": ["vite/client"], }, "references": [ diff --git a/yarn.lock b/yarn.lock index 4b59513b..53f13c5d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2233,6 +2233,15 @@ __metadata: languageName: node linkType: hard +"@types/react-syntax-highlighter@npm:^15.5.13": + version: 15.5.13 + resolution: "@types/react-syntax-highlighter@npm:15.5.13" + dependencies: + "@types/react": "npm:*" + checksum: 10/0cc47785326aea5effac9ee68c263abc6448c63b6a084eec3084fd13c08da93addfdc3102ff919cbe420aa71fbded9bf041cc2c51fe625c6ee0751e8a131bd38 + languageName: node + linkType: hard + "@types/react-transition-group@npm:^4.4.0": version: 4.4.4 resolution: "@types/react-transition-group@npm:4.4.4" @@ -2315,6 +2324,13 @@ __metadata: languageName: node linkType: hard +"@types/smpte-timecode@npm:^1.2.5": + version: 1.2.5 + resolution: "@types/smpte-timecode@npm:1.2.5" + checksum: 10/e91c0535df278bd3cbe9b5517c5edf084531322237ec876ec40154d79968faa343404822f5b85df42455a2749e1e7a13a1254b9fc893cf7de09823e0d74c4f03 + languageName: node + linkType: hard + "@types/sortablejs@npm:^1.15.0": version: 1.15.0 resolution: "@types/sortablejs@npm:1.15.0" @@ -7599,6 +7615,8 @@ __metadata: "@types/node": "npm:20" "@types/react": "npm:^18.2.66" "@types/react-dom": "npm:^18.2.22" + "@types/react-syntax-highlighter": "npm:^15.5.13" + "@types/smpte-timecode": "npm:^1.2.5" "@types/sortablejs": "npm:^1.15.0" "@types/yargs-parser": "npm:^21.0.3" "@typescript-eslint/eslint-plugin": "npm:^6.12.0"