import { useCallback, useMemo, useRef, useState } from 'react'; import { Alert, alpha, Box, Button, CircularProgress, Paper, Stack, Typography, } from '@mui/material'; import TextInputArea, { type ToolbarAction } from '@/components/TextInputArea'; import ImageIcon from '@mui/icons-material/Image'; import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; import { useTranslation } from 'react-i18next'; import CopyButton from '@/components/CopyButton'; import DecodeResultPaper from '@/components/DecodeResultPaper'; import { fileToBase64, isFileSizeValid, isSupportedImageType, isSupportedImageExtension, formatFileSize, base64ToBlob, downloadBlob, MAX_FILE_SIZE, } from '@/utils/base64Converter'; import type { Base64ToBlobResult, FileToBase64Result } from '@/utils/base64Converter'; import { useStorageState } from '@/utils/useStorageState'; import type { Base64ConvertDirection } from '@/types/storage'; import SwitchButtonGroup from '@/components/SwitchButtonGroup'; interface FileInfo { name: string; size: number; type: string; } const ERROR_MESSAGE_TO_I18N: Record = { 'Invalid Base64 string': 'invalidBase64', }; const isValidDirection = (val: unknown): val is Base64ConvertDirection => val === 'encode' || val === 'decode'; export default function ImageMode() { const { t } = useTranslation('base64Converter'); const [direction, setDirection] = useStorageState( 'base64Converter/imageMode/direction', 'encode', isValidDirection, ); // encode state const [result, setResult] = useState(null); const [info, setInfo] = useState(null); const [isLoading, setIsLoading] = useState(false); const [isDragging, setIsDragging] = useState(false); const imageInputRef = useRef(null); const cancelRef = useRef(false); // decode state const [decodeInput, setDecodeInput] = useState(''); const [decoded, setDecoded] = useState(null); const [decodedFileName, setDecodedFileName] = useState(''); // shared const [error, setError] = useState(null); const resetAll = useCallback(() => { cancelRef.current = true; setResult(null); setInfo(null); setIsLoading(false); setDecodeInput(''); setDecoded(null); setDecodedFileName(''); setError(null); if (imageInputRef.current) imageInputRef.current.value = ''; }, []); const handleClear = () => { resetAll(); }; const handleDirectionChange = (next: Base64ConvertDirection) => { if (!next || next === direction) return; resetAll(); setDirection(next); }; const handleFileSelect = async (file: File) => { cancelRef.current = false; setError(null); setResult(null); setInfo(null); if (!isFileSizeValid(file.size)) { setError(t('fileSizeExceeded', { max: `${MAX_FILE_SIZE / 1024 / 1024} MB` })); return; } if (!isSupportedImageType(file.type) && !isSupportedImageExtension(file.name)) { setError(t('unsupportedImageType')); return; } setInfo({ name: file.name, size: file.size, type: file.type || 'application/octet-stream', }); setIsLoading(true); try { const res = await fileToBase64(file); if (!cancelRef.current) setResult(res); } catch (e) { if (!cancelRef.current) { setError(e instanceof Error ? e.message : t('conversionFailed')); } } finally { if (!cancelRef.current) setIsLoading(false); } }; const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(true); }; const handleDragLeave = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); }; const handleDrop = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); const file = e.dataTransfer.files[0]; if (file) handleFileSelect(file); }; const handleDownload = () => { if (!decoded) return; downloadBlob(decoded.blob, decodedFileName || `decoded${decoded.suggestedExtension}`); }; const actions: ToolbarAction[] = useMemo( () => [ { key: 'decode', label: t('decode'), type: 'primary', position: 'bottom', disabled: (value: string) => !value.trim(), onClick: (value: string, helpers) => { helpers.setError(''); setDecoded(null); try { const res = base64ToBlob(value); setDecoded(res); setDecodedFileName(`decoded${res.suggestedExtension}`); } catch (e) { const message = e instanceof Error ? e.message : ''; const i18nKey = ERROR_MESSAGE_TO_I18N[message]; helpers.setError(i18nKey ? t(i18nKey) : message || t('conversionFailed')); } }, }, ], [t], ); return ( <> {direction === 'encode' && ( <> imageInputRef.current?.click()} sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', minHeight: 180, border: '2px dashed', borderColor: isDragging ? 'info.main' : info ? 'info.main' : 'divider', borderRadius: 3, p: 4, bgcolor: (theme) => isDragging ? alpha(theme.palette.info.main, 0.08) : info ? alpha(theme.palette.info.main, 0.04) : 'action.hover', cursor: 'pointer', transition: 'all 0.2s', '&:hover': { borderColor: 'info.main', bgcolor: (theme) => alpha(theme.palette.info.main, 0.04), }, }} > { const file = e.target.files?.[0]; if (file) handleFileSelect(file); }} /> {isLoading ? ( ) : info ? ( {result && ( )} {info.name} {formatFileSize(info.size)} ยท {info.type} {t('clickOrDropToReplace')} ) : ( {t('clickOrDropToImage')} {t('supportedFormats')} )} {error && {error}} {result && ( alpha(theme.palette.info.main, 0.04), border: '1px solid', borderColor: (theme) => alpha(theme.palette.info.main, 0.15), }} > {t('base64Output')} {result.output.length > 2000 ? `${result.output.substring(0, 2000)}...` : result.output} {t('originalSize')}: {formatFileSize(result.originalBytes)} {t('encodedSize')}: {formatFileSize(result.outputBytes)} )} {info && ( )} )} {direction === 'decode' && ( <> { setDecodeInput(v); setError(null); }} actions={actions} externalError={error || undefined} onClear={() => { setDecoded(null); setDecodedFileName(''); }} /> {decoded && ( )} )} ); }