/* 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 && (
)}
);
}
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 */}
{/* 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"
?
: }
)}
);
}
window.ScanView = ScanView;