import { useContext, useState, useRef, useEffect } from "react"; import { FaTimes, FaSync, FaCheckCircle, FaExclamationCircle, FaHourglassHalf, FaMusic, FaCompactDisc, FaStepForward } from "react-icons/fa"; import { QueueContext, type QueueItem, getStatus, getProgress, getCurrentTrackInfo, isActiveStatus, isTerminalStatus } from "@/contexts/queue-context"; import { useAuth } from "@/contexts/auth-context"; // Circular Progress Component const CircularProgress = ({ progress, isCompleted = false, size = 60, strokeWidth = 6 }: { progress: number; isCompleted?: boolean; size?: number; strokeWidth?: number; }) => { const radius = (size - strokeWidth) / 2; const circumference = radius * 2 * Math.PI; const strokeDashoffset = circumference - (progress / 100) * circumference; return (
{/* Background circle */} {/* Progress circle */} {/* Center content */}
{isCompleted ? ( ) : ( {Math.round(progress)}% )}
); }; // Status styling configuration const statusStyles = { initializing: { icon: , color: "text-info", bgColor: "bg-gradient-to-r from-blue-50 to-blue-100 dark:from-blue-900/20 dark:to-blue-800/30", borderColor: "border-info/30 dark:border-info/40", name: "Initializing", }, processing: { icon: , color: "text-processing", bgColor: "bg-gradient-to-r from-purple-50 to-purple-100 dark:from-purple-900/20 dark:to-purple-800/30", borderColor: "border-processing/30 dark:border-processing/40", name: "Processing", }, downloading: { icon: , color: "text-info", bgColor: "bg-gradient-to-r from-blue-50 to-blue-100 dark:from-blue-900/20 dark:to-blue-800/30", borderColor: "border-info/30 dark:border-info/40", name: "Downloading", }, "real-time": { icon: , color: "text-info", bgColor: "bg-gradient-to-r from-blue-50 to-blue-100 dark:from-blue-900/20 dark:to-blue-800/30", borderColor: "border-info/30 dark:border-info/40", name: "Downloading", }, done: { icon: , color: "text-success", bgColor: "bg-gradient-to-r from-green-50 to-green-100 dark:from-green-900/20 dark:to-green-800/30", borderColor: "border-success/30 dark:border-success/40", name: "Done", }, completed: { icon: , color: "text-success", bgColor: "bg-gradient-to-r from-green-50 to-green-100 dark:from-green-900/20 dark:to-green-800/30", borderColor: "border-success/30 dark:border-success/40", name: "Completed", }, error: { icon: , color: "text-error", bgColor: "bg-gradient-to-r from-red-50 to-red-100 dark:from-red-900/20 dark:to-red-800/30", borderColor: "border-error/30 dark:border-error/40", name: "Error", }, cancelled: { icon: , color: "text-warning", bgColor: "bg-gradient-to-r from-orange-50 to-orange-100 dark:from-orange-900/20 dark:to-orange-800/30", borderColor: "border-warning/30 dark:border-warning/40", name: "Cancelled", }, skipped: { icon: , color: "text-warning", bgColor: "bg-gradient-to-r from-yellow-50 to-yellow-100 dark:from-yellow-900/20 dark:to-yellow-800/30", borderColor: "border-warning/30 dark:border-warning/40", name: "Skipped", }, queued: { icon: , color: "text-content-muted dark:text-content-muted-dark", bgColor: "bg-gradient-to-r from-surface-muted to-surface-accent dark:from-surface-muted-dark dark:to-surface-accent-dark", borderColor: "border-border dark:border-border-dark", name: "Queued", }, retrying: { icon: , color: "text-warning", bgColor: "bg-gradient-to-r from-orange-50 to-orange-100 dark:from-orange-900/20 dark:to-orange-800/30", borderColor: "border-warning/30 dark:border-warning/40", name: "Retrying", }, } as const; // Skipped Task Component const SkippedTaskCard = ({ item }: { item: QueueItem }) => { const { removeItem } = useContext(QueueContext) || {}; const trackInfo = getCurrentTrackInfo(item); const TypeIcon = item.downloadType === "album" ? FaCompactDisc : FaMusic; return (
{/* Main content */}

