/* FreeSize — Scan view */ const { useState, useEffect, useRef, useMemo, useCallback } = React; function VolumeSelect({ volumes, value, onChange, className }) { const [open, setOpen] = useState(false); const ref = useRef(null); const cur = volumes.find((v) => v.id === value) || volumes[0]; const { fmt, heatColor } = window.FZ; useEffect(() => { const h = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }; document.addEventListener("mousedown", h); return () => document.removeEventListener("mousedown", h); }, []); const usedPct = (v) => ((v.total - v.free) / v.total) * 100; return (
{open && (
{volumes.map((v) => (
{ onChange(v.id); setOpen(false); }}>
{v.label} {fmt(v.free)} frei
{v.caption} · {fmt(v.total)}
))}
)}
); } // ---- Sunburst panel (radial space-filling, heatmap-coloured) ---- function Sunburst({ root, globalMax, onDrill, selected, onSelect }) { const { heatColor, heatT, fmt } = window.FZ; const [ref, size] = useSize(); const cx = size.w / 2, cy = size.h / 2; const radius = Math.max(0, Math.min(size.w, size.h) / 2 - 6); const hole = radius * 0.22; const maxRings = 4; const ringW = (radius - hole) / maxRings; const pt = (r, a) => [cx + r * Math.cos(a), cy + r * Math.sin(a)]; function annular(r0, r1, a0, a1) { const g = 0.004; a0 += g; a1 -= g; if (a1 <= a0) a1 = a0 + 0.001; const large = (a1 - a0) > Math.PI ? 1 : 0; const [x0o, y0o] = pt(r1, a0), [x1o, y1o] = pt(r1, a1), [x1i, y1i] = pt(r0, a1), [x0i, y0i] = pt(r0, a0); return `M${x0o.toFixed(2)} ${y0o.toFixed(2)} A${r1} ${r1} 0 ${large} 1 ${x1o.toFixed(2)} ${y1o.toFixed(2)} ` + `L${x1i.toFixed(2)} ${y1i.toFixed(2)} A${r0} ${r0} 0 ${large} 0 ${x0i.toFixed(2)} ${y0i.toFixed(2)} Z`; } const slices = []; function recurse(node, ring, a0, a1) { if (ring >= maxRings) return; const kids = (node.children || []).filter((c) => c.size > 0).sort((a, b) => b.size - a.size); let total = 0; kids.forEach((k) => (total += k.size)); if (total <= 0) return; const inner = hole + ring * ringW, outer = inner + ringW - 1.2; let a = a0; kids.forEach((k) => { const span = (a1 - a0) * (k.size / total), b = a + span; if (span >= 0.012) { slices.push({ node: k, d: annular(inner, outer, a, b) }); recurse(k, ring + 1, a, b); } a = b; }); } if (radius > 4 && root) recurse(root, 0, -Math.PI / 2, -Math.PI / 2 + 2 * Math.PI); return (
{radius > 4 && root && ( {slices.map((s) => { const isDir = s.node.type === "dir"; const drill = isDir && s.node.children && s.node.children.length; return ( { onSelect(s.node.id); if (drill) onDrill(s.node); }}> {s.node.name + " · " + fmt(s.node.size)} ); })} {root.name.length > 14 ? root.name.slice(0, 13) + "…" : root.name} {fmt(root.size)} )}
); } window.Sunburst = Sunburst; // deep prune by min size function pruneTree(node, minBytes) { if (node.type === "file") return node.size >= minBytes ? node : null; const kids = (node.children || []).map((c) => pruneTree(c, minBytes)).filter(Boolean); return { ...node, children: kids }; } function maxNodeSize(node, skipRoot) { let m = skipRoot ? 0 : node.size; (node.children || []).forEach((c) => { m = Math.max(m, maxNodeSize(c, false)); }); return m; } function findNode(node, id) { if (node.id === id) return node; for (const c of (node.children || [])) { const r = findNode(c, id); if (r) return r; } return null; } function pathTo(node, id, acc) { acc = acc || []; if (node.id === id) return [...acc, node]; for (const c of (node.children || [])) { const r = pathTo(c, id, [...acc, node]); if (r) return r; } return null; } function ScanView({ tweaks, nav }) { const volumes = window.VOLUMES; const [volume, setVolume] = useState("c"); const curVol = volumes.find((v) => v.id === volume); const [path, setPath] = useState(curVol.path); const [minSize, setMinSize] = useState(50); const [scanState, setScanState] = useState("idle"); // idle|scanning|done const [progress, setProgress] = useState({ files: 0, dirs: 0, bytes: 0, errors: 0, cur: "" }); const [expanded, setExpanded] = useState(new Set()); const [selected, setSelected] = useState(null); const [vizRootId, setVizRootId] = useState(null); const [vizMode, setVizMode] = useState("treemap"); // treemap|sunburst const timer = useRef(null); const { fmt, fmtInt, heatColor, heatT } = window.FZ; useEffect(() => { setPath(volumes.find((v) => v.id === volume).path); }, [volume]); const fullTree = useMemo(() => pruneTree(window.SCAN_DATA, minSize * 1024 * 1024), [minSize]); const globalMax = useMemo(() => maxNodeSize(fullTree, true), [fullTree]); const target = useMemo(() => ({ files: window.SCAN_DATA.files, dirs: window.SCAN_DATA.dirs, bytes: window.SCAN_DATA.size, errors: 7 }), []); const showTree = scanState !== "idle"; const vizRoot = useMemo(() => (vizRootId ? findNode(fullTree, vizRootId) || fullTree : fullTree), [vizRootId, fullTree]); const crumbs = useMemo(() => pathTo(fullTree, vizRoot.id, []) || [fullTree], [fullTree, vizRoot]); const samplePaths = [ "C:\\Users\\max\\AppData\\Local\\Docker", "C:\\Program Files (x86)\\Steam\\steamapps", "C:\\Windows\\WinSxS", "C:\\Users\\max\\Videos", "C:\\Users\\max\\Documents\\Projekte\\freesize\\node_modules", "C:\\ProgramData\\Package Cache", "C:\\hiberfil.sys", ]; const startScan = useCallback(() => { clearInterval(timer.current); setScanState("scanning"); setSelected(null); setVizRootId(null); setProgress({ files: 0, dirs: 0, bytes: 0, errors: 0, cur: samplePaths[0] }); setExpanded(new Set([window.SCAN_DATA.id])); const dur = 1900; const start = performance.now(); timer.current = setInterval(() => { const e = Math.min(1, (performance.now() - start) / dur); const ease = 1 - Math.pow(1 - e, 2.2); setProgress({ files: Math.round(target.files * ease), dirs: Math.round(target.dirs * ease), bytes: target.bytes * ease, errors: Math.round(target.errors * Math.min(1, e * 1.3)), cur: samplePaths[Math.floor(e * (samplePaths.length - 0.001))], }); if (e >= 1) { clearInterval(timer.current); setProgress({ ...target, cur: "" }); setScanState("done"); } }, 90); }, [target]); const cancelScan = useCallback(() => { clearInterval(timer.current); setScanState((s) => (s === "scanning" ? "done" : s)); setProgress((p) => ({ ...p, cur: "" })); }, []); useEffect(() => () => clearInterval(timer.current), []); const toggle = useCallback((id) => { setExpanded((prev) => { const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); return n; }); }, []); const reveal = useCallback((node) => { setSelected(node.id); }, []); const drill = useCallback((node) => { setVizRootId(node.id); }, []); const rowH = tweaks.density === "compact" ? 28 : tweaks.density === "comfy" ? 40 : 34; return (
{/* merged app bar: brand + scan controls + view tabs */}
setPath(e.target.value)} spellCheck={false} placeholder="Pfad oder \\Server\Freigabe …" />
setMinSize(Math.max(0, +e.target.value || 0))} title="Minimale Grösse (MB)" /> MB
{scanState === "scanning" ? : }
{/* status line */} {showTree && (
{scanState === "scanning" ? Scanne… : Fertig} {fmtInt(progress.files)} Dateien · {fmtInt(progress.dirs)} Ordner · {fmt(progress.bytes)} {progress.errors > 0 && ( {fmtInt(progress.errors)} ohne Zugriff )} {scanState === "scanning" && progress.cur && ( {progress.cur} )}
)} {/* body */} {!showTree ? (

Bereit zum Scannen

Volume oder Pfad wählen und „Scannen“ klicken. FreeSize zeigt dir dann, was deinen Speicher belegt — grösste Elemente zuerst.

) : (
Name Anteil Grösse
{fullTree.children && fullTree.children.length ? ( fullTree.children.map((c) => ( )) ) : (

Keine Einträge über der Mindestgrösse.

)}
{/* visual panel */}
Visualisierung {fmt(vizRoot.size)}
{crumbs.map((c, i) => ( {i > 0 && } i < crumbs.length - 1 && setVizRootId(c.id === fullTree.id ? null : c.id)}> {i === 0 ? "Wurzel" : c.name} ))}
{vizMode === "treemap" ? : }
klein gross
)}
); } window.ScanView = ScanView;