performance-stability #14
@@ -73,9 +73,20 @@ export default function DoomScrollView({ items, videoContext = 'mixed', onClose
|
|||||||
setTimeout(() => { cooldownRef.current = false }, 300)
|
setTimeout(() => { cooldownRef.current = false }, 300)
|
||||||
}, [goNext, goPrev])
|
}, [goNext, goPrev])
|
||||||
|
|
||||||
// Reset pause when switching items
|
// On navigation to a new item: reset pause state and start playing.
|
||||||
|
// Merging the reset + play() into one effect prevents the old isPaused=true
|
||||||
|
// value from calling pause() on the freshly-mounted video element before the
|
||||||
|
// reset fires. If autoplay is blocked by browser policy (common when unmuted),
|
||||||
|
// fall back to muted and retry — the user can unmute manually afterward.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsPaused(false)
|
setIsPaused(false)
|
||||||
|
if (!videoRef.current) return
|
||||||
|
videoRef.current.play().catch(() => {
|
||||||
|
if (!videoRef.current) return
|
||||||
|
videoRef.current.muted = true
|
||||||
|
setLocalMuted(true)
|
||||||
|
videoRef.current.play().catch(() => {})
|
||||||
|
})
|
||||||
}, [current?.url])
|
}, [current?.url])
|
||||||
|
|
||||||
// Sync muted imperatively — React's muted prop is not reliable
|
// Sync muted imperatively — React's muted prop is not reliable
|
||||||
@@ -83,7 +94,8 @@ export default function DoomScrollView({ items, videoContext = 'mixed', onClose
|
|||||||
if (videoRef.current) videoRef.current.muted = localMuted
|
if (videoRef.current) videoRef.current.muted = localMuted
|
||||||
}, [localMuted, current?.url])
|
}, [localMuted, current?.url])
|
||||||
|
|
||||||
// Sync play/pause imperatively
|
// Sync play/pause imperatively for user-initiated pause/unpause only.
|
||||||
|
// current?.url is intentionally excluded: navigation is handled above.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!videoRef.current) return
|
if (!videoRef.current) return
|
||||||
if (isPaused) {
|
if (isPaused) {
|
||||||
@@ -91,7 +103,7 @@ export default function DoomScrollView({ items, videoContext = 'mixed', onClose
|
|||||||
} else {
|
} else {
|
||||||
videoRef.current.play().catch(() => {})
|
videoRef.current.play().catch(() => {})
|
||||||
}
|
}
|
||||||
}, [isPaused, current?.url])
|
}, [isPaused])
|
||||||
|
|
||||||
// Auto-play timer — resets on each new item, pause, enable/disable, or interval change
|
// Auto-play timer — resets on each new item, pause, enable/disable, or interval change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useRef, useState, useCallback } from 'react'
|
import { useEffect, useRef, useState, useCallback } from 'react'
|
||||||
import type { TvSeries, TvSeason, TvEpisode } from '@/types'
|
import type { TvSeries, TvSeason, TvEpisode } from '@/types'
|
||||||
import type { DirectoryListing } from '@/types'
|
|
||||||
import FilterPanel from '@/components/FilterPanel'
|
import FilterPanel from '@/components/FilterPanel'
|
||||||
import VideoPlayerModal from '@/components/mixed/VideoPlayerModal'
|
import VideoPlayerModal from '@/components/mixed/VideoPlayerModal'
|
||||||
import TagSelector from '@/components/tags/TagSelector'
|
import TagSelector from '@/components/tags/TagSelector'
|
||||||
@@ -190,13 +190,30 @@ export default function TvView({ libraryId }: Props) {
|
|||||||
mediaType: 'video' as const,
|
mediaType: 'video' as const,
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
// No filters — use full recursive browse
|
// No filters — fetch all episodes via the TV API hierarchy
|
||||||
const data: DirectoryListing = await fetch(
|
const allSeries: TvSeries[] = await fetch(
|
||||||
`/api/browse?libraryId=${encodeURIComponent(libraryId)}&path=&recursive=true`
|
`/api/tv?libraryId=${encodeURIComponent(libraryId)}`
|
||||||
).then((r) => r.json())
|
).then((r) => r.json())
|
||||||
items = data.entries
|
const episodeLists = await Promise.all(
|
||||||
.filter((e) => e.type === 'file' && e.mediaType === 'video' && e.url)
|
allSeries.map(async (s) => {
|
||||||
.map((e) => ({ url: e.url!, name: e.name, mediaType: 'video' as const }))
|
const seasons: TvSeason[] = await fetch(
|
||||||
|
`/api/tv?libraryId=${encodeURIComponent(libraryId)}&seriesId=${encodeURIComponent(s.id)}`
|
||||||
|
).then((r) => r.json())
|
||||||
|
const seasonEps = await Promise.all(
|
||||||
|
seasons.map((season) =>
|
||||||
|
fetch(
|
||||||
|
`/api/tv?libraryId=${encodeURIComponent(libraryId)}&seriesId=${encodeURIComponent(s.id)}&seasonId=${encodeURIComponent(season.id)}`
|
||||||
|
).then((r) => r.json() as Promise<TvEpisode[]>)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return seasonEps.flat()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
items = episodeLists.flat().map((ep) => ({
|
||||||
|
url: `/api/file?libraryId=${encodeURIComponent(libraryId)}&path=${encodeURIComponent(ep.videoPath)}`,
|
||||||
|
name: ep.title,
|
||||||
|
mediaType: 'video' as const,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
setDoomScrollItems(items)
|
setDoomScrollItems(items)
|
||||||
setDoomScrollActive(true)
|
setDoomScrollActive(true)
|
||||||
|
|||||||
Reference in New Issue
Block a user