1use crate::category::FileCategory;
2use crate::scan::format_size;
3
4fn build_legend_html() -> String {
7 FileCategory::ALL
8 .iter()
9 .map(|cat| {
10 format!(
11 r#"<span class="item"><span class="swatch" style="background:{}"></span>{}</span>"#,
12 cat.color(),
13 cat.label()
14 )
15 })
16 .collect::<Vec<_>>()
17 .join("\n ")
18}
19
20pub fn generate_html(
30 tree_json: &str,
31 total_size: u64,
32 file_count: usize,
33 scan_path: &str,
34 leaf_depth: u16,
35) -> String {
36 let total_size_str = format_size(total_size);
37 let legend_html = build_legend_html();
38 format!(
39 r##"<!DOCTYPE html>
40<html lang="en">
41<head>
42<meta charset="utf-8">
43<title>dumap — {scan_path}</title>
44<style>
45 * {{ margin: 0; padding: 0; box-sizing: border-box; }}
46 body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #1a1a2e; color: #e0e0e0; }}
47 #header {{ padding: 12px 24px; background: #16213e; border-bottom: 1px solid #0f3460; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px; }}
48 #header h1 {{ font-size: 18px; font-weight: 600; }}
49 #header .stats {{ font-size: 14px; color: #a0a0a0; }}
50 #legend {{ display: flex; gap: 12px; flex-wrap: wrap; align-items: center; }}
51 #legend .item {{ display: flex; align-items: center; gap: 4px; font-size: 11px; color: #b0b0b0; }}
52 #legend .swatch {{ width: 10px; height: 10px; border-radius: 2px; display: inline-block; }}
53 #chart {{ width: 100%; height: calc(100vh - 80px); }}
54</style>
55</head>
56<body>
57<div id="header">
58 <div style="display:flex;align-items:center;gap:24px;">
59 <h1>dumap — {scan_path}</h1>
60 <div id="legend">
61 {legend_html}
62 </div>
63 </div>
64 <div class="stats">{file_count} files · {total_size_str} total</div>
65</div>
66<div id="chart"></div>
67<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
68<script>
69var chart = echarts.init(document.getElementById('chart'));
70var data = {tree_json};
71
72function formatBytes(b) {{
73 if (b >= 1099511627776) return (b/1099511627776).toFixed(1)+' TB';
74 if (b >= 1073741824) return (b/1073741824).toFixed(1)+' GB';
75 if (b >= 1048576) return (b/1048576).toFixed(1)+' MB';
76 if (b >= 1024) return (b/1024).toFixed(1)+' KB';
77 return b+' B';
78}}
79
80chart.setOption({{
81 tooltip: {{
82 formatter: function(info) {{
83 var val = info.value;
84 var path = info.treePathInfo.map(function(n){{ return n.name; }}).join('/');
85 return '<b>' + echarts.format.encodeHTML(path) + '</b><br/>' + formatBytes(val);
86 }}
87 }},
88 series: [{{
89 type: 'treemap',
90 data: data,
91 leafDepth: {leaf_depth},
92 roam: false,
93 breadcrumb: {{
94 top: 4,
95 left: 10,
96 itemStyle: {{ color: '#16213e', borderColor: '#0f3460' }},
97 textStyle: {{ color: '#e0e0e0', fontSize: 13 }}
98 }},
99 label: {{
100 show: true,
101 formatter: function(p) {{
102 return p.name + '\n' + formatBytes(p.value);
103 }},
104 fontSize: 12,
105 color: '#fff'
106 }},
107 upperLabel: {{
108 show: true,
109 height: 24,
110 color: '#fff',
111 fontSize: 13,
112 backgroundColor: 'transparent'
113 }},
114 itemStyle: {{
115 borderColor: '#1a1a2e',
116 borderWidth: 2,
117 gapWidth: 1
118 }},
119 levels: [
120 {{ itemStyle: {{ borderColor: '#555', borderWidth: 4, gapWidth: 4 }}, upperLabel: {{ show: false }} }},
121 {{ itemStyle: {{ borderColor: '#444', borderWidth: 2, gapWidth: 2 }}, upperLabel: {{ show: true }} }},
122 {{ itemStyle: {{ borderColor: '#333', borderWidth: 1, gapWidth: 1 }}, upperLabel: {{ show: true }} }},
123 {{ colorSaturation: [0.4, 0.8], itemStyle: {{ borderColor: '#2a2a3e', borderWidth: 1, gapWidth: 1 }} }}
124 ]
125 }}]
126}});
127
128window.addEventListener('resize', function() {{ chart.resize(); }});
129</script>
130</body>
131</html>"##
132 )
133}
134
135#[cfg(test)]
136#[path = "html_tests.rs"]
137mod html_tests;