| import { useState, useCallback, useEffect, useRef } from 'react'; |
| import type { ImageFile, OutputType, CompressionOptions } from '../types'; |
| import { decode, encode, getFileType } from '../utils/imageProcessing'; |
|
|
| export function useImageQueue( |
| options: CompressionOptions, |
| outputType: OutputType, |
| setImages: React.Dispatch<React.SetStateAction<ImageFile[]>> |
| ) { |
| const MAX_PARALLEL_PROCESSING = 3; |
| const [queue, setQueue] = useState<string[]>([]); |
| const processingCount = useRef(0); |
| const processingImages = useRef(new Set<string>()); |
|
|
| const processImage = useCallback(async (image: ImageFile) => { |
| if (processingImages.current.has(image.id)) { |
| return; |
| } |
| processingImages.current.add(image.id); |
| processingCount.current++; |
|
|
| try { |
| setImages((prev) => |
| prev.map((img) => |
| img.id === image.id |
| ? { ...img, status: 'processing' as const } |
| : img |
| ) |
| ); |
|
|
| const fileBuffer = await image.file.arrayBuffer(); |
| const sourceType = getFileType(image.file); |
| |
| if (!fileBuffer.byteLength) { |
| throw new Error('Empty file'); |
| } |
|
|
| |
| const imageData = await decode(sourceType, fileBuffer); |
| |
| if (!imageData || !imageData.width || !imageData.height) { |
| throw new Error('Invalid image data'); |
| } |
|
|
| |
| const compressedBuffer = await encode(outputType, imageData, options); |
| |
| if (!compressedBuffer.byteLength) { |
| throw new Error('Failed to compress image'); |
| } |
|
|
| const blob = new Blob([compressedBuffer], { type: `image/${outputType}` }); |
| const preview = URL.createObjectURL(blob); |
|
|
| setImages((prev) => |
| prev.map((img) => |
| img.id === image.id |
| ? { |
| ...img, |
| status: 'complete' as const, |
| preview, |
| blob, |
| compressedSize: compressedBuffer.byteLength, |
| outputType, |
| } |
| : img |
| ) |
| ); |
| } catch (error) { |
| console.error('Error processing image:', error); |
| setImages((prev) => |
| prev.map((img) => |
| img.id === image.id |
| ? { |
| ...img, |
| status: 'error' as const, |
| error: error instanceof Error |
| ? error.message |
| : 'Failed to process image', |
| } |
| : img |
| ) |
| ); |
| } finally { |
| processingImages.current.delete(image.id); |
| processingCount.current--; |
| |
| setTimeout(processNextInQueue, 0); |
| } |
| }, [options, outputType, setImages]); |
|
|
| const processNextInQueue = useCallback(() => { |
| console.log('Processing next in queue:', { |
| queueLength: queue.length, |
| processingCount: processingCount.current, |
| processingImages: [...processingImages.current] |
| }); |
|
|
| if (queue.length === 0) return; |
|
|
| |
| setImages(prev => { |
| const imagesToProcess = prev.filter(img => |
| queue.includes(img.id) && |
| !processingImages.current.has(img.id) && |
| processingCount.current < MAX_PARALLEL_PROCESSING |
| ); |
|
|
| console.log('Found images to process:', imagesToProcess.length); |
|
|
| if (imagesToProcess.length === 0) return prev; |
|
|
| |
| imagesToProcess.forEach((image, index) => { |
| setTimeout(() => { |
| processImage(image); |
| }, index * 100); |
| }); |
|
|
| |
| setQueue(current => current.filter(id => |
| !imagesToProcess.some(img => img.id === id) |
| )); |
|
|
| |
| return prev.map(img => |
| imagesToProcess.some(processImg => processImg.id === img.id) |
| ? { ...img, status: 'queued' as const } |
| : img |
| ); |
| }); |
| }, [queue, processImage, setImages]); |
|
|
| |
| useEffect(() => { |
| console.log('Queue changed:', queue.length); |
| if (queue.length > 0) { |
| processNextInQueue(); |
| } |
| }, [queue, processNextInQueue]); |
|
|
| const addToQueue = useCallback((imageId: string) => { |
| console.log('Adding to queue:', imageId); |
| setQueue(prev => [...prev, imageId]); |
| }, []); |
|
|
| return { addToQueue }; |
| } |