import { useCallback, useEffect, useRef, useState } from 'react'; import { fetchMedia } from './api'; import type { InsertPayload, MediaItem, VoltaxModalConfig } from './types'; import { VideoThumbPreview } from './VideoThumbPreview'; import { PlaylistThumb } from './PlaylistThumb'; import { formatDate, formatDuration, getVideoPreviewUrl } from './utils'; type MediaLibraryTabProps = { mode: 'video' | 'playlist'; onSelect: ( payload: InsertPayload | null ) => void; }; const SCROLL_THRESHOLD = 120; function getItemId( item: MediaItem ): string { return String( item.payload_id || item.id || '' ).trim(); } function groupCategoryOptions( options: VoltaxModalConfig['categories'] ) { const groups = new Map(); const ungrouped: typeof options = []; for ( const option of options ) { if ( option.group ) { const existing = groups.get( option.group ) ?? []; existing.push( option ); groups.set( option.group, existing ); continue; } ungrouped.push( option ); } return { groups, ungrouped }; } export function MediaLibraryTab( { mode, onSelect }: MediaLibraryTabProps ) { const config = window.voltaxModalConfig!; const [ query, setQuery ] = useState( '' ); const [ language, setLanguage ] = useState( '' ); const [ category, setCategory ] = useState( '' ); const [ playlistType, setPlaylistType ] = useState( '' ); const [ results, setResults ] = useState( [] ); const [ loading, setLoading ] = useState( true ); const [ loadingMore, setLoadingMore ] = useState( false ); const [ hasMore, setHasMore ] = useState( true ); const [ selectedId, setSelectedId ] = useState( '' ); const [ refreshKey, setRefreshKey ] = useState( 0 ); const scrollRef = useRef( null ); const requestIdRef = useRef( 0 ); const abortRef = useRef( null ); const loadPage = useCallback( async ( offset: number, replace: boolean ) => { abortRef.current?.abort(); const controller = new AbortController(); abortRef.current = controller; const requestId = ++requestIdRef.current; if ( replace ) { setLoading( true ); setLoadingMore( false ); } else { setLoadingMore( true ); } try { const { items, hasMore: pageHasMore } = await fetchMedia( { type: mode, query, language: mode === 'video' ? language : '', category: mode === 'video' ? category : '', playlistType: mode === 'playlist' ? playlistType : '', offset, signal: controller.signal, } ); if ( requestId !== requestIdRef.current ) { return; } setResults( ( current ) => { if ( replace ) { return items; } const existingIds = new Set( current.map( ( item ) => getItemId( item ) ) ); const nextItems = items.filter( ( item ) => ! existingIds.has( getItemId( item ) ) ); return [ ...current, ...nextItems ]; } ); setHasMore( pageHasMore ); } catch { if ( controller.signal.aborted || requestId !== requestIdRef.current ) { return; } if ( replace ) { setResults( [ { payload_id: 'error', error: 'Unable to load results. Please try again.', }, ] ); setHasMore( false ); } } finally { if ( requestId === requestIdRef.current ) { setLoading( false ); setLoadingMore( false ); } } }, [ mode, query, language, category, playlistType ] ); useEffect( () => { setResults( [] ); setHasMore( true ); setSelectedId( '' ); void loadPage( 0, true ); return () => { abortRef.current?.abort(); }; }, [ mode, query, language, category, playlistType, refreshKey, loadPage ] ); const handleRefresh = () => { setRefreshKey( ( current ) => current + 1 ); }; const handleScroll = useCallback( () => { const container = scrollRef.current; if ( ! container || loading || loadingMore || ! hasMore ) { return; } const nearBottom = container.scrollTop + container.clientHeight >= container.scrollHeight - SCROLL_THRESHOLD; if ( nearBottom ) { void loadPage( results.length, false ); } }, [ loading, loadingMore, hasMore, loadPage, results.length ] ); useEffect( () => { if ( ! selectedId ) { onSelect( null ); return; } const selectedItem = results.find( ( item ) => getItemId( item ) === selectedId ); if ( ! selectedItem || getItemId( selectedItem ) === 'error' ) { onSelect( null ); return; } const itemId = getItemId( selectedItem ); onSelect( { type: mode, id: itemId, title: selectedItem.title || itemId, image: selectedItem.image, preview_url: mode === 'video' ? getVideoPreviewUrl( selectedItem.sources ) || undefined : undefined, description: selectedItem.description || undefined, duration: selectedItem.duration || undefined, updated_at: selectedItem.updated_at ?? undefined, } ); }, [ selectedId, results, mode, onSelect ] ); const handleSelect = ( item: MediaItem ) => { const itemId = getItemId( item ); if ( ! itemId || itemId === 'error' ) { return; } setSelectedId( ( current ) => ( current === itemId ? '' : itemId ) ); }; const categoryOptions = config.categories?.length ? config.categories : []; const { groups: categoryGroups, ungrouped: ungroupedCategories } = groupCategoryOptions( categoryOptions ); return ( <>
setQuery( event.target.value ) } placeholder={ config.i18n.search } /> { mode === 'playlist' && ( config.playlistTypes?.length ?? 0 ) ? ( ) : null } { mode === 'video' && categoryOptions.length ? ( ) : null } { mode === 'video' && config.languages?.length ? ( ) : null }
{ loading && ! results.length ? (
{ config.i18n.searching }
) : ( <>
    { results.map( ( item, index ) => { const itemId = getItemId( item ); const isError = itemId === 'error'; if ( isError ) { return (
  • { item.error }
  • ); } const isSelected = !! selectedId && selectedId === itemId; const thumb = item.image; const dateLabel = formatDate( item.updated_at ); const durationLabel = mode === 'video' ? formatDuration( item.duration ) : ''; const playlistCount = item.items !== undefined && item.items !== null && item.items !== '' ? `${ item.items } ${ item.playlist_type === 5 ? config.i18n.playlistsCount : config.i18n.videosCount }` : ''; return (
  • ); } ) }
{ loadingMore ? (
{ config.i18n.loadingMore }
) : null } ) }
); }