diff --git a/src/components/detail/PersonalSection.tsx b/src/components/detail/PersonalSection.tsx index e8b2f11..daa2e41 100644 --- a/src/components/detail/PersonalSection.tsx +++ b/src/components/detail/PersonalSection.tsx @@ -32,16 +32,14 @@ export default function PersonalSection({ itemId, showRewatchToggle }: Props) { // notes correctly without leaking state). useEffect(() => { setLocalNote(entry?.note || '') - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [itemId]) + }, [itemId, entry?.note]) // Debounce writes by 400ms. useEffect(() => { if (localNote === (entry?.note || '')) return const id = setTimeout(() => setNote(itemId, localNote), 400) return () => clearTimeout(id) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [localNote]) + }, [localNote, entry?.note, itemId, setNote]) const rating = entry?.rating ?? 0 const rewatchCount = entry?.rewatchCount ?? 0 diff --git a/src/components/discover/Roulette.tsx b/src/components/discover/Roulette.tsx index 58c62b0..2488e8e 100644 --- a/src/components/discover/Roulette.tsx +++ b/src/components/discover/Roulette.tsx @@ -95,21 +95,21 @@ function RouletteModal({ const pick = pool[pickIndex % Math.max(1, pool.length)] || null - function spin() { + const spin = useCallback(() => { if (pool.length < 2) return let next = pickIndex // Avoid landing on the same pick consecutively. while (next === pickIndex) next = Math.floor(Math.random() * pool.length) setPickIndex(next) setSpinNonce(n => n + 1) - } + }, [pool.length, pickIndex]) - function open() { + const open = useCallback(() => { if (!pick) return const mediaType = pick.media_type === 'tv' || pick.first_air_date ? 'tv' : 'movie' navigate(`/item/tmdb-${mediaType}-${pick.id}`) onClose() - } + }, [pick, navigate, onClose]) useEffect(() => { function onKey(e: KeyboardEvent) { @@ -121,8 +121,7 @@ function RouletteModal({ } window.addEventListener('keydown', onKey) return () => window.removeEventListener('keydown', onKey) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [pickIndex, pool.length]) + }, [pickIndex, pool.length, onClose, spin]) const title = pick?.title || pick?.name || '' const year = (pick?.release_date || pick?.first_air_date || '').slice(0, 4) diff --git a/src/components/request/RequestModal.tsx b/src/components/request/RequestModal.tsx index 95f78e3..2f14a99 100644 --- a/src/components/request/RequestModal.tsx +++ b/src/components/request/RequestModal.tsx @@ -113,8 +113,7 @@ export default function RequestModal({ open, onClose, tmdbId, kind, tmdbData }: const init: Record = {} for (const s of tvSeasons) init[s.season_number ?? 0] = true setSeasonsRequested(init) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [tmdbData?.id]) + }, [kind, tvSeasons]) // Esc closes. useEffect(() => { diff --git a/src/hooks/use-playback-reporting.ts b/src/hooks/use-playback-reporting.ts index b5583a9..2992311 100644 --- a/src/hooks/use-playback-reporting.ts +++ b/src/hooks/use-playback-reporting.ts @@ -133,6 +133,5 @@ export function usePlaybackReporting(args: Args) { debugLog('[playback] stop report failed', err), ) } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [itemId]) + }, [itemId, mediaSourceId, startTimeTicks, progressRef]) } diff --git a/src/hooks/use-prebuffer.ts b/src/hooks/use-prebuffer.ts index 40c459c..be29e87 100644 --- a/src/hooks/use-prebuffer.ts +++ b/src/hooks/use-prebuffer.ts @@ -76,6 +76,5 @@ export function usePrebuffer(item: BaseItemDto | null | undefined, armed: boolea }).catch(() => { /* warm-only; ignore */ }) return () => { cancelled = true } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [armed, item?.Id]) + }, [armed, item?.Id, item?.Type, qc]) } diff --git a/src/pages/LibraryPage.tsx b/src/pages/LibraryPage.tsx index 869d1a3..f67f290 100644 --- a/src/pages/LibraryPage.tsx +++ b/src/pages/LibraryPage.tsx @@ -92,8 +92,7 @@ export default function LibraryPage({ type }: Props) { }, [selected.size, clearSelection]) useEffect(() => { return () => clearSelection() - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + }, [clearSelection]) const { data: libraries } = useLibraries() const collectionType = COLLECTION_TYPE_MAP[type] diff --git a/src/pages/PlayerPage.tsx b/src/pages/PlayerPage.tsx index 2bad6bb..96c5984 100644 --- a/src/pages/PlayerPage.tsx +++ b/src/pages/PlayerPage.tsx @@ -332,18 +332,22 @@ export default function PlayerPage() { /* Resume prompt: show on first mount when there's a saved position * past the threshold AND the user wants the prompt AND the URL didn't - * already specify ?resume=true (queue navigation path). */ + * already specify ?resume=true (queue navigation path). + * Intentionally scoped to item?.Id only - we only want to evaluate + * the resume condition once per item, not re-trigger when item data + * refreshes or prefs change mid-playback. */ + const resumePromptShownRef = useRef(null) useEffect(() => { - if (!item) return + if (!item || resumePromptShownRef.current === item.Id) return const pos = Number(item.UserData?.PlaybackPositionTicks ?? 0) const thresholdSec = usePreferencesStore.getState().resumeThresholdSec ?? 5 const threshold = thresholdSec * 10_000_000 const fromQueue = searchParams.get('resume') === 'true' if (showResumePromptPref && !fromQueue && pos > threshold) { setResumePromptOpen(true) + resumePromptShownRef.current = item.Id } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [item?.Id]) + }, [item?.Id, searchParams, showResumePromptPref]) /* Auto-rewatch counter: when an already-played item starts playing * again, record the rewatch. We trip it at most once per item-mount @@ -358,8 +362,7 @@ export default function PlayerPage() { if (rewatchedItemIdRef.current === item.Id) return rewatchedItemIdRef.current = item.Id usePersonalData.getState().incrementRewatch(item.Id) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [item?.Id]) + }, [item]) /* Auto-recap trigger: decide once per item. Recap card waits for the * resume prompt (if any) to resolve before appearing. */ @@ -376,8 +379,7 @@ export default function PlayerPage() { } else { setRecapPending(false) } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [item?.Id, recapTrigger.shouldShow]) + }, [item?.Id, recapTrigger.shouldShow, showRecapCardPref, item]) useEffect(() => { if (recapPending && !resumePromptOpen) { setRecapCardOpen(true) @@ -429,8 +431,7 @@ export default function PlayerPage() { qc.removeQueries({ queryKey: ['jellyfin', 'episodes', evictId], exact: false }) } catch { /* ignore */ } } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [id]) + }, [id, qc]) /* Aggressive teardown on player unmount. * @@ -638,8 +639,7 @@ export default function PlayerPage() { p.currentTime = target if (seriesId) recordSkippedSeconds(seriesId, 'credits', target - from) } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentMarker?.type, currentMarker?.startSec, skipIntros, skipCredits]) + }, [currentMarker?.type, currentMarker?.startSec, skipIntros, skipCredits, duration, seriesId]) /* Imperatively switch the active subtitle track. We use 'hidden' rather * than 'showing' so the browser doesn't paint its own caption UI over our @@ -720,8 +720,7 @@ export default function PlayerPage() { // default: prefer a language match, then default-flagged, then off const match = pickSubtitle(subs, subtitleLanguage) setSubtitleIndex(match?.Index ?? null) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [id, mediaSourceId, subtitleStreams.length, subtitleMode, subtitleLanguage]) + }, [id, mediaSourceId, subtitleStreams.length, subtitleMode, subtitleLanguage, item]) /* ── Playback reporting ──────────────────────────────────── */ const playSessionId = playbackInfo?.PlaySessionId || undefined diff --git a/src/pages/library/modals.tsx b/src/pages/library/modals.tsx index df4e992..dffdfee 100644 --- a/src/pages/library/modals.tsx +++ b/src/pages/library/modals.tsx @@ -110,8 +110,7 @@ export function SurpriseMeModal({ } else if (!open) { setPick(null) } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [open, items.length]) + }, [open, items]) return ( diff --git a/src/pages/settings/sections/Shortcuts.tsx b/src/pages/settings/sections/Shortcuts.tsx index 661d502..93a956a 100644 --- a/src/pages/settings/sections/Shortcuts.tsx +++ b/src/pages/settings/sections/Shortcuts.tsx @@ -117,8 +117,7 @@ export function ShortcutsSection() { } window.addEventListener('keydown', onKey, true) return () => window.removeEventListener('keydown', onKey, true) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [capturingId, overrides]) + }, [capturingId, overrides, commit]) return (