Spaces:
Sleeping
Sleeping
| /** | |
| * DownloadWorkspaceButton — FIX-6 | |
| * Bottone compatto stile Replit per scaricare il VFS come ZIP. | |
| * Da montare nella StatusBar (slot destra). | |
| */ | |
| import { useState, useEffect, useCallback } from "react"; | |
| import { vfsAsync, onVfsChanged } from "@/lib/vfsDb"; | |
| import { downloadBlob } from "@/lib/fileConverter"; | |
| export function DownloadWorkspaceButton() { | |
| const [fileCount, setFileCount] = useState(0); | |
| const [state, setState] = useState<"idle" | "loading" | "done">("idle"); | |
| const reload = useCallback(() => { | |
| vfsAsync.list().then(f => setFileCount(f.length)).catch(() => {}); | |
| }, []); | |
| useEffect(() => { | |
| reload(); | |
| return onVfsChanged(reload); | |
| }, [reload]); | |
| useEffect(() => { | |
| if (state !== "done") return; | |
| const t = setTimeout(() => setState("idle"), 2000); | |
| return () => clearTimeout(t); | |
| }, [state]); | |
| if (fileCount === 0) return null; | |
| const handleClick = async () => { | |
| if (state !== "idle") return; | |
| setState("loading"); | |
| try { | |
| const { default: JSZip } = await import("jszip"); | |
| const zip = new JSZip(); | |
| const files = await vfsAsync.list(); | |
| for (const f of files) { | |
| const c = f.opfs ? (await vfsAsync.read(f.path) ?? "") : f.content; | |
| zip.file(f.path.startsWith("/") ? f.path.slice(1) : f.path, c); | |
| } | |
| const blob = await zip.generateAsync({ type: "blob", compression: "DEFLATE", compressionOptions: { level: 6 } }); | |
| downloadBlob(blob, `workspace-${Date.now()}.zip`); | |
| setState("done"); | |
| } catch { | |
| setState("idle"); | |
| } | |
| }; | |
| const isDone = state === "done"; | |
| const isLoading = state === "loading"; | |
| return ( | |
| <button | |
| onClick={handleClick} | |
| disabled={isLoading} | |
| title={`Scarica workspace (${fileCount} file) come ZIP`} | |
| style={{ | |
| all: "unset", | |
| display: "flex", | |
| alignItems: "center", | |
| gap: 4, | |
| height: "100%", | |
| padding: "0 10px", | |
| borderLeft: "1px solid rgba(255,255,255,0.04)", | |
| fontSize: "0.62rem", | |
| fontFamily: '"JetBrains Mono", ui-monospace, monospace', | |
| fontWeight: 600, | |
| letterSpacing: "0.025em", | |
| color: isDone ? "#34d399" : "rgba(148,163,184,0.55)", | |
| cursor: isLoading ? "wait" : "pointer", | |
| transition: "color 0.15s", | |
| whiteSpace: "nowrap", | |
| WebkitTapHighlightColor: "transparent", | |
| } as React.CSSProperties} | |
| onPointerEnter={e => { | |
| if (!isLoading && !isDone) | |
| (e.currentTarget as HTMLButtonElement).style.color = "rgba(148,163,184,0.85)"; | |
| }} | |
| onPointerLeave={e => { | |
| if (!isDone) | |
| (e.currentTarget as HTMLButtonElement).style.color = "rgba(148,163,184,0.55)"; | |
| }} | |
| > | |
| {isDone ? ( | |
| <svg width={9} height={9} viewBox="0 0 24 24" fill="none" | |
| stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"> | |
| <polyline points="20 6 9 17 4 12" /> | |
| </svg> | |
| ) : isLoading ? ( | |
| <svg width={9} height={9} viewBox="0 0 24 24" fill="none" | |
| stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" | |
| style={{ animation: "sb-spin 0.8s linear infinite" }}> | |
| <path d="M21 12a9 9 0 1 1-6.219-8.56" /> | |
| </svg> | |
| ) : ( | |
| <svg width={9} height={9} viewBox="0 0 24 24" fill="none" | |
| stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"> | |
| <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /> | |
| <polyline points="7 10 12 15 17 10" /> | |
| <line x1="12" y1="15" x2="12" y2="3" /> | |
| </svg> | |
| )} | |
| {isDone ? "salvato" : isLoading ? "zip…" : "zip"} | |
| <style>{`@keyframes sb-spin{to{transform:rotate(360deg)}}`}</style> | |
| </button> | |
| ); | |
| } | |
| export default DownloadWorkspaceButton; | |