'use client' import { useEffect, useState, useCallback, useRef } from 'react' import type { DirectoryListing, FileEntry } from '@/types' import VideoPlayerModal from './VideoPlayerModal' import ImageLightbox from './ImageLightbox' import TagSelector from '@/components/tags/TagSelector' import FilterPanel from '@/components/FilterPanel' interface Props { libraryId: string initialPath: string } type ModalState = | { type: 'video'; url: string; name: string } | { type: 'image'; url: string; name: string } | null type TagPanelState = { entry: FileEntry; mediaKey: string } | null export default function MixedView({ libraryId, initialPath }: Props) { const [currentPath, setCurrentPath] = useState(initialPath) const [listing, setListing] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [modal, setModal] = useState(null) const [tagPanel, setTagPanel] = useState(null) const [search, setSearch] = useState('') const [selectedTagIds, setSelectedTagIds] = useState>(new Set()) const [assignments, setAssignments] = useState>({}) const [filterRefreshKey, setFilterRefreshKey] = useState(0) const toggleTag = (tagId: string) => setSelectedTagIds((prev) => { const next = new Set(prev) next.has(tagId) ? next.delete(tagId) : next.add(tagId) return next }) const loadPath = useCallback( (path: string) => { setLoading(true) setError(null) fetch( `/api/browse?libraryId=${encodeURIComponent(libraryId)}&path=${encodeURIComponent(path)}` ) .then((r) => r.json()) .then((data: DirectoryListing) => { setListing(data) setCurrentPath(path) setLoading(false) }) .catch(() => { setError('Failed to load directory') setLoading(false) }) }, [libraryId] ) useEffect(() => { loadPath(initialPath) }, [loadPath, initialPath]) const fetchAssignments = useCallback(() => { fetch(`/api/tags/library-assignments?libraryId=${encodeURIComponent(libraryId)}`) .then((r) => r.json()) .then(setAssignments) .catch(() => {}) }, [libraryId]) useEffect(() => { fetchAssignments() }, [fetchAssignments]) const handleEntry = (entry: FileEntry) => { if (entry.type === 'directory') { const newPath = currentPath ? `${currentPath}/${entry.name}` : entry.name loadPath(newPath) return } if (!entry.url) return if (entry.mediaType === 'video') { setModal({ type: 'video', url: entry.url, name: entry.name }) } else if (entry.mediaType === 'image') { setModal({ type: 'image', url: entry.url, name: entry.name }) } else { // Download other file types window.open(entry.url, '_blank') } } const handleTagEntry = (entry: FileEntry) => { const relativePath = currentPath ? `${currentPath}/${entry.name}` : entry.name const mediaKey = `${libraryId}:${encodeURIComponent(relativePath)}` setTagPanel({ entry, mediaKey }) } const navigateUp = () => { const parts = currentPath.split('/').filter(Boolean) parts.pop() loadPath(parts.join('/')) } // Build breadcrumb segments const breadcrumbs = currentPath ? currentPath.split('/').filter(Boolean) : [] const mediaKeyFor = (entry: FileEntry) => { const rel = currentPath ? `${currentPath}/${entry.name}` : entry.name return `${libraryId}:${encodeURIComponent(rel)}` } const filteredEntries = (listing?.entries ?? []).filter((entry) => { if (search && !entry.name.toLowerCase().includes(search.toLowerCase())) return false if (selectedTagIds.size > 0 && entry.type !== 'directory') { const entryTags = assignments[mediaKeyFor(entry)] ?? [] if (![...selectedTagIds].every((id) => entryTags.includes(id))) return false } return true }) return (
{/* Breadcrumb */} {loading && } {error && (
{error}
)} {!loading && !error && listing && ( <> {filteredEntries.length === 0 ? (
This folder is empty.
) : (
{/* Up button */} {breadcrumbs.length > 0 && ( )} {filteredEntries.map((entry) => ( ))}
)} )} {modal?.type === 'video' && ( setModal(null)} /> )} {modal?.type === 'image' && ( setModal(null)} /> )} {/* Tag panel */} {tagPanel && (
{ if (e.target === e.currentTarget) setTagPanel(null) }} >

Tags

{tagPanel.entry.name}

{ setFilterRefreshKey((k) => k + 1); fetchAssignments() }} />
)}
) } function EntryTile({ entry, onOpen, onTag }: { entry: FileEntry; onOpen: (e: FileEntry) => void; onTag: (e: FileEntry) => void }) { type ImgState = 'loading' | 'loaded' | 'error' const [imgState, setImgState] = useState( entry.thumbnailUrl ? 'loading' : 'error' ) // Reset image state when the entry changes (e.g. navigating to a new folder) const prevUrl = useRef(entry.thumbnailUrl) if (prevUrl.current !== entry.thumbnailUrl) { prevUrl.current = entry.thumbnailUrl if (entry.thumbnailUrl) setImgState('loading') else setImgState('error') } const isDir = entry.type === 'directory' const isVideo = entry.mediaType === 'video' const showThumbnail = !isDir && imgState !== 'error' && entry.thumbnailUrl // Icon shown when no thumbnail available or while loading const icon = isDir ? '📁' : isVideo ? '▶' : entry.mediaType === 'image' ? '🖼' : '📄' return (
onOpen(entry)} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onOpen(entry) } }} className="group relative flex flex-col rounded-xl border overflow-hidden text-xs transition-all cursor-pointer" style={{ borderColor: 'var(--border)', backgroundColor: 'var(--surface)', aspectRatio: '1 / 1' }} onMouseEnter={(e) => { ;(e.currentTarget as HTMLElement).style.borderColor = 'var(--accent)' ;(e.currentTarget as HTMLElement).style.transform = 'translateY(-1px)' }} onMouseLeave={(e) => { ;(e.currentTarget as HTMLElement).style.borderColor = 'var(--border)' ;(e.currentTarget as HTMLElement).style.transform = 'translateY(0)' }} > {/* Thumbnail image — hidden until loaded */} {entry.thumbnailUrl && ( // eslint-disable-next-line @next/next/no-img-element setImgState('loaded')} onError={() => setImgState('error')} /> )} {/* Skeleton pulse shown while image is loading */} {imgState === 'loading' && (
)} {/* Icon fallback — shown for dirs, other files, and failed thumbnails */} {!showThumbnail && imgState !== 'loading' && (
{icon}
)} {/* Bottom label — always shown */}
{entry.name}
{/* Video play badge — top-right overlay */} {isVideo && imgState === 'loaded' && (
)} {/* Tag button — top-left, shown on hover */}
) } function LoadingSkeleton() { return (
{Array.from({ length: 12 }).map((_, i) => (
))}
) }