diff --git a/client/src/components/images/ImageConverter.tsx b/client/src/components/images/ImageConverter.tsx index efc3fcd..6b46d85 100644 --- a/client/src/components/images/ImageConverter.tsx +++ b/client/src/components/images/ImageConverter.tsx @@ -1,4 +1,4 @@ -import { useState } from 'preact/hooks'; +import { useState, useRef, useEffect } from 'preact/hooks'; import { useWasm } from '@/hooks/useWasm'; import { to_grayscale, @@ -13,6 +13,7 @@ import AlgorithmsContainer from '@/components/algorithms/AlgorithmsContainer'; import { ConversionAlgorithm, ConversionAlgorithmType, + getAlgorithmName, } from '@/models/algorithms'; import { TargetedEvent } from 'preact/compat'; @@ -68,6 +69,31 @@ const ImageConverter = () => { const [previewsAspectRatios, setPreviewsAspectRatios] = useState(16 / 10); const [errorMessage, setErrorMessage] = useState(); + const [isProcessing, setIsProcessing] = useState(false); + const [processingProgress, setProcessingProgress] = useState(0); + const [currentAlgorithm, setCurrentAlgorithm] = useState(''); + + const prevSrcUrlRef = useRef(null); + const prevResultUrlRef = useRef(null); + const YIELD_DELAY_MS = 10; + const FINAL_DISPLAY_DELAY_MS = 500; + + const cleanupBlobUrls = () => { + if (prevSrcUrlRef.current) { + URL.revokeObjectURL(prevSrcUrlRef.current); + prevSrcUrlRef.current = null; + } + if (prevResultUrlRef.current) { + URL.revokeObjectURL(prevResultUrlRef.current); + prevResultUrlRef.current = null; + } + }; + + useEffect(() => { + return () => { + cleanupBlobUrls(); + }; + }, []); const handleUpload = async (e: TargetedEvent) => { const file = e.currentTarget.files?.[0]; @@ -81,8 +107,21 @@ const ImageConverter = () => { try { const processedBytes = processBytes(file.type, bytes); setRawBytes(processedBytes); + + if (prevSrcUrlRef.current) { + URL.revokeObjectURL(prevSrcUrlRef.current); + } + const blob = new Blob([processedBytes]); - setImgSrc(URL.createObjectURL(blob)); + const newUrl = URL.createObjectURL(blob); + prevSrcUrlRef.current = newUrl; + setImgSrc(newUrl); + + if (prevResultUrlRef.current) { + URL.revokeObjectURL(prevResultUrlRef.current); + prevResultUrlRef.current = null; + setImgResult(null); + } } catch (err) { setErrorMessage(`Upload error: ${err}`); setImgSrc(null); @@ -90,28 +129,72 @@ const ImageConverter = () => { } }; - const handleRun = () => { + const handleRun = async () => { const enabledAlgorithms = algorithms.filter((a) => a.enabled); if (!rawBytes || !wasmReady) return; if (enabledAlgorithms.length === 0) { setErrorMessage('No algorithms selected'); return; } + setErrorMessage(undefined); + setIsProcessing(true); + setProcessingProgress(0); + setCurrentAlgorithm(''); - let processedBytes: Uint8Array | undefined = - Uint8Array.from(rawBytes); - for (const algorithm of enabledAlgorithms) { - processedBytes = convert(processedBytes, algorithm, setErrorMessage); - if (!processedBytes) { - console.error(`Conversion failed for algorithm: ${algorithm.type}`); - return; + try { + let processedBytes: Uint8Array | undefined = + Uint8Array.from(rawBytes); + + for (let i = 0; i < enabledAlgorithms.length; i++) { + const algorithm = enabledAlgorithms[i]; + + setCurrentAlgorithm(getAlgorithmName(algorithm.type)); + setProcessingProgress(Math.round((i / enabledAlgorithms.length) * 100)); + + processedBytes = convert(processedBytes, algorithm, setErrorMessage); + if (!processedBytes) { + console.error(`Conversion failed for algorithm: ${algorithm.type}`); + return; + } + + if (prevResultUrlRef.current) { + URL.revokeObjectURL(prevResultUrlRef.current); + } + + const intermediateBlob = new Blob([processedBytes], { type: 'image/png' }); + const newUrl = URL.createObjectURL(intermediateBlob); + prevResultUrlRef.current = newUrl; + setImgResult(newUrl); + + await new Promise(resolve => setTimeout(resolve, YIELD_DELAY_MS)); } - } - const blob = new Blob([processedBytes], { type: 'image/png' }); - setImgResult(URL.createObjectURL(blob)); - setErrorMessage(undefined); + setProcessingProgress(100); + setCurrentAlgorithm('Complete'); + + if (prevResultUrlRef.current) { + URL.revokeObjectURL(prevResultUrlRef.current); + } + + const finalBlob = new Blob([processedBytes], { type: 'image/png' }); + const finalUrl = URL.createObjectURL(finalBlob); + prevResultUrlRef.current = finalUrl; + setImgResult(finalUrl); + setErrorMessage(undefined); + + await new Promise(resolve => setTimeout(resolve, FINAL_DISPLAY_DELAY_MS)); + } catch (error) { + const errorMessage = + error instanceof Error + ? error.message + : JSON.stringify(error); + setErrorMessage(`Processing error: ${errorMessage}`); + } finally { + setIsProcessing(false); + setProcessingProgress(0); + setCurrentAlgorithm(''); + } }; return ( @@ -131,7 +214,7 @@ const ImageConverter = () => { error={errorMessage} /> -
+
{ : 'No image selected' } /> + + {/* Progress Indicator */} + {isProcessing && ( +
+
+
+ {currentAlgorithm || 'Processing...'} +
+
+
+
+
+ {processingProgress}% +
+
+
+ )}
@@ -162,10 +265,15 @@ const ImageConverter = () => { />