From bc2a5cc7af91cdc3611c324a229262ec8aee4654 Mon Sep 17 00:00:00 2001 From: young Date: Fri, 28 Aug 2026 09:54:11 +1000 Subject: [PATCH] Tighten React hook dependencies --- src/App.tsx | 2 +- src/components/launch/SourceSelector.tsx | 21 +- src/components/video-editor/VideoPlayback.tsx | 22 +- .../audio/useSourceAudioTrackSettings.ts | 207 +++++++++--------- .../components/waveform/AudioWaveform.tsx | 13 +- 5 files changed, 142 insertions(+), 123 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 9e1f4e4c..513320f4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -47,7 +47,7 @@ export default function App() { loadAllCustomFonts().catch((error) => { console.error("Failed to load custom fonts:", error); }); - }, []); + }, [isMacOS]); useEffect(() => { document.title = diff --git a/src/components/launch/SourceSelector.tsx b/src/components/launch/SourceSelector.tsx index baa4da59..dcbde5ed 100644 --- a/src/components/launch/SourceSelector.tsx +++ b/src/components/launch/SourceSelector.tsx @@ -42,7 +42,7 @@ export function MarqueeText({ text }: { text: string }) { useLayoutEffect(() => { const node = staticRef.current; - if (!node) return; + if (!node || node.textContent !== text) return; const checkOverflow = () => { setOverflowing(node.scrollWidth > node.clientWidth + 1); }; @@ -81,7 +81,10 @@ export const SourceSelectorContent = ({ selectedSource = "Screen", loading = false, onSourceSelect = () => undefined, -}: Pick) => { +}: Pick< + SourceSelectorProps, + "screenSources" | "windowSources" | "selectedSource" | "loading" | "onSourceSelect" +>) => { const t = useScopedT("launch"); const renderSourceItem = (source: DesktopSource, index: number) => { const isSelected = selectedSource === source.name; @@ -116,12 +119,14 @@ export const SourceSelectorContent = ({ )} -
+
- {source.sourceType === "screen" ? t("recording.screen") : t("recording.window")} + {source.sourceType === "screen" + ? t("recording.screen") + : t("recording.window")}
@@ -156,7 +161,9 @@ export const SourceSelectorContent = ({
- {screenSources.map((source, index) => renderSourceItem(source, index))} + {screenSources.map((source, index) => + renderSourceItem(source, index), + )}
) : null} @@ -166,7 +173,9 @@ export const SourceSelectorContent = ({ {t("recording.windows")}
- {windowSources.map((source, index) => renderSourceItem(source, index))} + {windowSources.map((source, index) => + renderSourceItem(source, index), + )}
) : null} diff --git a/src/components/video-editor/VideoPlayback.tsx b/src/components/video-editor/VideoPlayback.tsx index 52e9fa43..82b3c5c7 100644 --- a/src/components/video-editor/VideoPlayback.tsx +++ b/src/components/video-editor/VideoPlayback.tsx @@ -1260,7 +1260,7 @@ const VideoPlayback = forwardRef( const nextVolume = Math.max(0, Math.min(1, volume)); video.volume = nextVolume; video.muted = nextVolume <= 0.001; - }, [volume, videoPath]); + }, [volume]); useEffect(() => { layoutVideoContentRef.current = layoutVideoContent; @@ -1362,12 +1362,12 @@ const VideoPlayback = forwardRef( return () => { cancelled = true; }; - }, [aspectRatio, borderRadius, cropRegion, frame, frameUpdateCounter, padding]); + }, [frame, frameUpdateCounter]); // Always re-run geometric layout when layout props change, even if frame sprite isn't reloaded. useEffect(() => { - layoutVideoContentRef.current?.(); - }, [aspectRatio, borderRadius, cropRegion, padding]); + layoutVideoContent(); + }, [layoutVideoContent]); const selectedZoom = useMemo(() => { if (!selectedZoomId) return null; @@ -1545,6 +1545,7 @@ const VideoPlayback = forwardRef( useEffect(() => { suspendRenderingRef.current = suspendRendering; + if (!pixiReady) return; const app = appRef.current; if (!app?.ticker) { return; @@ -1656,6 +1657,7 @@ const VideoPlayback = forwardRef( }, [speedRegions]); useEffect(() => { + if (!pixiReady) return; const videoEffectsContainer = videoEffectsContainerRef.current; const zoomBlurFilter = zoomBlurFilterRef.current; const motionBlurFilter = motionBlurFilterRef.current; @@ -1783,7 +1785,7 @@ const VideoPlayback = forwardRef( motionBlurStateRef.current = createMotionBlurState(); videoEffectsContainer.filters = zoomMotionBlur > 0 ? [motionBlurFilter, zoomBlurFilter] : null; - }, [videoPath, zoomMotionBlur]); + }, [zoomMotionBlur]); useEffect(() => { zoomMotionBlurTuningRef.current = zoomMotionBlurTuning; @@ -1905,7 +1907,7 @@ const VideoPlayback = forwardRef( } }); }); - }, [pixiReady, videoReady, layoutVideoContent, cropRegion]); + }, [pixiReady, videoReady, layoutVideoContent]); useEffect(() => { if (!pixiReady || !videoReady) return; @@ -2013,11 +2015,13 @@ const VideoPlayback = forwardRef( syncWebcamMedia(); }, [syncWebcamMedia]); + // biome-ignore lint/correctness/useExhaustiveDependencies: The media path intentionally triggers source-specific state reset. useEffect(() => { setWebcamVideoDimensions(null); lastWebcamSyncTimeRef.current = null; }, [webcamVideoPath]); + // biome-ignore lint/correctness/useExhaustiveDependencies: The wallpaper identity intentionally resets media synchronization. useEffect(() => { lastBackgroundSyncTimeRef.current = null; }, [wallpaper]); @@ -2171,8 +2175,9 @@ const VideoPlayback = forwardRef( cursorContainerRef.current = null; videoSpriteRef.current = null; }; - }, [initializePixiRenderer, onError]); + }, [initializePixiRenderer, onError, syncPreviewMotionBlurQuality]); + // biome-ignore lint/correctness/useExhaustiveDependencies: A new media path must reset the persistent video element. useEffect(() => { const video = videoRef.current; if (!video) return; @@ -2273,7 +2278,7 @@ const VideoPlayback = forwardRef( videoSpriteRef.current = null; }; - }, [pixiReady, videoReady, onTimeUpdate, updateOverlayForRegion]); + }, [layoutVideoContent, onPlayStateChange, onTimeUpdate, pixiReady, videoReady]); useEffect(() => { if (!pixiReady || !videoReady) return; @@ -2650,7 +2655,6 @@ const VideoPlayback = forwardRef( }, [ pixiReady, videoReady, - clampFocusToStage, applyWebcamBubbleLayout, borderRadius, padding, diff --git a/src/components/video-editor/audio/useSourceAudioTrackSettings.ts b/src/components/video-editor/audio/useSourceAudioTrackSettings.ts index 9f126853..c1a96d4c 100644 --- a/src/components/video-editor/audio/useSourceAudioTrackSettings.ts +++ b/src/components/video-editor/audio/useSourceAudioTrackSettings.ts @@ -5,110 +5,115 @@ import type { } from "@/components/video-editor/audio/audioTypes"; interface UseSourceAudioTrackSettingsParams { - selectedClipId: string | null; - activeClipId: string | null; - sourceAudioTrackSettingsByClip: Record; - setSourceAudioTrackSettingsByClip: React.Dispatch< - React.SetStateAction> - >; - defaultSourceAudioTrackSettings: SourceAudioTrackSettings; - setDefaultSourceAudioTrackSettings: React.Dispatch>; + selectedClipId: string | null; + activeClipId: string | null; + sourceAudioTrackSettingsByClip: Record; + setSourceAudioTrackSettingsByClip: React.Dispatch< + React.SetStateAction> + >; + defaultSourceAudioTrackSettings: SourceAudioTrackSettings; + setDefaultSourceAudioTrackSettings: React.Dispatch< + React.SetStateAction + >; } export interface UseSourceAudioTrackSettingsResult { - sourceAudioTrackMeta: SourceAudioTrackMeta; - activeSourceAudioTrackSettings: SourceAudioTrackSettings; - selectedClipSourceAudioTrackSettings: SourceAudioTrackSettings; - getSourceAudioTrackSettingsForClip: (clipId: string | null) => SourceAudioTrackSettings; - onSourceAudioTracksMetaChange: (tracks: SourceAudioTrackMeta) => void; - onSelectedClipSourceAudioTrackVolumeChange: (id: string, volume: number) => void; - onSelectedClipSourceAudioTrackNormalizeChange: (id: string, normalize: boolean) => void; + sourceAudioTrackMeta: SourceAudioTrackMeta; + activeSourceAudioTrackSettings: SourceAudioTrackSettings; + selectedClipSourceAudioTrackSettings: SourceAudioTrackSettings; + getSourceAudioTrackSettingsForClip: (clipId: string | null) => SourceAudioTrackSettings; + onSourceAudioTracksMetaChange: (tracks: SourceAudioTrackMeta) => void; + onSelectedClipSourceAudioTrackVolumeChange: (id: string, volume: number) => void; + onSelectedClipSourceAudioTrackNormalizeChange: (id: string, normalize: boolean) => void; } function isSameTrackMeta(left: SourceAudioTrackMeta, right: SourceAudioTrackMeta): boolean { - if (left.length !== right.length) return false; - for (let index = 0; index < left.length; index += 1) { - const leftTrack = left[index]; - const rightTrack = right[index]; - if (!leftTrack || !rightTrack) return false; - if (leftTrack.id !== rightTrack.id || leftTrack.label !== rightTrack.label) { - return false; - } - } - return true; + if (left.length !== right.length) return false; + for (let index = 0; index < left.length; index += 1) { + const leftTrack = left[index]; + const rightTrack = right[index]; + if (!leftTrack || !rightTrack) return false; + if (leftTrack.id !== rightTrack.id || leftTrack.label !== rightTrack.label) { + return false; + } + } + return true; } export function useSourceAudioTrackSettings({ - selectedClipId, - activeClipId, - sourceAudioTrackSettingsByClip, - setSourceAudioTrackSettingsByClip, - defaultSourceAudioTrackSettings, - setDefaultSourceAudioTrackSettings, + selectedClipId, + activeClipId, + sourceAudioTrackSettingsByClip, + setSourceAudioTrackSettingsByClip, + defaultSourceAudioTrackSettings, + setDefaultSourceAudioTrackSettings, }: UseSourceAudioTrackSettingsParams): UseSourceAudioTrackSettingsResult { - const [sourceAudioTrackMeta, setSourceAudioTrackMeta] = useState([]); + const [sourceAudioTrackMeta, setSourceAudioTrackMeta] = useState([]); - const activeSourceAudioTrackSettings = useMemo(() => { - if (!activeClipId) { - return defaultSourceAudioTrackSettings; - } - return { - ...defaultSourceAudioTrackSettings, - ...(sourceAudioTrackSettingsByClip[activeClipId] ?? {}), - }; - }, [activeClipId, defaultSourceAudioTrackSettings, sourceAudioTrackSettingsByClip]); + const activeSourceAudioTrackSettings = useMemo(() => { + if (!activeClipId) { + return defaultSourceAudioTrackSettings; + } + return { + ...defaultSourceAudioTrackSettings, + ...(sourceAudioTrackSettingsByClip[activeClipId] ?? {}), + }; + }, [activeClipId, defaultSourceAudioTrackSettings, sourceAudioTrackSettingsByClip]); - const selectedClipSourceAudioTrackSettings = useMemo(() => { - if (!selectedClipId) { - return defaultSourceAudioTrackSettings; - } - return { - ...defaultSourceAudioTrackSettings, - ...(sourceAudioTrackSettingsByClip[selectedClipId] ?? {}), - }; - }, [defaultSourceAudioTrackSettings, selectedClipId, sourceAudioTrackSettingsByClip]); + const selectedClipSourceAudioTrackSettings = useMemo(() => { + if (!selectedClipId) { + return defaultSourceAudioTrackSettings; + } + return { + ...defaultSourceAudioTrackSettings, + ...(sourceAudioTrackSettingsByClip[selectedClipId] ?? {}), + }; + }, [defaultSourceAudioTrackSettings, selectedClipId, sourceAudioTrackSettingsByClip]); - const onSourceAudioTracksMetaChange = useCallback((tracks: SourceAudioTrackMeta) => { - setSourceAudioTrackMeta((prev) => (isSameTrackMeta(prev, tracks) ? prev : tracks)); - setDefaultSourceAudioTrackSettings((prev) => { - const next: SourceAudioTrackSettings = {}; - for (const track of tracks) { - next[track.id] = prev[track.id] ?? { volume: 1, normalize: false }; - } - const prevKeys = Object.keys(prev); - const nextKeys = Object.keys(next); - if (prevKeys.length !== nextKeys.length) { - return next; - } - for (const key of nextKeys) { - const prevSetting = prev[key]; - const nextSetting = next[key]; - if (!prevSetting || !nextSetting) { - return next; - } - if ( - prevSetting.volume !== nextSetting.volume || - prevSetting.normalize !== nextSetting.normalize - ) { - return next; - } - } - return prev; - }); - }, []); + const onSourceAudioTracksMetaChange = useCallback( + (tracks: SourceAudioTrackMeta) => { + setSourceAudioTrackMeta((prev) => (isSameTrackMeta(prev, tracks) ? prev : tracks)); + setDefaultSourceAudioTrackSettings((prev) => { + const next: SourceAudioTrackSettings = {}; + for (const track of tracks) { + next[track.id] = prev[track.id] ?? { volume: 1, normalize: false }; + } + const prevKeys = Object.keys(prev); + const nextKeys = Object.keys(next); + if (prevKeys.length !== nextKeys.length) { + return next; + } + for (const key of nextKeys) { + const prevSetting = prev[key]; + const nextSetting = next[key]; + if (!prevSetting || !nextSetting) { + return next; + } + if ( + prevSetting.volume !== nextSetting.volume || + prevSetting.normalize !== nextSetting.normalize + ) { + return next; + } + } + return prev; + }); + }, + [setDefaultSourceAudioTrackSettings], + ); - const getSourceAudioTrackSettingsForClip = useCallback( - (clipId: string | null): SourceAudioTrackSettings => { - if (!clipId) { - return defaultSourceAudioTrackSettings; - } - return { - ...defaultSourceAudioTrackSettings, - ...(sourceAudioTrackSettingsByClip[clipId] ?? {}), - }; - }, - [defaultSourceAudioTrackSettings, sourceAudioTrackSettingsByClip], - ); + const getSourceAudioTrackSettingsForClip = useCallback( + (clipId: string | null): SourceAudioTrackSettings => { + if (!clipId) { + return defaultSourceAudioTrackSettings; + } + return { + ...defaultSourceAudioTrackSettings, + ...(sourceAudioTrackSettingsByClip[clipId] ?? {}), + }; + }, + [defaultSourceAudioTrackSettings, sourceAudioTrackSettingsByClip], + ); const onSelectedClipSourceAudioTrackVolumeChange = useCallback( (id: string, volume: number) => { @@ -137,7 +142,7 @@ export function useSourceAudioTrackSettings({ }; }); }, - [defaultSourceAudioTrackSettings, selectedClipId], + [defaultSourceAudioTrackSettings, selectedClipId, setSourceAudioTrackSettingsByClip], ); const onSelectedClipSourceAudioTrackNormalizeChange = useCallback( @@ -161,16 +166,16 @@ export function useSourceAudioTrackSettings({ }; }); }, - [defaultSourceAudioTrackSettings, selectedClipId], + [defaultSourceAudioTrackSettings, selectedClipId, setSourceAudioTrackSettingsByClip], ); - return { - sourceAudioTrackMeta, - activeSourceAudioTrackSettings, - selectedClipSourceAudioTrackSettings, - getSourceAudioTrackSettingsForClip, - onSourceAudioTracksMetaChange, - onSelectedClipSourceAudioTrackVolumeChange, - onSelectedClipSourceAudioTrackNormalizeChange, - }; + return { + sourceAudioTrackMeta, + activeSourceAudioTrackSettings, + selectedClipSourceAudioTrackSettings, + getSourceAudioTrackSettingsForClip, + onSourceAudioTracksMetaChange, + onSelectedClipSourceAudioTrackVolumeChange, + onSelectedClipSourceAudioTrackNormalizeChange, + }; } diff --git a/src/components/video-editor/timeline/components/waveform/AudioWaveform.tsx b/src/components/video-editor/timeline/components/waveform/AudioWaveform.tsx index 1b192651..2a5300f1 100644 --- a/src/components/video-editor/timeline/components/waveform/AudioWaveform.tsx +++ b/src/components/video-editor/timeline/components/waveform/AudioWaveform.tsx @@ -44,6 +44,7 @@ function AudioWaveformComponent({ } }, []); + // biome-ignore lint/correctness/useExhaustiveDependencies: resizeKey intentionally redraws the canvas after ResizeObserver notifications. useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; @@ -79,15 +80,15 @@ function AudioWaveformComponent({ const visibleStartMs = segmentStartMs ?? range.start; const visibleEndMs = segmentEndMs ?? range.end; const visibleDurationMs = visibleEndMs - visibleStartMs; - + if (visibleDurationMs <= 0) return; const midY = height / 2; ctx.beginPath(); - + for (let px = 0; px < width; px++) { const t = visibleStartMs + (px / width) * visibleDurationMs; - + // If the timeline time is beyond the actual audio duration, we draw nothing (flat line) if (t < 0 || t > durationMs) continue; @@ -95,12 +96,12 @@ function AudioWaveformComponent({ const leftIndex = Math.floor(exactIndex); const rightIndex = Math.min(peakData.length - 1, leftIndex + 1); const mix = exactIndex - leftIndex; - + let amplitude = peakData[leftIndex] * (1 - mix) + peakData[rightIndex] * mix; - + if (normalize) amplitude = Math.sqrt(Math.max(0, amplitude)); amplitude = Math.max(0, Math.min(1, amplitude * gain)); - + const barHeight = amplitude * midY * 0.85; ctx.moveTo(px, midY - barHeight);