{item.name}

{item.artist}

{/* Show current track info for parent downloads */} {(item.downloadType === "album" || item.downloadType === "playlist") && trackInfo.title && (

{trackInfo.current}/{trackInfo.total}: {trackInfo.title}

)}
{/* Status and actions */}
Skipped
{/* Remove button */}
{/* Skip reason */} {item.error && (

Skipped: {item.error}

)}
); }; // Cancelled Task Component const CancelledTaskCard = ({ item }: { item: QueueItem }) => { const { removeItem } = useContext(QueueContext) || {}; const trackInfo = getCurrentTrackInfo(item); const TypeIcon = item.downloadType === "album" ? FaCompactDisc : FaMusic; return (
{/* Main content */}

{item.name}

{item.artist}

{/* Show current track info for parent downloads */} {(item.downloadType === "album" || item.downloadType === "playlist") && trackInfo.title && (

{trackInfo.current}/{trackInfo.total}: {trackInfo.title}

)}
{/* Status and actions */}
Cancelled
{/* Remove button */}
{/* Cancellation reason */} {item.error && (

Cancelled: {item.error}

)}
); }; const QueueItemCard = ({ item, cachedStatus }: { item: QueueItem, cachedStatus: string }) => { const { removeItem, cancelItem } = useContext(QueueContext) || {}; const status = cachedStatus; const progress = getProgress(item); const trackInfo = getCurrentTrackInfo(item); const styleInfo = statusStyles[status as keyof typeof statusStyles] || statusStyles.queued; const isTerminal = isTerminalStatus(status); const isActive = isActiveStatus(status); // Get type icon const TypeIcon = item.downloadType === "album" ? FaCompactDisc : FaMusic; return (
{/* Main content */}
{styleInfo.icon}

{item.name}

{item.artist}

{/* Show current track info for parent downloads */} {(item.downloadType === "album" || item.downloadType === "playlist") && trackInfo.title && (

{trackInfo.current}/{trackInfo.total}: {trackInfo.title}

)}
{/* Status and progress */}
{styleInfo.name}
{/* Summary info for completed downloads */} {isTerminal && item.summary && item.downloadType !== "track" && (

{item.summary.total_successful}/{trackInfo.total || item.summary.total_successful + item.summary.total_failed + item.summary.total_skipped} tracks

)}
{/* Circular progress for active downloads */} {isActive && progress !== undefined && (
)} {/* Completed progress for finished downloads */} {isTerminal && status === "done" && item.downloadType === "track" && (
)} {/* Action buttons */}
{isTerminal ? ( ) : ( )}
{/* Error message */} {item.error && (

{status === "cancelled" ? "Cancelled: " : status === "skipped" ? "Skipped: " : "Error: "} {item.error}

)} {/* Summary for completed downloads with multiple tracks */} {isTerminal && item.summary && item.downloadType !== "track" && (

Download Summary

{item.summary.total_successful + item.summary.total_failed + item.summary.total_skipped} tracks total
{item.summary.total_successful > 0 && (
{item.summary.total_successful} successful
)} {item.summary.total_failed > 0 && (
{item.summary.total_failed} failed
)} {item.summary.total_skipped > 0 && (
{item.summary.total_skipped} skipped
)}
)}
); }; export const Queue = () => { const context = useContext(QueueContext); const { authEnabled, isAuthenticated } = useAuth(); // Check if user is authenticated (only relevant when auth is enabled) const isUserAuthenticated = !authEnabled || isAuthenticated; const [startY, setStartY] = useState(null); const [isDragging, setIsDragging] = useState(false); const [dragDistance, setDragDistance] = useState(0); const queueRef = useRef(null); const scrollContainerRef = useRef(null); const headerRef = useRef(null); const [canDrag, setCanDrag] = useState(false); // Virtual scrolling state const [visibleItemCount, setVisibleItemCount] = useState(7); const [isLoadingMoreItems, setIsLoadingMoreItems] = useState(false); // Track items that recently transitioned to terminal states const [recentlyTerminated, setRecentlyTerminated] = useState>(new Map()); const previousStatusRef = useRef>(new Map()); const INITIAL_ITEM_COUNT = 7; const LOAD_MORE_THRESHOLD = 0.8; // Load more when 80% scrolled through visible items const TERMINAL_STATE_DISPLAY_DURATION = 3000; // 3 seconds const { items = [], isVisible = false, toggleVisibility = () => {}, cancelAll = () => {}, clearCompleted = () => {}, hasMore = false, isLoadingMore = false, loadMoreTasks = () => {}, totalTasks = 0 } = context || {}; // Track status changes and identify transitions to terminal states useEffect(() => { if (!items || items.length === 0) return; const currentStatuses = new Map(); const newlyTerminated = new Map(); const currentTime = Date.now(); // Check each item for status changes items.forEach(item => { const currentStatus = getStatus(item); const previousStatus = previousStatusRef.current.get(item.id); currentStatuses.set(item.id, currentStatus); // If item transitioned from non-terminal to terminal state if (previousStatus && !isTerminalStatus(previousStatus) && isTerminalStatus(currentStatus)) { newlyTerminated.set(item.id, currentTime); } }); // Update previous statuses previousStatusRef.current = currentStatuses; // Add newly terminated items to tracking if (newlyTerminated.size > 0) { setRecentlyTerminated(prev => { const updated = new Map(prev); newlyTerminated.forEach((timestamp, itemId) => { updated.set(itemId, timestamp); }); return updated; }); // Set up cleanup timers for newly terminated items newlyTerminated.forEach((timestamp, itemId) => { setTimeout(() => { setRecentlyTerminated(prev => { const updated = new Map(prev); // Only remove if the timestamp matches (prevents removing newer entries) if (updated.get(itemId) === timestamp) { updated.delete(itemId); } return updated; }); }, TERMINAL_STATE_DISPLAY_DURATION); }); } }, [items]); // Cleanup recently terminated items when items are removed from the queue useEffect(() => { if (!items || items.length === 0) { setRecentlyTerminated(new Map()); previousStatusRef.current = new Map(); return; } // Remove tracking for items that are no longer in the queue const currentItemIds = new Set(items.map(item => item.id)); setRecentlyTerminated(prev => { const updated = new Map(); prev.forEach((timestamp, itemId) => { if (currentItemIds.has(itemId)) { updated.set(itemId, timestamp); } }); return updated; }); // Clean up previous status tracking for removed items const newPreviousStatuses = new Map(); previousStatusRef.current.forEach((status, itemId) => { if (currentItemIds.has(itemId)) { newPreviousStatuses.set(itemId, status); } }); previousStatusRef.current = newPreviousStatuses; }, [items?.length]); // Trigger when items array length changes // Infinite scroll and virtual scrolling useEffect(() => { if (!isVisible) return; const scrollContainer = scrollContainerRef.current; if (!scrollContainer) return; const handleScroll = () => { const { scrollTop, scrollHeight, clientHeight } = scrollContainer; const scrollPercentage = (scrollTop + clientHeight) / scrollHeight; // Virtual scrolling - load more visible items if user has scrolled through most visible items if (scrollPercentage > LOAD_MORE_THRESHOLD && !isLoadingMoreItems) { const totalAvailableItems = items.length; if (visibleItemCount < totalAvailableItems) { setIsLoadingMoreItems(true); // Gradually increase visible items (add 5 more each time) setTimeout(() => { setVisibleItemCount(prev => Math.min(prev + 5, totalAvailableItems)); setIsLoadingMoreItems(false); }, 100); // Small delay for smooth UX } } // Server-side pagination - only trigger when we've shown most of our items if (scrollPercentage > 0.9 && hasMore && !isLoadingMore && visibleItemCount >= items.length * 0.8) { loadMoreTasks(); } }; scrollContainer.addEventListener('scroll', handleScroll); return () => scrollContainer.removeEventListener('scroll', handleScroll); }, [isVisible, hasMore, isLoadingMore, loadMoreTasks, visibleItemCount, items.length, isLoadingMoreItems]); // Reset visible item count when items change significantly (new downloads, etc.) useEffect(() => { // If we have fewer items than currently visible, adjust down if (items.length < visibleItemCount) { setVisibleItemCount(Math.max(INITIAL_ITEM_COUNT, items.length)); } }, [items.length, visibleItemCount]); // Reset visible item count when queue visibility changes useEffect(() => { if (isVisible) { // Reset to initial count when queue opens for optimal performance setVisibleItemCount(INITIAL_ITEM_COUNT); } }, [isVisible]); // Mobile drag-to-dismiss const handleTouchStart = (e: React.TouchEvent) => { const touch = e.touches[0]; const scrollContainer = scrollContainerRef.current; const headerElement = headerRef.current; const touchedHeader = headerElement?.contains(e.target as Node); const scrollAtTop = scrollContainer ? scrollContainer.scrollTop <= 10 : true; // Allow dragging from header or anywhere when scrolled to top if (touchedHeader || scrollAtTop) { setCanDrag(true); setStartY(touch.clientY); setIsDragging(false); setDragDistance(0); // Prevent event from bubbling to backdrop e.stopPropagation(); } else { setCanDrag(false); } }; const handleTouchMove = (e: React.TouchEvent) => { if (!canDrag || startY === null) return; const touch = e.touches[0]; const deltaY = touch.clientY - startY; if (deltaY > 0) { // Start dragging with a smaller threshold for better responsiveness if (!isDragging && deltaY > 5) { setIsDragging(true); e.preventDefault(); e.stopPropagation(); } if (isDragging) { e.preventDefault(); e.stopPropagation(); const clampedDelta = Math.min(deltaY, 250); setDragDistance(clampedDelta); if (queueRef.current) { const resistance = Math.pow(clampedDelta / 250, 0.6); const transformY = clampedDelta * resistance; const opacity = Math.max(0.2, 1 - (clampedDelta / 400)); queueRef.current.style.transform = `translateY(${transformY}px)`; queueRef.current.style.opacity = `${opacity}`; queueRef.current.style.transition = 'none'; } } } }; const handleTouchEnd = (e: React.TouchEvent) => { if (!canDrag || startY === null) { resetDragState(); return; } // Prevent event from bubbling to backdrop e.stopPropagation(); if (queueRef.current) { queueRef.current.style.transition = 'transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1)'; // Lower threshold and ensure both conditions are met if (isDragging && dragDistance > 60) { queueRef.current.style.transform = 'translateY(100%)'; queueRef.current.style.opacity = '0'; setTimeout(() => { toggleVisibility(); if (queueRef.current) { queueRef.current.style.transform = ''; queueRef.current.style.opacity = ''; queueRef.current.style.transition = ''; } resetDragState(); }, 300); } else { queueRef.current.style.transform = ''; queueRef.current.style.opacity = ''; setTimeout(() => { if (queueRef.current) { queueRef.current.style.transition = ''; } resetDragState(); }, 300); } } else { resetDragState(); } }; const resetDragState = () => { setStartY(null); setIsDragging(false); setDragDistance(0); setCanDrag(false); // Ensure queue element is reset if (queueRef.current) { queueRef.current.style.transform = ''; queueRef.current.style.opacity = ''; queueRef.current.style.transition = ''; } }; // Prevent body scroll on mobile useEffect(() => { if (!isVisible) return; const isMobile = window.innerWidth < 768; if (!isMobile) return; const originalOverflow = document.body.style.overflow; const originalTouchAction = document.body.style.touchAction; document.body.style.overflow = 'hidden'; document.body.style.touchAction = 'none'; return () => { document.body.style.overflow = originalOverflow; document.body.style.touchAction = originalTouchAction; }; }, [isVisible]); if (!context || !isVisible || !isUserAuthenticated) return null; // Optimize: Calculate status once per item and reuse throughout render const itemsWithStatus = items.map(item => ({ ...item, _cachedStatus: getStatus(item) })); const hasActive = itemsWithStatus.some(item => isActiveStatus(item._cachedStatus)); const hasFinished = itemsWithStatus.some(item => isTerminalStatus(item._cachedStatus)); // Sort items by priority using cached status const sortedItems = [...itemsWithStatus].sort((a, b) => { const statusA = a._cachedStatus; const statusB = b._cachedStatus; const getPriority = (status: string) => { const priorities = { "real-time": 1, downloading: 2, processing: 3, initializing: 4, retrying: 5, queued: 6, done: 7, completed: 7, error: 8, cancelled: 9, skipped: 10 } as Record; return priorities[status as keyof typeof priorities] || 10; }; return getPriority(statusA) - getPriority(statusB); }); // Helper function to determine if an item should be visible const shouldShowItem = (item: QueueItem) => { const status = getStatus(item); // Always show non-terminal items if (!isTerminalStatus(status)) { return true; } // Show items that recently transitioned to terminal states (within 3 seconds) // This includes done, error, cancelled, and skipped states if (recentlyTerminated.has(item.id)) { return true; } // Show items with recent callbacks (items that were already terminal when first seen) return (item.lastCallback && 'timestamp' in item.lastCallback); }; return ( <> {/* Mobile backdrop */}
{ // Only prevent default if not touching the queue if (!queueRef.current?.contains(e.target as Node)) { e.preventDefault(); } }} onTouchMove={(e) => { // Only prevent default if not dragging the queue if (!isDragging) { e.preventDefault(); } }} onTouchEnd={(e) => { // Only prevent default and close if not dragging the queue if (!isDragging && !queueRef.current?.contains(e.target as Node)) { e.preventDefault(); toggleVisibility(); } }} style={{ touchAction: isDragging ? 'none' : 'auto' }} />
{/* Enhanced drag indicator for mobile */}
60 ? "w-16 h-1.5 bg-success animate-pulse" : "w-14 h-1 bg-warning" : "w-12 h-1 bg-content-muted dark:bg-content-muted-dark opacity-60 animate-pulse" }`} />

Download Queue ({totalTasks}) {items.length > INITIAL_ITEM_COUNT && ( Showing {Math.min(visibleItemCount, items.filter(shouldShowItem).length)} of {items.filter(shouldShowItem).length} )}

{(() => { const visibleItems = sortedItems.filter(shouldShowItem); // Apply virtual scrolling - only show limited number of items const itemsToRender = visibleItems.slice(0, visibleItemCount); const hasMoreVisibleItems = visibleItems.length > visibleItemCount; return visibleItems.length === 0 ? (

The queue is empty.

Downloads will appear here

) : ( <> {/* Render visible items */} {itemsToRender.map(item => { if (item._cachedStatus === "cancelled") { return ; } if (item._cachedStatus === "skipped") { return ; } return ; })} {/* Virtual scrolling loading indicator */} {(isLoadingMoreItems || hasMoreVisibleItems) && (
{isLoadingMoreItems ? ( <>
Loading more items... ) : hasMoreVisibleItems ? ( <> Scroll to see {visibleItems.length - visibleItemCount} more items ) : null}
)} {/* Server-side loading indicator */} {isLoadingMore && (
Loading more tasks...
)} {/* Server-side load more button */} {hasMore && !isLoadingMore && visibleItemCount >= items.length * 0.8 && (
)} ); })()}
); };