chore: release v2.4

- Add theme system (Dark/Light palettes, ModernTheme, ThemeManager)
- Add InputDialog, Spinner common view
- Add DuplicatesCsvExportService
- Refresh views, dialogs, and view models across tabs
- Update localization strings (en/fr)
- Tweak services (transfer, permissions, search, user access, ownership elevation, bulk operations)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dev
2026-04-20 11:23:11 +02:00
parent 8f30a60d2a
commit 12dd1de9f2
93 changed files with 8708 additions and 1159 deletions
@@ -1,4 +1,5 @@
using SharepointToolbox.Core.Models;
using SharepointToolbox.Localization;
using System.Text;
namespace SharepointToolbox.Services.Export;
@@ -10,17 +11,23 @@ namespace SharepointToolbox.Services.Export;
/// </summary>
public class DuplicatesHtmlExportService
{
/// <summary>
/// Builds a self-contained HTML string rendering one collapsible card per
/// <see cref="DuplicateGroup"/>. The document ships with inline CSS and a
/// tiny JS toggle so no external assets are needed.
/// </summary>
public string BuildHtml(IReadOnlyList<DuplicateGroup> groups, ReportBranding? branding = null)
{
var T = TranslationSource.Instance;
var sb = new StringBuilder();
sb.AppendLine("<!DOCTYPE html>");
sb.AppendLine("<html lang=\"en\">");
sb.AppendLine("<head>");
sb.AppendLine("<meta charset=\"UTF-8\">");
sb.AppendLine("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">");
sb.AppendLine($"<title>{T["report.title.duplicates"]}</title>");
sb.AppendLine("""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SharePoint Duplicate Detection Report</title>
<style>
body { font-family: 'Segoe UI', Arial, sans-serif; font-size: 13px; margin: 20px; background: #f5f5f5; }
h1 { color: #0078d4; }
@@ -54,11 +61,9 @@ public class DuplicatesHtmlExportService
<body>
""");
sb.Append(BrandingHtmlHelper.BuildBrandingHeader(branding));
sb.AppendLine("""
<h1>Duplicate Detection Report</h1>
""");
sb.AppendLine($"<h1>{T["report.title.duplicates_short"]}</h1>");
sb.AppendLine($"<p class=\"summary\">{groups.Count:N0} duplicate group(s) found.</p>");
sb.AppendLine($"<p class=\"summary\">{groups.Count:N0} {T["report.text.duplicate_groups_found"]}</p>");
for (int i = 0; i < groups.Count; i++)
{
@@ -70,19 +75,19 @@ public class DuplicatesHtmlExportService
<div class="group-card">
<div class="group-header" onclick="toggleGroup({i})">
<span class="group-name">{H(g.Name)}</span>
<span class="badge {badgeClass}">{count} copies</span>
<span class="badge {badgeClass}">{count} {T["report.text.copies"]}</span>
</div>
<div class="group-body" id="gb-{i}">
<table>
<thead>
<tr>
<th>#</th>
<th>Name</th>
<th>Library</th>
<th>Path</th>
<th>Size</th>
<th>Created</th>
<th>Modified</th>
<th>{T["report.col.number"]}</th>
<th>{T["report.col.name"]}</th>
<th>{T["report.col.library"]}</th>
<th>{T["report.col.path"]}</th>
<th>{T["report.col.size"]}</th>
<th>{T["report.col.created"]}</th>
<th>{T["report.col.modified"]}</th>
</tr>
</thead>
<tbody>
@@ -116,18 +121,59 @@ public class DuplicatesHtmlExportService
""");
}
sb.AppendLine($"<p class=\"generated\">Generated: {DateTime.Now:yyyy-MM-dd HH:mm}</p>");
sb.AppendLine($"<p class=\"generated\">{T["report.text.generated_colon"]} {DateTime.Now:yyyy-MM-dd HH:mm}</p>");
sb.AppendLine("</body></html>");
return sb.ToString();
}
/// <summary>Writes the HTML report to the specified file path using UTF-8.</summary>
public async Task WriteAsync(IReadOnlyList<DuplicateGroup> groups, string filePath, CancellationToken ct, ReportBranding? branding = null)
{
var html = BuildHtml(groups, branding);
await System.IO.File.WriteAllTextAsync(filePath, html, Encoding.UTF8, ct);
}
/// <summary>
/// Writes one or more HTML reports depending on <paramref name="splitMode"/> and
/// <paramref name="layout"/>. Single → one file. BySite + SeparateFiles → one
/// file per site. BySite + SingleTabbed → one file with per-site iframe tabs.
/// </summary>
public async Task WriteAsync(
IReadOnlyList<DuplicateGroup> 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);