'use client'; import React from 'react'; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Progress } from '@/components/ui/progress'; import { Button } from '@/components/ui/button'; import { Download, Package, Settings, Rocket, CheckCircle2, XCircle, Loader2, } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { usePluginInstallTasks, InstallStage, PluginInstallTask, } from './PluginInstallTaskContext'; import { cn } from '@/lib/utils'; const STAGES: { key: InstallStage; icon: React.ElementType; i18nKey: string; }[] = [ { key: InstallStage.DOWNLOADING, icon: Download, i18nKey: 'plugins.installProgress.downloading', }, { key: InstallStage.INSTALLING_DEPS, icon: Package, i18nKey: 'plugins.installProgress.installingDeps', }, { key: InstallStage.INITIALIZING, icon: Settings, i18nKey: 'plugins.installProgress.initializing', }, { key: InstallStage.LAUNCHING, icon: Rocket, i18nKey: 'plugins.installProgress.launching', }, ]; function getStageIndex(stage: InstallStage): number { const idx = STAGES.findIndex((s) => s.key === stage); return idx >= 0 ? idx : -1; } function formatFileSize(bytes: number): string { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return (bytes / Math.pow(k, i)).toFixed(1) + ' ' + sizes[i]; } /** * A single stage row — used in both active (single) and completed (all) views. */ function StageRow({ icon: Icon, label, isActive, isCompleted, isError, detail, }: { icon: React.ElementType; label: string; isActive: boolean; isCompleted: boolean; isError: boolean; detail?: React.ReactNode; }) { return (
{/* Left: status indicator */}
{isCompleted ? ( ) : isError && isActive ? ( ) : isActive ? ( ) : ( )}
{/* Middle: label + detail */}
{label} {/* Small icon after text */}
{detail && (
{detail}
)}
); } function formatSpeed(bytesPerSec: number): string { if (bytesPerSec === 0) return '0 B/s'; const k = 1024; const sizes = ['B/s', 'KB/s', 'MB/s', 'GB/s']; const i = Math.floor(Math.log(bytesPerSec) / Math.log(k)); return (bytesPerSec / Math.pow(k, i)).toFixed(1) + ' ' + sizes[i]; } function TaskProgressContent({ task }: { task: PluginInstallTask }) { const { t } = useTranslation(); const currentStageIndex = getStageIndex(task.stage); const isDone = task.stage === InstallStage.DONE; const isError = task.stage === InstallStage.ERROR; /** Build detail node for a stage */ const getStageDetail = ( stageKey: InstallStage, isCompletedView: boolean, ): React.ReactNode | undefined => { if (stageKey === InstallStage.DOWNLOADING) { // Show download progress: current / total + speed const dlTotal = task.downloadTotal || task.fileSize; const dlCurrent = task.downloadCurrent; const dlSpeed = task.downloadSpeed; if (isCompletedView && dlTotal) { // Done view: just show total size return t('plugins.installProgress.downloadSize', { size: formatFileSize(dlTotal), }); } if (dlTotal && dlCurrent != null) { const parts: string[] = []; parts.push(`${formatFileSize(dlCurrent)} / ${formatFileSize(dlTotal)}`); if (dlSpeed && dlSpeed > 0) { parts.push(formatSpeed(dlSpeed)); } return parts.join(' · '); } if (dlTotal) { return t('plugins.installProgress.downloadSize', { size: formatFileSize(dlTotal), }); } return undefined; } if (stageKey === InstallStage.INSTALLING_DEPS) { const total = task.depsTotal; const installed = task.depsInstalled; const remaining = task.depsRemaining; const currentDep = task.currentDep; const dlSize = task.depsDownloadedSize; const speed = task.depsSpeed; if (isCompletedView && total != null) { const parts: string[] = []; parts.push(t('plugins.installProgress.depsInfo', { count: total })); if (dlSize && dlSize > 0) { parts.push(formatFileSize(dlSize)); } return parts.join(' · '); } if (total != null && installed != null) { const parts: string[] = []; parts.push( t('plugins.installProgress.depsProgress', { installed, total, remaining: remaining ?? total - installed, }), ); if (dlSize && dlSize > 0) { parts.push(formatFileSize(dlSize)); } if (speed && speed > 0) { parts.push(formatSpeed(speed)); } if (currentDep) { return ( <> {parts.join(' · ')}
{currentDep} ); } return parts.join(' · '); } if (total != null) { return t('plugins.installProgress.depsInfo', { count: total }); } return undefined; } return undefined; }; return (
{/* Overall progress bar — always blue */}
{isDone ? t('plugins.installProgress.completed') : isError ? t('plugins.installProgress.failed') : t('plugins.installProgress.overallProgress')} {isDone ? '100%' : `${task.overallProgress}%`}
div]:bg-blue-500 dark:[&>div]:bg-blue-400', 'bg-blue-100 dark:bg-blue-900/30', isDone && '[&>div]:bg-green-500 dark:[&>div]:bg-green-400 bg-green-100 dark:bg-green-900/30', isError && '[&>div]:bg-red-500 dark:[&>div]:bg-red-400 bg-red-100 dark:bg-red-900/30', )} />
{/* Stage display */}
{isDone ? /* When done: show all stages with completed style */ STAGES.map((stageConfig) => ( )) : isError ? /* Error: show the failed stage */ currentStageIndex >= 0 && ( ) : /* In progress: only show the current active stage */ currentStageIndex >= 0 && ( )}
{/* Done banner */} {isDone && (
{t('plugins.installProgress.installComplete')}
)} {/* Error detail */} {isError && task.error && (

{task.error}

)}
); } export default function PluginInstallProgressDialog() { const { t } = useTranslation(); const { tasks, selectedTaskId, setSelectedTaskId, removeTask } = usePluginInstallTasks(); const selectedTask = tasks.find((t) => t.id === selectedTaskId) || null; const open = !!selectedTask; const handleClose = () => { setSelectedTaskId(null); }; const handleDismiss = () => { if (selectedTask) { if ( selectedTask.stage === InstallStage.DONE || selectedTask.stage === InstallStage.ERROR ) { removeTask(selectedTask.id); } } setSelectedTaskId(null); }; return ( !o && handleClose()}> {selectedTask ? t('plugins.installProgress.title', { name: selectedTask.pluginName, }) : t('plugins.installProgress.titleGeneric')} {selectedTask && }
{selectedTask && (selectedTask.stage === InstallStage.DONE || selectedTask.stage === InstallStage.ERROR) && ( )}
); }