import React, { useRef, useMemo, useCallback } from 'react'; import { TrashIcon, UploadIcon, ImageIcon, CheckIcon } from './Icons'; import { UserSettings, PresetWallpaper, BackgroundType, WallpaperFit } from '../types'; import { PRESET_WALLPAPERS } from '../constants'; import { useToast } from '../context/ToastContext'; import { useTranslation } from '../i18n'; interface WallpaperManagerProps { settings: UserSettings; onUpdateSettings: (newSettings: UserSettings) => void; } const WallpaperManager: React.FC = ({ settings, onUpdateSettings }) => { const [customUrl, setCustomUrl] = React.useState(''); const fileInputRef = useRef(null); const { showToast } = useToast(); const { t } = useTranslation(); const WALLPAPER_FITS: { value: WallpaperFit; label: string }[] = [ { value: 'cover', label: t.cover }, { value: 'contain', label: t.contain }, { value: 'fill', label: t.fill }, { value: 'repeat', label: t.repeat }, { value: 'center', label: t.center }, ]; const handlePresetWallpaper = useCallback((preset: PresetWallpaper) => { onUpdateSettings({ ...settings, backgroundUrl: preset.url, backgroundType: preset.type }); // Optional: showToast(`已应用壁纸: ${preset.name}`, 'success'); }, [settings, onUpdateSettings]); const handleCustomUrlApply = useCallback(() => { if (!customUrl.trim()) return; // Validate URL protocol, only allow https and http const trimmedUrl = customUrl.trim(); try { const url = new URL(trimmedUrl); if (!['https:', 'http:'].includes(url.protocol)) { showToast(t.unsupportedProtocol, 'error'); return; } } catch { showToast(t.invalidUrlFormat, 'error'); return; } const isVideo = trimmedUrl.match(/\.(mp4|webm|ogg)$/i); const type: BackgroundType = isVideo ? 'video' : 'image'; const newWallpaper: PresetWallpaper = { id: Date.now().toString(), name: 'Custom URL', type: type, url: trimmedUrl, isCustom: true }; onUpdateSettings({ ...settings, backgroundUrl: newWallpaper.url, backgroundType: newWallpaper.type, customWallpapers: [...settings.customWallpapers, newWallpaper] }); setCustomUrl(''); showToast(t.customWallpaperApplied, 'success'); }, [customUrl, settings, onUpdateSettings, showToast, t]); const handleFileUpload = useCallback((e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; // File size limit: 3.5MB (approximately 4.7MB after Base64 encoding, leaving space for other settings data) const MAX_FILE_SIZE = 3.5 * 1024 * 1024; if (file.size > MAX_FILE_SIZE) { showToast(t.fileSizeExceeded, 'error', 5000); e.target.value = ''; return; } // Strict MIME type validation const ALLOWED_IMAGE_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml']; const ALLOWED_VIDEO_TYPES = ['video/mp4', 'video/webm', 'video/ogg']; const ALLOWED_TYPES = [...ALLOWED_IMAGE_TYPES, ...ALLOWED_VIDEO_TYPES]; if (!ALLOWED_TYPES.includes(file.type)) { showToast(t.unsupportedFileType, 'error', 4000); e.target.value = ''; return; } const type: BackgroundType = ALLOWED_VIDEO_TYPES.includes(file.type) ? 'video' : 'image'; const reader = new FileReader(); reader.onload = (event) => { const base64Url = event.target?.result as string; // Validate that file content matches the declared MIME type if (type === 'image' && !base64Url.startsWith('data:image/')) { showToast(t.fileContentMismatch, 'error'); return; } if (type === 'video' && !base64Url.startsWith('data:video/')) { showToast(t.fileContentMismatch, 'error'); return; } // Check Base64 encoded size const base64Size = new Blob([base64Url]).size; const estimatedTotalSize = base64Size + JSON.stringify(settings).length; // localStorage limit is 5MB, we set a strict 5MB limit if (estimatedTotalSize > 5 * 1024 * 1024) { showToast(t.storageFull, 'error', 5000); return; } const newWallpaper: PresetWallpaper = { id: Date.now().toString(), name: file.name, type: type, url: base64Url, isCustom: true }; try { onUpdateSettings({ ...settings, backgroundUrl: base64Url, backgroundType: type, customWallpapers: [...settings.customWallpapers, newWallpaper] }); showToast(t.wallpaperUploaded, 'success'); } catch (error) { showToast(t.storageFull, 'error', 5000); console.error('Failed to save wallpaper:', error); } }; reader.onerror = () => { showToast(t.fileContentMismatch, 'error'); }; reader.readAsDataURL(file); e.target.value = ''; }, [settings, onUpdateSettings, showToast, t]); const handleDeleteWallpaper = useCallback((e: React.MouseEvent, wallpaperToDelete: PresetWallpaper) => { e.stopPropagation(); if (!wallpaperToDelete.isCustom) return; const newCustomWallpapers = settings.customWallpapers.filter( w => w.id !== wallpaperToDelete.id && w.url !== wallpaperToDelete.url ); let newBgUrl = settings.backgroundUrl; let newBgType = settings.backgroundType; if (settings.backgroundUrl === wallpaperToDelete.url) { newBgUrl = PRESET_WALLPAPERS[0].url; newBgType = PRESET_WALLPAPERS[0].type; } onUpdateSettings({ ...settings, customWallpapers: newCustomWallpapers, backgroundUrl: newBgUrl, backgroundType: newBgType }); showToast(t.wallpaperDeleted, 'info'); }, [settings, onUpdateSettings, showToast, t]); const allWallpapers = useMemo(() => { return [...PRESET_WALLPAPERS, ...settings.customWallpapers]; }, [settings.customWallpapers]); return (
{t.wallpaperSettings} {/* Wallpaper grid */}
{allWallpapers.map((preset) => ( ))}
{/* Custom wallpaper controls */}
{/* Local upload */}
{/* URL input */}
setCustomUrl(e.target.value)} placeholder={t.enterImageVideoUrl} className="w-full bg-black/20 border border-white/10 rounded-xl pl-9 pr-3 py-2 text-sm text-white focus:border-white/30 focus:outline-none transition-colors" onKeyDown={(e) => e.key === 'Enter' && handleCustomUrlApply()} />
{/* Wallpaper fit mode */}
{WALLPAPER_FITS.map((fit) => ( ))}
); }; export default WallpaperManager;