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 f4cc81bb71
64 changed files with 3315 additions and 405 deletions
@@ -1,6 +1,7 @@
using System.IO;
using System.Text;
using SharepointToolbox.Core.Models;
using SharepointToolbox.Localization;
namespace SharepointToolbox.Services.Export;
@@ -13,15 +14,16 @@ public class SearchHtmlExportService
{
public string BuildHtml(IReadOnlyList<SearchResult> results, 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.search"]}</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 File Search Results</title>
<style>
body { font-family: 'Segoe UI', Arial, sans-serif; font-size: 13px; margin: 20px; background: #f5f5f5; }
h1 { color: #0078d4; }
@@ -45,27 +47,27 @@ public class SearchHtmlExportService
<body>
""");
sb.Append(BrandingHtmlHelper.BuildBrandingHeader(branding));
sb.AppendLine("""
<h1>File Search Results</h1>
sb.AppendLine($"""
<h1>{T["report.title.search_short"]}</h1>
<div class="toolbar">
<label for="filterInput">Filter:</label>
<input id="filterInput" type="text" placeholder="Filter rows…" oninput="filterTable()" />
<label for="filterInput">{T["report.filter.label"]}</label>
<input id="filterInput" type="text" placeholder="{T["report.filter.placeholder_rows"]}" oninput="filterTable()" />
<span id="resultCount"></span>
</div>
""");
sb.AppendLine("""
sb.AppendLine($"""
<table id="resultsTable">
<thead>
<tr>
<th onclick="sortTable(0)">File Name</th>
<th onclick="sortTable(1)">Extension</th>
<th onclick="sortTable(2)">Path</th>
<th onclick="sortTable(3)">Created</th>
<th onclick="sortTable(4)">Created By</th>
<th onclick="sortTable(5)">Modified</th>
<th onclick="sortTable(6)">Modified By</th>
<th class="num" onclick="sortTable(7)">Size</th>
<th onclick="sortTable(0)">{T["report.col.file_name"]}</th>
<th onclick="sortTable(1)">{T["report.col.extension"]}</th>
<th onclick="sortTable(2)">{T["report.col.path"]}</th>
<th onclick="sortTable(3)">{T["report.col.created"]}</th>
<th onclick="sortTable(4)">{T["report.col.created_by"]}</th>
<th onclick="sortTable(5)">{T["report.col.modified"]}</th>
<th onclick="sortTable(6)">{T["report.col.modified_by"]}</th>
<th class="num" onclick="sortTable(7)">{T["report.col.size"]}</th>
</tr>
</thead>
<tbody>
@@ -93,7 +95,7 @@ public class SearchHtmlExportService
sb.AppendLine(" </tbody>\n</table>");
int count = results.Count;
sb.AppendLine($"<p class=\"generated\">Generated: {DateTime.Now:yyyy-MM-dd HH:mm} — {count:N0} result(s)</p>");
sb.AppendLine($"<p class=\"generated\">{T["report.text.generated_colon"]} {DateTime.Now:yyyy-MM-dd HH:mm} — {count:N0} {T["report.text.results_parens"]}</p>");
sb.AppendLine($$"""
<script>
@@ -126,10 +128,10 @@ public class SearchHtmlExportService
rows[i].className = match ? '' : 'hidden';
if (match) visible++;
}
document.getElementById('resultCount').innerText = q ? (visible + ' of {{count:N0}} shown') : '';
document.getElementById('resultCount').innerText = q ? (visible + ' {{T["report.text.of"]}} {{count:N0}} {{T["report.text.shown"]}}') : '';
}
window.onload = function() {
document.getElementById('resultCount').innerText = '{{count:N0}} result(s)';
document.getElementById('resultCount').innerText = '{{count:N0}} {{T["report.text.results_parens"]}}';
};
</script>
</body></html>