feat: add error recovery with retry functionality
Error recovery features: - **Retry button**: Appears on failed conversions - **Smart retry logic**: Re-attempts conversion with same settings - **State management**: Properly resets job state before retry - **Progress tracking**: Shows progress during retry attempt - **Toast notifications**: Informs user of retry success/failure - **History updates**: Successful retries added to history - **Visual feedback**: RefreshCw icon with clear button label Implementation: - Added onRetry prop to ConversionPreview component - Implemented handleRetry function in FileConverter - Reuses existing conversion logic for consistency - Maintains all conversion options during retry - Updates job status through proper state flow: error → loading → processing → completed/error User experience: - One-click retry for failed conversions - No need to re-upload file or reconfigure settings - Clear visual indication when retry is in progress - Helpful error messages if retry also fails 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -244,6 +244,109 @@ export function FileConverter() {
|
||||
addToast(`Downloaded ${files.length} files as ZIP`, 'success');
|
||||
};
|
||||
|
||||
const handleRetry = async (jobId: string) => {
|
||||
const jobIndex = conversionJobs.findIndex(j => j.id === jobId);
|
||||
if (jobIndex === -1 || !outputFormat) return;
|
||||
|
||||
const job = conversionJobs[jobIndex];
|
||||
|
||||
try {
|
||||
// Reset job to loading
|
||||
setConversionJobs((prev) =>
|
||||
prev.map((j, idx) => idx === jobIndex ? {
|
||||
...j,
|
||||
status: 'loading' as const,
|
||||
progress: 0,
|
||||
error: undefined,
|
||||
startTime: Date.now(),
|
||||
} : j)
|
||||
);
|
||||
|
||||
// Update to processing
|
||||
setConversionJobs((prev) =>
|
||||
prev.map((j, idx) => idx === jobIndex ? { ...j, status: 'processing' as const, progress: 10 } : j)
|
||||
);
|
||||
|
||||
// Call appropriate converter
|
||||
let result;
|
||||
|
||||
switch (outputFormat.converter) {
|
||||
case 'ffmpeg':
|
||||
result = await convertWithFFmpeg(job.inputFile, outputFormat.extension, conversionOptions, (progress) => {
|
||||
setConversionJobs((prev) =>
|
||||
prev.map((j, idx) => idx === jobIndex ? { ...j, progress } : j)
|
||||
);
|
||||
});
|
||||
break;
|
||||
|
||||
case 'imagemagick':
|
||||
result = await convertWithImageMagick(
|
||||
job.inputFile,
|
||||
outputFormat.extension,
|
||||
conversionOptions,
|
||||
(progress) => {
|
||||
setConversionJobs((prev) =>
|
||||
prev.map((j, idx) => idx === jobIndex ? { ...j, progress } : j)
|
||||
);
|
||||
}
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown converter: ${outputFormat.converter}`);
|
||||
}
|
||||
|
||||
// Update job with result
|
||||
if (result.success && result.blob) {
|
||||
setConversionJobs((prev) =>
|
||||
prev.map((j, idx) => idx === jobIndex ? {
|
||||
...j,
|
||||
status: 'completed' as const,
|
||||
progress: 100,
|
||||
result: result.blob,
|
||||
endTime: Date.now(),
|
||||
} : j)
|
||||
);
|
||||
|
||||
addToast('Conversion completed successfully!', 'success');
|
||||
|
||||
// Add to history
|
||||
addToHistory({
|
||||
inputFileName: job.inputFile.name,
|
||||
inputFormat: job.inputFormat.name,
|
||||
outputFormat: outputFormat.name,
|
||||
outputFileName: `output.${outputFormat.extension}`,
|
||||
fileSize: result.blob.size,
|
||||
result: result.blob,
|
||||
});
|
||||
} else {
|
||||
setConversionJobs((prev) =>
|
||||
prev.map((j, idx) => idx === jobIndex ? {
|
||||
...j,
|
||||
status: 'error' as const,
|
||||
error: result.error || 'Unknown error',
|
||||
endTime: Date.now(),
|
||||
} : j)
|
||||
);
|
||||
|
||||
addToast(result.error || 'Retry failed', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
|
||||
setConversionJobs((prev) =>
|
||||
prev.map((j, idx) => idx === jobIndex ? {
|
||||
...j,
|
||||
status: 'error' as const,
|
||||
error: errorMessage,
|
||||
endTime: Date.now(),
|
||||
} : j)
|
||||
);
|
||||
|
||||
addToast(`Retry failed: ${errorMessage}`, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const isConverting = conversionJobs.some(job => job.status === 'loading' || job.status === 'processing');
|
||||
const isConvertDisabled = selectedFiles.length === 0 || !outputFormat || isConverting;
|
||||
const completedCount = conversionJobs.filter(job => job.status === 'completed').length;
|
||||
@@ -356,7 +459,11 @@ export function FileConverter() {
|
||||
{conversionJobs.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
{conversionJobs.map((job) => (
|
||||
<ConversionPreview key={job.id} job={job} />
|
||||
<ConversionPreview
|
||||
key={job.id}
|
||||
job={job}
|
||||
onRetry={() => handleRetry(job.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user