From 92ca90dbfe34a6439d75ed6e7ec879d9f8e51945 Mon Sep 17 00:00:00 2001 From: KhooLy <73142442+KhooLy@users.noreply.github.com> Date: Thu, 2 Jul 2026 17:15:54 +0300 Subject: [PATCH] perf: cut player overlay re-renders on seekbar hover and skip progress, preload next hero slide --- src/components/HeroSection.tsx | 9 ++ src/components/ReactPlayerOverlay.tsx | 169 ++++++++++++++------------ 2 files changed, 100 insertions(+), 78 deletions(-) diff --git a/src/components/HeroSection.tsx b/src/components/HeroSection.tsx index f073dc7..bd6ab40 100644 --- a/src/components/HeroSection.tsx +++ b/src/components/HeroSection.tsx @@ -88,6 +88,15 @@ export const HeroSection = React.memo(function HeroSection({ meta, slides, onPla return () => window.clearInterval(id); }, [canSlide, items.length, isActive]); + useEffect(() => { + if (!canSlide) return; + const next = items[(activeIndex + 1) % items.length]; + if (!next) return; + const nextBg = (preferSeasonPosters ? seasonPosterUrl(next) : undefined) ?? next.background ?? next.poster; + if (nextBg) { const img = new Image(); img.src = nextBg; } + if (next.logo) { const img = new Image(); img.src = next.logo; } + }, [canSlide, items, activeIndex, preferSeasonPosters]); + const goTo = (index: number) => { if (!canSlide) return; slideToIndex(index); diff --git a/src/components/ReactPlayerOverlay.tsx b/src/components/ReactPlayerOverlay.tsx index 7bae5ca..53a8026 100644 --- a/src/components/ReactPlayerOverlay.tsx +++ b/src/components/ReactPlayerOverlay.tsx @@ -120,7 +120,6 @@ export function ReactPlayerOverlay({ closePlayer, onFirstFrame, initialTitle, in const [episodes, setEpisodes] = useState([]); const [showEpisodePanel, setShowEpisodePanel] = useState(false); const [activeSkip, setActiveSkip] = useState(null); - const [skipProgress, setSkipProgress] = useState(0); const [showNextEpCard, setShowNextEpCard] = useState(false); const [trackPopover, setTrackPopover] = useState<'audio' | 'sub' | 'speed' | null>(null); const [miniPlayerActive, setMiniPlayerActive] = useState(false); @@ -139,9 +138,6 @@ export function ReactPlayerOverlay({ closePlayer, onFirstFrame, initialTitle, in const [audioTracks, setAudioTracks] = useState([]); const [subTracks, setSubTracks] = useState([]); const [feedback, setFeedback] = useState(null); - const [seekPreview, setSeekPreview] = useState<{ x: number; time: number; chapter: string | null } | null>(null); - const [seekThumbImg, setSeekThumbImg] = useState(null); - const seekThumbTimerRef = useRef | null>(null); const [showVolumeSlider, setShowVolumeSlider] = useState(false); const volumeHideTimer = useRef | null>(null); const [showSeekOverlay, setShowSeekOverlay] = useState(false); @@ -158,6 +154,7 @@ export function ReactPlayerOverlay({ closePlayer, onFirstFrame, initialTitle, in const contextMenuRef = useRef(null); const segFillRefs = useRef<(HTMLDivElement | null)[]>([]); const segBufRefs = useRef<(HTMLDivElement | null)[]>([]); + const skipFillRef = useRef(null); const chapterSegmentsRef = useRef | null>(null); const chaptersRef = useRef([]); @@ -384,9 +381,10 @@ export function ReactPlayerOverlay({ closePlayer, onFirstFrame, initialTitle, in activeSkipKeyRef.current = newSkipKey; setActiveSkip(seg ? { label: skipLabelForType(seg.type), startMs: seg.startTime, endMs: seg.endTime } : null); } - if (seg) { + if (seg && skipFillRef.current) { const span = seg.endTime - seg.startTime; - setSkipProgress(span > 0 ? Math.min(1, Math.max(0, (posMs - seg.startTime) / span)) : 0); + const skipFrac = span > 0 ? Math.min(1, Math.max(0, (posMs - seg.startTime) / span)) : 0; + skipFillRef.current.style.width = `${(skipFrac * 100).toFixed(2)}%`; } if (dur > 0 && nextEpSubtitle) { @@ -616,49 +614,6 @@ export function ReactPlayerOverlay({ closePlayer, onFirstFrame, initialTitle, in applyFills(frac); }, [fractionFromSeekbarEvent, resetActivity, applyFills]); - const onSeekMouseMove = useCallback((e: React.MouseEvent) => { - const bar = seekbarRef.current; - if (!bar) return; - const rect = bar.getBoundingClientRect(); - const frac = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); - const previewTime = frac * durRef.current; - const chaps = chaptersRef.current; - let chapterName: string | null = null; - if (chaps.length > 0) { - let found = chaps[0].title; - for (const ch of chaps) { - if (ch.startMs / 1000 <= previewTime) found = ch.title; - else break; - } - chapterName = found || null; - } - setSeekPreview({ x: e.clientX - rect.left, time: previewTime, chapter: chapterName }); - if (isDraggingRef.current) { - dragPosRef.current = frac; - applyFills(frac); - } - }, [applyFills]); - - const onSeekMouseLeave = useCallback(() => { - setSeekPreview(null); - setSeekThumbImg(null); - if (seekThumbTimerRef.current) { clearTimeout(seekThumbTimerRef.current); seekThumbTimerRef.current = null; } - }, []); - - useEffect(() => { - if (!seekPreview) { setSeekThumbImg(null); return; } - const time = seekPreview.time; - if (seekThumbTimerRef.current) clearTimeout(seekThumbTimerRef.current); - seekThumbTimerRef.current = setTimeout(() => { - invoke('player_get_seek_thumbnail', { timePos: time }) - .then((img) => { if (img) setSeekThumbImg(img); }) - .catch((err) => console.error('player_get_seek_thumbnail failed', err)); - }, 120); - return () => { - if (seekThumbTimerRef.current) clearTimeout(seekThumbTimerRef.current); - }; - }, [seekPreview?.time]); - useEffect(() => { const onMouseUp = () => { if (!isDraggingRef.current) return; @@ -838,8 +793,6 @@ export function ReactPlayerOverlay({ closePlayer, onFirstFrame, initialTitle, in : null; chapterSegmentsRef.current = chapterSegments; - const seekHovered = seekPreview !== null; - if (miniPlayerActive) { return (
@@ -987,7 +944,7 @@ export function ReactPlayerOverlay({ closePlayer, onFirstFrame, initialTitle, in > {activeSkip.label} -
+
)} @@ -1083,51 +1040,33 @@ export function ReactPlayerOverlay({ closePlayer, onFirstFrame, initialTitle, in >
-
+
{chapterSegments ? ( chapterSegments.map((seg, i) => ( -
+
{ segBufRefs.current[i] = el; }} style={{ position: 'absolute', left: 0, top: 0, width: '0%', height: '100%', background: 'rgba(255,255,255,0.5)' }} />
{ segFillRefs.current[i] = el; }} style={{ position: 'absolute', left: 0, top: 0, width: '0%', height: '100%', background: '#E53935' }} />
)) ) : ( <> -
-
+
+
)}
- {seekPreview && ( -
- {seekThumbImg && ( -
- -
- )} -
- {seekPreview.chapter && ( - - {seekPreview.chapter} - - )} - - {fmtTime(seekPreview.time)} - -
-
- )} +
@@ -1213,6 +1152,80 @@ export function ReactPlayerOverlay({ closePlayer, onFirstFrame, initialTitle, in ); } +function SeekPreview({ barRef, durRef, chaptersRef }: { + barRef: React.RefObject; + durRef: React.MutableRefObject; + chaptersRef: React.MutableRefObject; +}) { + const [preview, setPreview] = useState<{ x: number; time: number; chapter: string | null } | null>(null); + const [thumbImg, setThumbImg] = useState(null); + const thumbTimerRef = useRef | null>(null); + + useEffect(() => { + const bar = barRef.current; + if (!bar) return; + const onMove = (e: MouseEvent) => { + const rect = bar.getBoundingClientRect(); + const frac = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); + const previewTime = frac * durRef.current; + const chaps = chaptersRef.current; + let chapterName: string | null = null; + if (chaps.length > 0) { + let found = chaps[0].title; + for (const ch of chaps) { + if (ch.startMs / 1000 <= previewTime) found = ch.title; + else break; + } + chapterName = found || null; + } + setPreview({ x: e.clientX - rect.left, time: previewTime, chapter: chapterName }); + }; + const onLeave = () => setPreview(null); + bar.addEventListener('mousemove', onMove); + bar.addEventListener('mouseleave', onLeave); + return () => { + bar.removeEventListener('mousemove', onMove); + bar.removeEventListener('mouseleave', onLeave); + }; + }, [barRef, durRef, chaptersRef]); + + useEffect(() => { + if (!preview) { setThumbImg(null); return; } + const time = preview.time; + if (thumbTimerRef.current) clearTimeout(thumbTimerRef.current); + thumbTimerRef.current = setTimeout(() => { + invoke('player_get_seek_thumbnail', { timePos: time }) + .then((img) => { if (img) setThumbImg(img); }) + .catch((err) => console.error('player_get_seek_thumbnail failed', err)); + }, 120); + return () => { + if (thumbTimerRef.current) clearTimeout(thumbTimerRef.current); + }; + }, [preview?.time]); + + if (!preview) return null; + + return ( +
+ {thumbImg && ( +
+ +
+ )} +
+ {preview.chapter && ( + + {preview.chapter} + + )} + + {fmtTime(preview.time)} + +
+
+ ); +} + const styles = { iconBtn: { background: 'none',