import React, { useState, useEffect, useRef, useCallback } from 'react'; import { __ } from '@wordpress/i18n'; import { numberFormat, percentFormat, formatBytes } from '../lib/i18n'; import { useNavigate } from 'react-router-dom'; import Card from '../components/ui/card'; import { Image, HardDrive } from 'lucide-react'; import ScanActionButton from '../components/ui/scan-action-button'; import SettingsButton from '../components/ui/settings-button'; import { regenerateNow, regenerateBackground, getRegenerateProgress, cancelRegenerate, getSettings, saveSettings, getDashboardStats, getDashboardOptimizationStats, RegenerateResponse, } from '../api'; import { TotalImagesIcon2, TotalThumbnailsIcon2, ImageProcessedIcon, ImageDeletedIcon, ImageCreatedIcon, SpaceSavedIcon2, FailedIcon, } from '../components/icons'; import Header from '../components/layout/Header'; import PluginPage from '../components/layout/PluginPage'; import { Tooltip } from '../components/ui/tooltip'; export default function RegenerateThumbnails({ tooltip }: { tooltip?: string } = {}) { const navigate = useNavigate(); const [chunkSize, setChunkSize] = useState('20'); const [loading, setLoading] = useState(true); const [regenerating, setRegenerating] = useState(false); const [hasStarted, setHasStarted] = useState(false); const [noImages, setNoImages] = useState(false); const [progress, setProgress] = useState(0); const [totalImages, setTotalImages] = useState(0); const [totalThumbnails, setTotalThumbnails] = useState(0); const [regenStats, setRegenStats] = useState({ processed: 0, created: 0, deleted: 0, space_saved_label: '', not_found: 0, failed: 0, }); const pollRef = useRef | null>(null); const generationRef = useRef(0); const stopPolling = useCallback(() => { if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; } }, []); const refreshStats = useCallback(() => { // Force a fresh thumbnail count (bypass the 6h cache) so the page never // shows a stale value on load or after a regeneration completes. getDashboardOptimizationStats(true) .then((r: any) => { if (r?.data?.total_thumbnails != null) { setTotalThumbnails(Number(r.data.total_thumbnails) || 0); } }) .catch(() => { }); }, []); const pollProgress = useCallback(() => { stopPolling(); pollRef.current = setInterval(async () => { try { const res = await getRegenerateProgress(); if (res.success && res.data) { setProgress(res.data.progress); setTotalImages(res.data.total); setRegenStats({ processed: res.data.processed, created: res.data.created, deleted: res.data.deleted, space_saved_label: res.data.space_saved_label || '', not_found: res.data.not_found || 0, failed: res.data.failed || 0, }); if (res.data.is_complete) { setRegenerating(false); stopPolling(); refreshStats(); } } } catch { // keep polling } }, 5000); }, [stopPolling, refreshStats]); useEffect(() => { getDashboardStats() .then((res: any) => { if (res?.data?.total_images != null) { setTotalImages(Number(res.data.total_images) || 0); } }) .catch(() => { }); // Fresh thumbnail count on page open (bypasses the 6h cache). refreshStats(); // Always check live progress first — state option may not be saved if user reloaded early. getRegenerateProgress() .then((progressRes: any) => { if (progressRes?.success && progressRes?.data && progressRes.data.total > 0) { setHasStarted(true); setProgress(progressRes.data.progress); setTotalImages(progressRes.data.total); setRegenStats({ processed: progressRes.data.processed, created: progressRes.data.created, deleted: progressRes.data.deleted, space_saved_label: progressRes.data.space_saved_label || '', not_found: progressRes.data.not_found || 0, failed: progressRes.data.failed || 0, }); if (!progressRes.data.is_complete) { setRegenerating(true); pollProgress(); } else { refreshStats(); } setLoading(false); } else { setLoading(false); } }) .catch(() => { setLoading(false); }); return () => stopPolling(); }, []); const handleScanAgain = () => { generationRef.current++; setHasStarted(false); setRegenerating(false); setNoImages(false); setProgress(0); setTotalImages(0); setRegenStats({ processed: 0, created: 0, deleted: 0, space_saved_label: '', not_found: 0, failed: 0, }); stopPolling(); cancelRegenerate(); saveSettings('thumbpress_regen_view_state', 'initial'); }; const handleRegenerateNow = async () => { const myGen = ++generationRef.current; setRegenerating(true); setHasStarted(true); setNoImages(false); setProgress(0); setRegenStats({ processed: 0, created: 0, deleted: 0, space_saved_label: '', not_found: 0, failed: 0, }); let offset = 0; let limit = chunkSize ? parseInt(chunkSize) : 20; if (!limit || limit < 1) limit = 40; let thumbsDeleted = 0; let thumbsCreated = 0; let spaceSaved = 0; let notFound = 0; let failed = 0; let processedCount = 0; let firstChunk = true; const processChunk = async () => { if (generationRef.current !== myGen) return; try { const res: RegenerateResponse = await regenerateNow( offset, limit, thumbsDeleted, thumbsCreated, spaceSaved, notFound, processedCount, failed, ); if (generationRef.current !== myGen) return; if (!res.success) { setRegenerating(false); if (firstChunk) { setNoImages(true); setHasStarted(true); } return; } const data = typeof res.data === 'object' ? res.data : { message: String(res.data), progress: 0, offset: 0, thumbs_deleted: 0, thumbs_created: 0, total_images_count: 0, }; if (firstChunk) { firstChunk = false; if (Number(data.total_images_count) === 0) { setRegenerating(false); setNoImages(true); setHasStarted(true); return; } setHasStarted(true); } const currentProgress = Number(data.progress) || 0; const currentOffset = Number(data.offset) || 0; setProgress(currentProgress); setTotalImages(Number(data.total_images_count) || 0); setRegenStats({ processed: Number(data.processed_count) || 0, created: Number(data.thumbs_created) || 0, deleted: Number(data.thumbs_deleted) || 0, space_saved_label: data.space_saved_label || '', not_found: Number(data.not_found) || 0, failed: Number(data.failed) || 0, }); offset = currentOffset; thumbsDeleted = Number(data.thumbs_deleted) || 0; thumbsCreated = Number(data.thumbs_created) || 0; spaceSaved = Number(data.space_saved) || 0; notFound = Number(data.not_found) || 0; failed = Number(data.failed) || 0; processedCount = Number(data.processed_count) || 0; if (currentProgress < 100 && currentOffset > 0) { setTimeout(processChunk, 500); } else { setRegenerating(false); refreshStats(); } } catch (err) { setRegenerating(false); console.error('Regenerate chunk failed:', err); } }; processChunk(); }; const handleRegenerateBackground = async () => { try { const limit = chunkSize ? parseInt(chunkSize) : 20; setRegenerating(true); setHasStarted(true); setNoImages(false); setProgress(0); setRegenStats({ processed: 0, created: 0, deleted: 0, space_saved_label: '', not_found: 0, failed: 0 }); const res = await regenerateBackground(limit); if (!res.success || res.data?.total === 0) { setRegenerating(false); setNoImages(true); setHasStarted(true); return; } setHasStarted(true); saveSettings('thumbpress_regen_view_state', 'progress'); setProgress(0); setRegenStats({ processed: 0, created: 0, deleted: 0, space_saved_label: '', not_found: 0, failed: 0, }); pollProgress(); } catch (err) { setRegenerating(false); console.error('Background regeneration failed:', err); } }; const circumference = 2 * Math.PI * 80; const strokeDashoffset = circumference - (progress / 100) * circumference; const StatCard = ({ icon, value, label, accent, }: { icon: React.ReactNode; value: number | string; label: string; accent?: 'red'; }) => (
{icon}

{value}

{label}

); const imageIcon = ; return ( <>
navigate('/settings?tab=thumbnails')} /> ) : ( navigate('/settings?tab=thumbnails')} /> ) } > {loading ? (
) : !hasStarted || noImages ? (

{noImages ? __('No Images Found', 'image-sizes') : __('Ready to Regenerate Thumbnails?', 'image-sizes')}

{noImages ? __('There are no images to regenerate.', 'image-sizes') : <> {__('Click "Regenerate Now" to create fresh thumbnails for all images. Configure your thumbnail sizes in ', 'image-sizes')}{' '} navigate('/settings?tab=thumbnails')} className="text-thumbpress-primary cursor-pointer" > {__('settings', 'image-sizes')} {__('.', 'image-sizes')} }

setChunkSize(e.target.value)} className="flex w-full !rounded-lg !border !border-thumbpress-border bg-white !px-4 !h-[56px] text-sm placeholder:text-gray-400 focus:!outline-none focus:!shadow-none" />
) : (

{progress >= 100 ? __('Image Regeneration Complete', 'image-sizes') : __('Image Regeneration in Progress', 'image-sizes')}

{progress >= 100 ? __('All thumbnails have been regenerated successfully.', 'image-sizes') : __('Regenerating media library thumbnails using selected sizes.', 'image-sizes')} {progress < 100 && ( {__('Cancel', 'image-sizes')} )}

{ percentFormat( progress ) }
} value={ numberFormat( totalImages ) } label={__('Total Images', 'image-sizes')} /> } value={ numberFormat( regenStats.processed ) } label={__('Images Processed', 'image-sizes')} /> } value={ numberFormat( regenStats.failed || 0 ) } label={__('Images Failed', 'image-sizes')} accent="red" /> } value={ numberFormat( regenStats.not_found || 0 ) } label={__('Images Not Found', 'image-sizes')} /> } value={ numberFormat( totalThumbnails ) } label={__('Total Thumbnails', 'image-sizes')} /> } value={ numberFormat( regenStats.deleted ) } label={__('Images Deleted', 'image-sizes')} /> } value={ numberFormat( regenStats.created ) } label={__('Images Created', 'image-sizes')} /> } value={regenStats.space_saved_label || formatBytes(0)} label={__('Space Saved', 'image-sizes')} />
)} ); }