+ {/* 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 (
+ <>
+
+ {/* Overall progress bar — always blue */}
+
+
+
+ {isDone
+ ? t('plugins.installProgress.completed')
+ : isError
+ ? t('plugins.installProgress.failed')
+ : t('plugins.installProgress.overallProgress')}
+
+
+ {isDone ? '100%' : `${task.overallProgress}%`}
+
+
+
+
+ {/* 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 && (
+
+ )}
+
+ );
+}
+
+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 (
+