Compare commits
2 Commits
6f86750a99
...
6c2443fa2c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c2443fa2c | ||
|
|
5d4d11512d |
@@ -16,6 +16,8 @@ interface Props {
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const HISTORY_CAP = 100
|
||||
|
||||
function pickRandom(items: DoomScrollItem[], excludeRecent: DoomScrollItem[]): DoomScrollItem {
|
||||
const excludeCount = Math.min(excludeRecent.length, items.length - 1)
|
||||
const recentUrls = new Set(excludeRecent.slice(-excludeCount).map((i) => i.url))
|
||||
@@ -55,9 +57,9 @@ export default function DoomScrollView({ items, videoContext = 'mixed', onClose
|
||||
const next = pickRandom(items, history)
|
||||
setHistory((h) => {
|
||||
const updated = [...h, next]
|
||||
return updated.length > 100 ? updated.slice(-100) : updated
|
||||
return updated.length > HISTORY_CAP ? updated.slice(-HISTORY_CAP) : updated
|
||||
})
|
||||
return idx + 1
|
||||
return Math.min(idx + 1, HISTORY_CAP - 1)
|
||||
})
|
||||
}, [items, history])
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import ImageLightbox from './ImageLightbox'
|
||||
import TagSelector from '@/components/tags/TagSelector'
|
||||
import FilterPanel from '@/components/FilterPanel'
|
||||
import DoomScrollView, { type DoomScrollItem } from '@/components/DoomScrollView'
|
||||
import { isBrowserPlayable } from '@/lib/browser-media'
|
||||
|
||||
interface Props {
|
||||
libraryId: string
|
||||
@@ -200,7 +201,7 @@ export default function MixedView({ libraryId, initialPath }: Props) {
|
||||
// When filters are active, doom scroll uses filteredEntries (already filtered by search/tags).
|
||||
// When no filters, doom scroll uses the full recursiveEntries.
|
||||
const doomScrollItems: DoomScrollItem[] = (filtersActive ? filteredEntries : recursiveEntries)
|
||||
.filter((e) => e.type === 'file' && (e.mediaType === 'video' || e.mediaType === 'image') && e.url)
|
||||
.filter((e) => e.type === 'file' && (e.mediaType === 'video' || e.mediaType === 'image') && e.url && isBrowserPlayable(e.name))
|
||||
.map((e) => ({ url: e.url!, name: e.name, mediaType: e.mediaType as 'video' | 'image' }))
|
||||
|
||||
return (
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { Movie } from '@/types'
|
||||
import MovieDetailModal from './MovieDetailModal'
|
||||
import FilterPanel from '@/components/FilterPanel'
|
||||
import DoomScrollView, { type DoomScrollItem } from '@/components/DoomScrollView'
|
||||
import { isBrowserPlayable } from '@/lib/browser-media'
|
||||
|
||||
interface Props {
|
||||
libraryId: string
|
||||
@@ -74,7 +75,7 @@ export default function MoviesView({ libraryId }: Props) {
|
||||
|
||||
const handleDoomScroll = () => {
|
||||
// Use filtered movies — respects any active search/tag filters automatically
|
||||
const items: DoomScrollItem[] = filtered.map((m) => ({
|
||||
const items: DoomScrollItem[] = filtered.filter((m) => isBrowserPlayable(m.videoPath)).map((m) => ({
|
||||
url: `/api/file?libraryId=${encodeURIComponent(libraryId)}&path=${encodeURIComponent(m.videoPath)}`,
|
||||
name: m.title,
|
||||
mediaType: 'video' as const,
|
||||
|
||||
@@ -8,6 +8,7 @@ import VideoPlayerModal from '@/components/mixed/VideoPlayerModal'
|
||||
import TagSelector from '@/components/tags/TagSelector'
|
||||
import EpisodeCard from './EpisodeCard'
|
||||
import DoomScrollView, { type DoomScrollItem } from '@/components/DoomScrollView'
|
||||
import { isBrowserPlayable } from '@/lib/browser-media'
|
||||
|
||||
interface Props {
|
||||
libraryId: string
|
||||
@@ -184,7 +185,7 @@ export default function TvView({ libraryId }: Props) {
|
||||
return seasonEps.flat()
|
||||
})
|
||||
)
|
||||
items = episodeLists.flat().map((ep) => ({
|
||||
items = episodeLists.flat().filter((ep) => isBrowserPlayable(ep.videoPath)).map((ep) => ({
|
||||
url: `/api/file?libraryId=${encodeURIComponent(libraryId)}&path=${encodeURIComponent(ep.videoPath)}`,
|
||||
name: ep.title,
|
||||
mediaType: 'video' as const,
|
||||
@@ -209,7 +210,7 @@ export default function TvView({ libraryId }: Props) {
|
||||
return seasonEps.flat()
|
||||
})
|
||||
)
|
||||
items = episodeLists.flat().map((ep) => ({
|
||||
items = episodeLists.flat().filter((ep) => isBrowserPlayable(ep.videoPath)).map((ep) => ({
|
||||
url: `/api/file?libraryId=${encodeURIComponent(libraryId)}&path=${encodeURIComponent(ep.videoPath)}`,
|
||||
name: ep.title,
|
||||
mediaType: 'video' as const,
|
||||
|
||||
19
src/lib/browser-media.ts
Normal file
19
src/lib/browser-media.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Browser-native media formats safe for use in <video> and <img> elements.
|
||||
* Kept separate from the broader scanner extension sets (media-utils.ts, files.ts)
|
||||
* which include server-side-only formats like .mkv, .avi, .tiff, etc.
|
||||
*/
|
||||
|
||||
export const BROWSER_VIDEO_EXTENSIONS = new Set(['.mp4', '.webm', '.mov', '.m4v'])
|
||||
export const BROWSER_IMAGE_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'])
|
||||
|
||||
/**
|
||||
* Returns true if the file at `filename` (or path) has a browser-playable extension.
|
||||
* Uses lastIndexOf to avoid importing the Node `path` module in client components.
|
||||
*/
|
||||
export function isBrowserPlayable(filename: string): boolean {
|
||||
const dot = filename.lastIndexOf('.')
|
||||
if (dot === -1) return false
|
||||
const ext = filename.slice(dot).toLowerCase()
|
||||
return BROWSER_VIDEO_EXTENSIONS.has(ext) || BROWSER_IMAGE_EXTENSIONS.has(ext)
|
||||
}
|
||||
Reference in New Issue
Block a user