diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 404a2b9c..6ad384fc 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -87,7 +87,7 @@ import BigWaveform from './components/BigWaveform'; import isDev from './isDev'; import { Chapter, ChromiumHTMLVideoElement, CustomTagsByFile, EdlFileType, FfmpegCommandLog, FilesMeta, FormatTimecode, ParamsByStreamId, ParseTimecode, PlaybackMode, SegmentColorIndex, SegmentTags, SegmentToExport, StateSegment, Thumbnail, TunerType } from './types'; -import { CaptureFormat, KeyboardAction, Html5ifyMode } from '../../../types'; +import { CaptureFormat, KeyboardAction, Html5ifyMode, WaveformMode } from '../../../types'; import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../../ffprobe'; const electron = window.require('electron'); @@ -151,7 +151,7 @@ function App() { // State per application launch const lastOpenedPathRef = useRef(); - const [waveformMode, setWaveformMode] = useState<'big-waveform' | 'waveform'>(); + const [waveformMode, setWaveformMode] = useState(); const [thumbnailsEnabled, setThumbnailsEnabled] = useState(false); const [keyframesEnabled, setKeyframesEnabled] = useState(true); const [showRightBar, setShowRightBar] = useState(true); @@ -215,7 +215,7 @@ function App() { const videoRef = useRef(null); const videoContainerRef = useRef(null); - const setOutputPlaybackRate = useCallback((v) => { + const setOutputPlaybackRate = useCallback((v: number) => { setOutputPlaybackRateState(v); if (videoRef.current) videoRef.current.playbackRate = v; }, []); @@ -1634,7 +1634,7 @@ function App() { const toggleLastCommands = useCallback(() => setLastCommandsVisible((val) => !val), []); const toggleSettings = useCallback(() => setSettingsVisible((val) => !val), []); - const seekClosestKeyframe = useCallback((direction) => { + const seekClosestKeyframe = useCallback((direction: number) => { const time = findNearestKeyFrameTime({ time: getRelevantTime(), direction }); if (time == null) return; userSeekAbs(time); diff --git a/src/renderer/src/BottomBar.jsx b/src/renderer/src/BottomBar.tsx similarity index 85% rename from src/renderer/src/BottomBar.jsx rename to src/renderer/src/BottomBar.tsx index 206a82a7..48c9d730 100644 --- a/src/renderer/src/BottomBar.jsx +++ b/src/renderer/src/BottomBar.tsx @@ -1,4 +1,4 @@ -import { memo, useCallback, useEffect, useMemo, useState } from 'react'; +import { CSSProperties, Dispatch, SetStateAction, memo, useCallback, useEffect, useMemo, useState } from 'react'; import { motion } from 'framer-motion'; import { MdRotate90DegreesCcw } from 'react-icons/md'; import { useTranslation } from 'react-i18next'; @@ -23,16 +23,18 @@ import { useSegColors } from './contexts'; import { isExactDurationMatch } from './util/duration'; import useUserSettings from './hooks/useUserSettings'; import { askForPlaybackRate } from './dialogs'; +import { ApparentCutSegment, FormatTimecode, ParseTimecode, SegmentToExport, StateSegment } from './types'; +import { WaveformMode } from '../../../types'; const { clipboard } = window.require('electron'); -const zoomOptions = Array.from({ length: 13 }).fill().map((unused, z) => 2 ** z); +const zoomOptions = Array.from({ length: 13 }).fill(undefined).map((_unused, z) => 2 ** z); const leftRightWidth = 100; // eslint-disable-next-line react/display-name -const InvertCutModeButton = memo(({ invertCutSegments, setInvertCutSegments }) => { +const InvertCutModeButton = memo(({ invertCutSegments, setInvertCutSegments }: { invertCutSegments: boolean, setInvertCutSegments: Dispatch> }) => { const { t } = useTranslation(); const onYinYangClick = useCallback(() => { @@ -65,15 +67,26 @@ const InvertCutModeButton = memo(({ invertCutSegments, setInvertCutSegments }) = // eslint-disable-next-line react/display-name -const CutTimeInput = memo(({ darkMode, cutTime, setCutTime, startTimeOffset, seekAbs, currentCutSeg, currentApparentCutSeg, isStart, formatTimecode, parseTimecode }) => { +const CutTimeInput = memo(({ darkMode, cutTime, setCutTime, startTimeOffset, seekAbs, currentCutSeg, currentApparentCutSeg, isStart, formatTimecode, parseTimecode }: { + darkMode: boolean, + cutTime: number, + setCutTime: (type: 'start' | 'end', v: number) => void, + startTimeOffset: number, + seekAbs: (a: number) => void, + currentCutSeg: StateSegment, + currentApparentCutSeg: ApparentCutSegment, + isStart?: boolean, + formatTimecode: FormatTimecode, + parseTimecode: ParseTimecode, +}) => { const { t } = useTranslation(); const { getSegColor } = useSegColors(); - const [cutTimeManual, setCutTimeManual] = useState(); + const [cutTimeManual, setCutTimeManual] = useState(); // Clear manual overrides if upstream cut time has changed useEffect(() => { - setCutTimeManual(); + setCutTimeManual(undefined); }, [setCutTimeManual, currentApparentCutSeg.start, currentApparentCutSeg.end]); const isCutTimeManualSet = () => cutTimeManual !== undefined; @@ -83,7 +96,7 @@ const CutTimeInput = memo(({ darkMode, cutTime, setCutTime, startTimeOffset, see return `.1em solid ${darkMode ? segColor.desaturate(0.4).lightness(50).string() : segColor.desaturate(0.2).lightness(60).string()}`; }, [currentCutSeg, darkMode, getSegColor]); - const cutTimeInputStyle = { + const cutTimeInputStyle: CSSProperties = { border, borderRadius: 5, backgroundColor: 'var(--gray5)', transition: darkModeTransition, fontSize: 13, textAlign: 'center', padding: '1px 5px', marginTop: 0, marginBottom: 0, marginLeft: isStart ? 0 : 5, marginRight: isStart ? 5 : 0, boxSizing: 'border-box', fontFamily: 'inherit', width: 90, outline: 'none', }; @@ -92,7 +105,7 @@ const CutTimeInput = memo(({ darkMode, cutTime, setCutTime, startTimeOffset, see try { setCutTime(isStart ? 'start' : 'end', timeWithoutOffset); seekAbs(timeWithoutOffset); - setCutTimeManual(); + setCutTimeManual(undefined); } catch (err) { console.error('Cannot set cut time', err); // If we get an error from setCutTime, remain in the editing state (cutTimeManual) @@ -104,7 +117,7 @@ const CutTimeInput = memo(({ darkMode, cutTime, setCutTime, startTimeOffset, see e.preventDefault(); // Don't proceed if not a valid time value - const timeWithOffset = parseTimecode(cutTimeManual); + const timeWithOffset = cutTimeManual != null ? parseTimecode(cutTimeManual) : undefined; if (timeWithOffset === undefined) return; trySetTime(timeWithOffset); @@ -158,7 +171,7 @@ const CutTimeInput = memo(({ darkMode, cutTime, setCutTime, startTimeOffset, see title={isStart ? t('Manually input current segment\'s start time') : t('Manually input current segment\'s end time')} onChange={(e) => handleCutTimeInput(e.target.value)} onPaste={handleCutTimePaste} - onBlur={() => setCutTimeManual()} + onBlur={() => setCutTimeManual(undefined)} onContextMenu={handleContextMenu} value={isCutTimeManualSet() ? cutTimeManual @@ -181,6 +194,54 @@ function BottomBar({ toggleShowThumbnails, toggleWaveformMode, waveformMode, showThumbnails, outputPlaybackRate, setOutputPlaybackRate, formatTimecode, parseTimecode, +}: { + zoom: number, + setZoom: Dispatch>, + timelineToggleComfortZoom: () => void, + isRotationSet: boolean, + rotation: number, + areWeCutting: boolean, + increaseRotation: () => void, + cleanupFilesDialog: () => void, + captureSnapshot: () => void, + onExportPress: () => void, + segmentsToExport: SegmentToExport[], + hasVideo: boolean, + seekAbs: (a: number) => void, + currentSegIndexSafe: number, + cutSegments: StateSegment[], + currentCutSeg: StateSegment, + setCutStart: () => void, + setCutEnd: () => void, + setCurrentSegIndex: Dispatch>, + jumpTimelineStart: () => void, + jumpTimelineEnd: () => void, + jumpCutEnd: () => void, + jumpCutStart: () => void, + startTimeOffset: number, + setCutTime: (type: 'start' | 'end', v: number) => void, + currentApparentCutSeg: ApparentCutSegment, + playing: boolean, + shortStep: (a: number) => void, + togglePlay: () => void, + toggleLoopSelectedSegments: () => void, + hasAudio: boolean, + keyframesEnabled: boolean, + toggleShowKeyframes: () => void, + seekClosestKeyframe: (a: number) => void, + detectedFps: number | undefined, + isFileOpened: boolean, + selectedSegments: ApparentCutSegment[], + darkMode: boolean, + setDarkMode: Dispatch>, + toggleShowThumbnails: () => void, + toggleWaveformMode: () => void, + waveformMode: WaveformMode | undefined, + showThumbnails: boolean, + outputPlaybackRate: number, + setOutputPlaybackRate: (v: number) => void, + formatTimecode: FormatTimecode, + parseTimecode: ParseTimecode, }) { const { t } = useTranslation(); const { getSegColor } = useSegColors(); @@ -188,7 +249,7 @@ function BottomBar({ // ok this is a bit over-engineered but what the hell! const loopSelectedSegmentsButtonStyle = useMemo(() => { // cannot have less than 1 gradient element: - const selectedSegmentsSafe = (selectedSegments.length > 1 ? selectedSegments : [selectedSegments[0], selectedSegments[0]]).slice(0, 10); + const selectedSegmentsSafe = (selectedSegments.length > 1 ? selectedSegments : [selectedSegments[0]!, selectedSegments[0]!]).slice(0, 10); const gradientColors = selectedSegmentsSafe.map((seg, i) => { const segColor = getSegColorRaw(seg); @@ -233,7 +294,7 @@ function BottomBar({ const opacity = seg ? undefined : 0.5; const text = seg ? `${newIndex + 1}` : '-'; const wide = text.length > 1; - const segButtonStyle = { + const segButtonStyle: CSSProperties = { backgroundColor, opacity, padding: `6px ${wide ? 4 : 6}px`, borderRadius: 10, color: seg ? 'white' : undefined, fontSize: wide ? 12 : 14, width: 20, boxSizing: 'border-box', letterSpacing: -1, lineHeight: '10px', fontWeight: 'bold', margin: '0 6px', }; @@ -262,7 +323,7 @@ function BottomBar({ {hasAudio && ( toggleWaveformMode()} diff --git a/src/renderer/src/animations.js b/src/renderer/src/animations.ts similarity index 100% rename from src/renderer/src/animations.js rename to src/renderer/src/animations.ts diff --git a/src/renderer/src/colors.js b/src/renderer/src/colors.ts similarity index 100% rename from src/renderer/src/colors.js rename to src/renderer/src/colors.ts diff --git a/src/renderer/src/dialogs/extractFrames.jsx b/src/renderer/src/dialogs/extractFrames.tsx similarity index 95% rename from src/renderer/src/dialogs/extractFrames.jsx rename to src/renderer/src/dialogs/extractFrames.tsx index 342e6ea9..968f5150 100644 --- a/src/renderer/src/dialogs/extractFrames.jsx +++ b/src/renderer/src/dialogs/extractFrames.tsx @@ -4,8 +4,8 @@ import Swal from '../swal'; // eslint-disable-next-line import/prefer-default-export -export async function askExtractFramesAsImages({ segmentsNumFrames, plural, fps }) { - const { value: captureChoice } = await Swal.fire({ +export async function askExtractFramesAsImages({ segmentsNumFrames, plural, fps }: { segmentsNumFrames: number, plural: boolean, fps: number }) { + const { value: captureChoice } = await Swal.fire({ text: i18n.t(plural ? 'Extract frames of the selected segments as images' : 'Extract frames of the current segment as images'), icon: 'question', input: 'radio', @@ -23,7 +23,7 @@ export async function askExtractFramesAsImages({ segmentsNumFrames, plural, fps if (!captureChoice) return undefined; - let filter; + let filter: string | undefined; let estimatedMaxNumFiles = segmentsNumFrames; if (captureChoice === 'thumbnailFilter') { @@ -44,7 +44,7 @@ export async function askExtractFramesAsImages({ segmentsNumFrames, plural, fps } if (captureChoice === 'selectNthSec' || captureChoice === 'selectNthFrame') { - let nthFrame; + let nthFrame: number; if (captureChoice === 'selectNthFrame') { const { value } = await Swal.fire({ text: i18n.t('Capture exactly one image every nth frame'), diff --git a/src/renderer/src/ffmpeg-parameters.js b/src/renderer/src/ffmpeg-parameters.ts similarity index 100% rename from src/renderer/src/ffmpeg-parameters.js rename to src/renderer/src/ffmpeg-parameters.ts diff --git a/src/renderer/src/ffprobe.js b/src/renderer/src/ffprobe.ts similarity index 95% rename from src/renderer/src/ffprobe.js rename to src/renderer/src/ffprobe.ts index 45e04d1c..537ebf59 100644 --- a/src/renderer/src/ffprobe.js +++ b/src/renderer/src/ffprobe.ts @@ -1,6 +1,8 @@ // This code is for future use (e.g. creating black video to fill in using same codec parameters) -export function parseLevel(videoStream) { +import { FFprobeStream } from '../../../ffprobe'; + +export function parseLevel(videoStream: FFprobeStream) { const { level: levelNumeric, codec_name: videoCodec } = videoStream; if (levelNumeric == null || Number.isNaN(levelNumeric)) return undefined; @@ -9,7 +11,7 @@ export function parseLevel(videoStream) { if (levelNumeric === 9) return '1b'; // 13 is 1.3. That are all like that (20 is 2.0, etc) except 1b which is 9. let level = (levelNumeric / 10).toFixed(1); // https://stackoverflow.com/questions/42619191/what-does-level-mean-in-ffprobe-output - if (level >= 0) { + if (parseFloat(level) >= 0) { if (level.slice(-2) === '.0') level = level.slice(0, -2); // slice off .0 const validLevels = ['1', '1b', '1.1', '1.2', '1.3', '2', '2.1', '2.2', '3', '3.1', '3.2', '4', '4.1', '4.2', '5', '5.1', '5.2', '6', '6.1', '6.2']; // https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels if (validLevels.includes(level)) return level; @@ -17,7 +19,7 @@ export function parseLevel(videoStream) { } else if (videoCodec === 'hevc') { // Note that on MacOS we don't use x265, but videotoolbox let level = (levelNumeric / 30).toFixed(1); // https://stackoverflow.com/questions/69983131/whats-the-difference-between-ffprobe-level-and-h-264-level - if (level >= 0) { + if (parseFloat(level) >= 0) { if (level.slice(-2) === '.0') level = level.slice(0, -2); // slice off .0 const validLevels = ['1', '2', '2.1', '3', '3.1', '4', '4.1', '5', '5.1', '5.2', '6', '6.1', '6.2']; // https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding_tiers_and_levels if (validLevels.includes(level)) return level; diff --git a/src/renderer/src/hooks/useSegments.ts b/src/renderer/src/hooks/useSegments.ts index ecb2fb51..b90611fb 100644 --- a/src/renderer/src/hooks/useSegments.ts +++ b/src/renderer/src/hooks/useSegments.ts @@ -230,7 +230,7 @@ function useSegments({ filePath, workingRef, setWorking, setCutProgress, videoSt setCutSegments(cutSegmentsNew); }, [setCutSegments, cutSegments]); - const setCutTime = useCallback((type, time) => { + const setCutTime = useCallback((type: 'start' | 'end', time: number) => { if (!isDurationValid(duration)) return; const currentSeg = currentCutSeg; diff --git a/src/renderer/src/hooks/useWhatChanged.js b/src/renderer/src/hooks/useWhatChanged.ts similarity index 100% rename from src/renderer/src/hooks/useWhatChanged.js rename to src/renderer/src/hooks/useWhatChanged.ts diff --git a/src/renderer/src/mifi.js b/src/renderer/src/mifi.ts similarity index 84% rename from src/renderer/src/mifi.js rename to src/renderer/src/mifi.ts index fa2cb747..fa3af799 100644 --- a/src/renderer/src/mifi.js +++ b/src/renderer/src/mifi.ts @@ -17,11 +17,11 @@ export async function loadMifiLink() { } } -export async function runStartupCheck({ ffmpeg }) { +export async function runStartupCheck({ ffmpeg }: { ffmpeg: boolean }) { try { if (ffmpeg) await runFfmpegStartupCheck(); } catch (err) { - if (['EPERM', 'EACCES'].includes(err.code)) { + if (err instanceof Error && 'code' in err && typeof err.code === 'string' && ['EPERM', 'EACCES'].includes(err.code)) { toast.fire({ timer: 30000, icon: 'error', diff --git a/src/renderer/src/outFormats.js b/src/renderer/src/outFormats.ts similarity index 100% rename from src/renderer/src/outFormats.js rename to src/renderer/src/outFormats.ts diff --git a/tsconfig.web.json b/tsconfig.web.json index 6e8ae213..56a2e8c4 100644 --- a/tsconfig.web.json +++ b/tsconfig.web.json @@ -5,8 +5,6 @@ "noEmit": true, "noImplicitAny": false, // todo - "checkJs": false, // todo - "allowJs": true, // todo }, "references": [ { "path": "./tsconfig.main.json" }, diff --git a/types.ts b/types.ts index 31789eec..2ccd9c37 100644 --- a/types.ts +++ b/types.ts @@ -112,6 +112,8 @@ export interface ApiKeyboardActionRequest { export type Html5ifyMode = 'fastest' | 'fast-audio-remux' | 'fast-audio' | 'fast' | 'slow' | 'slow-audio' | 'slowest'; +export type WaveformMode = 'big-waveform' | 'waveform'; + // This is the contract with the user, see https://github.com/mifi/lossless-cut/blob/master/expressions.md export interface ScopeSegment { label: string,