groups, ReportBranding? branding = null)
{
var T = TranslationSource.Instance;
var sb = new StringBuilder();
sb.AppendLine("");
sb.AppendLine("");
sb.AppendLine("");
sb.AppendLine("");
sb.AppendLine("");
sb.AppendLine($"{T["report.title.duplicates"]}");
sb.AppendLine("""
""");
sb.Append(BrandingHtmlHelper.BuildBrandingHeader(branding));
sb.AppendLine($"{T["report.title.duplicates_short"]}
");
sb.AppendLine($"{groups.Count:N0} {T["report.text.duplicate_groups_found"]}
");
for (int i = 0; i < groups.Count; i++)
{
var g = groups[i];
int count = g.Items.Count;
string badgeClass = "badge-dup";
sb.AppendLine($"""
| {T["report.col.number"]} |
{T["report.col.name"]} |
{T["report.col.library"]} |
{T["report.col.path"]} |
{T["report.col.size"]} |
{T["report.col.created"]} |
{T["report.col.modified"]} |
""");
for (int j = 0; j < g.Items.Count; j++)
{
var item = g.Items[j];
string size = item.SizeBytes.HasValue ? FormatSize(item.SizeBytes.Value) : string.Empty;
string created = item.Created.HasValue ? item.Created.Value.ToString("yyyy-MM-dd") : string.Empty;
string modified = item.Modified.HasValue ? item.Modified.Value.ToString("yyyy-MM-dd") : string.Empty;
sb.AppendLine($"""
| {j + 1} |
{H(item.Name)} |
{H(item.Library)} |
{H(item.Path)} |
{size} |
{created} |
{modified} |
""");
}
sb.AppendLine("""
""");
}
sb.AppendLine($"{T["report.text.generated_colon"]} {DateTime.Now:yyyy-MM-dd HH:mm}
");
sb.AppendLine("");
return sb.ToString();
}
/// Writes the HTML report to the specified file path using UTF-8.
public async Task WriteAsync(IReadOnlyList groups, string filePath, CancellationToken ct, ReportBranding? branding = null)
{
var html = BuildHtml(groups, branding);
await System.IO.File.WriteAllTextAsync(filePath, html, Encoding.UTF8, ct);
}
///
/// Writes one or more HTML reports depending on and
/// . Single → one file. BySite + SeparateFiles → one
/// file per site. BySite + SingleTabbed → one file with per-site iframe tabs.
///
public async Task WriteAsync(
IReadOnlyList groups,
string basePath,
ReportSplitMode splitMode,
HtmlSplitLayout layout,
CancellationToken ct,
ReportBranding? branding = null)
{
if (splitMode != ReportSplitMode.BySite)
{
await WriteAsync(groups, basePath, ct, branding);
return;
}
var partitions = DuplicatesCsvExportService.PartitionBySite(groups).ToList();
if (layout == HtmlSplitLayout.SingleTabbed)
{
var parts = partitions
.Select(p => (p.Label, Html: BuildHtml(p.Partition, branding)))
.ToList();
var T = TranslationSource.Instance;
var tabbed = ReportSplitHelper.BuildTabbedHtml(parts, T["report.title.duplicates_short"]);
await System.IO.File.WriteAllTextAsync(basePath, tabbed, Encoding.UTF8, ct);
return;
}
foreach (var partition in partitions)
{
ct.ThrowIfCancellationRequested();
var path = ReportSplitHelper.BuildPartitionPath(basePath, partition.Label);
await WriteAsync(partition.Partition, path, ct, branding);
}
}
private static string H(string value) =>
System.Net.WebUtility.HtmlEncode(value ?? string.Empty);
private static string FormatSize(long bytes)
{
if (bytes >= 1_073_741_824L) return $"{bytes / 1_073_741_824.0:F2} GB";
if (bytes >= 1_048_576L) return $"{bytes / 1_048_576.0:F2} MB";
if (bytes >= 1024L) return $"{bytes / 1024.0:F2} KB";
return $"{bytes} B";
}
}