Files
EnvelopeGenerator/EnvelopeGenerator.ReceiverUI/Pages/EnvelopeViewer.razor
TekH dec2b81afe Refactor to use SignatureDto and SignatureService
Replaced AnnotationDto and AnnotationService with SignatureDto
and SignatureService for handling signature data. Marked
AnnotationDto and AnnotationService as obsolete.

Added the SignatureDto class to represent signature data and
introduced the SignatureService class to fetch signature data
from the API. Updated EnvelopeViewer.razor to use
SignatureService, replacing AnnotationService, and added a
debug log for retrieved signatures.

Performed general refactoring to align with the new signature
data model and functionality.
2026-06-06 19:14:42 +02:00

430 lines
21 KiB
Plaintext
Raw Blame History

@page "/envelope/{EnvelopeKey}"
@using EnvelopeGenerator.ReceiverUI.Services
@using Microsoft.Extensions.Options
@using EnvelopeGenerator.ReceiverUI.Options
@using Microsoft.JSInterop
@inject DocumentService DocumentService
@inject NavigationManager Navigation
@inject IOptions<ApiOptions> AppOptions
@inject IOptions<PdfViewerOptions> PdfViewerOptions
@inject IJSRuntime JSRuntime
@inject SignatureService SignatureService
@implements IAsyncDisposable
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
<link href="css/envelope-viewer.css" rel="stylesheet" />
<link href="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf_viewer.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<script src="js/pdf-viewer.js"></script>
<div class="envelope-viewer-layout">
<div class="envelope-action-bar">
<div class="envelope-action-bar__inner">
<div class="d-flex align-items-center gap-3">
<div class="envelope-logo">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 16 16">
<path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z"/>
</svg>
</div>
<div>
<div class="envelope-title">Dokumentenansicht</div>
<div class="envelope-key">ID: @EnvelopeKey</div>
</div>
</div>
</div>
</div>
<div class="envelope-content">
@if (_isLoading) {
<div class="d-flex justify-content-center align-items-center h-100">
<div class="text-center">
<div class="spinner-border text-white mb-3" style="width: 3.5rem; height: 3.5rem;" role="status">
<span class="visually-hidden">L<>dt...</span>
</div>
<p class="text-white fw-semibold">Dokument wird geladen...</p>
</div>
</div>
} else if (_errorMessage is not null) {
<div class="error-container">
<div class="alert alert-danger shadow-lg">
<div class="d-flex align-items-start">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" class="me-3 flex-shrink-0" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z"/>
</svg>
<div>
<h5 class="mb-2">Fehler beim Laden des Dokuments</h5>
<p class="mb-0">@_errorMessage</p>
</div>
</div>
</div>
</div>
} else if (!string.IsNullOrWhiteSpace(_pdfDataUrl)) {
<div class="pdf-viewer-container">
@if (_pdfLoaded) {
<div class="pdf-toolbar">
<div class="pdf-toolbar__section">
<button class="pdf-toolbar__btn pdf-toolbar__btn--toggle" @onclick="ToggleThumbnails" title="@(_showThumbnails ? "Seitenleiste ausblenden" : "Seitenleiste einblenden")">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
<path d="M1 2.5A1.5 1.5 0 0 1 2.5 1h3A1.5 1.5 0 0 1 7 2.5v3A1.5 1.5 0 0 1 5.5 7h-3A1.5 1.5 0 0 1 1 5.5v-3zm8 0A1.5 1.5 0 0 1 10.5 1h3A1.5 1.5 0 0 1 15 2.5v3A1.5 1.5 0 0 1 13.5 7h-3A1.5 1.5 0 0 1 9 5.5v-3zm-8 8A1.5 1.5 0 0 1 2.5 9h3A1.5 1.5 0 0 1 7 10.5v3A1.5 1.5 0 0 1 5.5 15h-3A1.5 1.5 0 0 1 1 13.5v-3zm8 0A1.5 1.5 0 0 1 10.5 9h3a1.5 1.5 0 0 1 1.5 1.5v3a1.5 1.5 0 0 1-1.5 1.5h-3A1.5 1.5 0 0 1 9 13.5v-3z"/>
</svg>
</button>
</div>
<div class="pdf-toolbar__divider"></div>
<div class="pdf-toolbar__section">
<button class="pdf-toolbar__btn" @onclick="PreviousPage" disabled="@(_currentPage <= 1)" title="Vorherige Seite">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z"/>
</svg>
</button>
<div class="pdf-toolbar__page-input-group">
<input type="number" class="pdf-toolbar__page-input" min="1" max="@_totalPages" value="@_currentPage" @onchange="OnPageInputChanged" />
<span class="pdf-toolbar__page-total">/ @_totalPages</span>
</div>
<button class="pdf-toolbar__btn" @onclick="NextPage" disabled="@(_currentPage >= _totalPages)" title="Nächste Seite">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"/>
</svg>
</button>
</div>
<div class="pdf-toolbar__divider"></div>
<div class="pdf-toolbar__section pdf-toolbar__zoom-section">
<button class="pdf-toolbar__btn" @onclick="ZoomOut" disabled="@(_currentZoom <= 50)" title="Verkleinern">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0zM4 6a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1H4z"/>
</svg>
</button>
<div class="pdf-toolbar__zoom-slider-container">
<input type="range" class="pdf-toolbar__zoom-slider" min="50" max="300" step="@(PdfViewerOptions.Value.ZoomStepPercentage)" value="@_currentZoom" @oninput="OnZoomSliderChanged" title="@(_currentZoom)%" />
<div class="pdf-toolbar__zoom-label">@(_currentZoom)%</div>
</div>
<button class="pdf-toolbar__btn" @onclick="ZoomIn" disabled="@(_currentZoom >= 300)" title="Vergrößern">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0zM6.5 3a.5.5 0 0 0-1 0v2.5H3a.5.5 0 0 0 0 1h2.5V9a.5.5 0 0 0 1 0V6.5H9a.5.5 0 0 0 0-1H6.5V3z"/>
</svg>
</button>
</div>
<div class="pdf-toolbar__divider"></div>
<div class="pdf-toolbar__section">
<button class="pdf-toolbar__btn pdf-toolbar__btn--preset" @onclick="() => SetZoom(100)" title="Tatsächliche Größe">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16" class="me-1">
<path d="M8 4.754a3.246 3.246 0 1 0 0 6.492 3.246 3.246 0 0 0 0-6.492zM5.754 8a2.246 2.246 0 1 1 4.492 0 2.246 2.246 0 0 1-4.492 0z"/>
<path d="M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 0 1-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 0 1-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 0 1 .52 1.255l-.16.292c-.892 1.64.901 3.434 2.541 2.54l.292-.159a.873.873 0 0 1 1.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 0 1 1.255-.52l.292.16c1.64.893 3.434-.902 2.54-2.541l-.159-.292a.873.873 0 0 1 .52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 0 1-.52-1.255l.16-.292c.893-1.64-.902-3.433-2.541-2.54l-.292.159a.873.873 0 0 1-1.255-.52l-.094-.319z"/>
</svg>
100%
</button>
<button class="pdf-toolbar__btn pdf-toolbar__btn--preset" @onclick="FitToWidth" title="An Breite anpassen">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16" class="me-1">
<path d="M1 3.5a.5.5 0 0 1 .5-.5h13a.5.5 0 0 1 0 1h-13a.5.5 0 0 1-.5-.5zM7.5 7a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1h-1z"/>
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2zm1 0v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1z"/>
</svg>
Breite
</button>
</div>
</div>
}
<div class="pdf-frame">
@if (_pdfLoaded && _showThumbnails) {
<!-- PDF Thumbnail Sidebar -->
<div class="pdf-thumbnails" style="width: @(_thumbnailWidth)px">
<div class="pdf-thumbnails__content">
@for (int i = 1; i <= _totalPages; i++) {
var pageNum = i;
<div class="pdf-thumbnail @(pageNum == _currentPage ? "pdf-thumbnail--active" : "")" @onclick="() => GoToPageFromThumbnail(pageNum)">
<div class="pdf-thumbnail__preview">
<canvas id="thumb-canvas-@pageNum" class="pdf-thumbnail__canvas"></canvas>
</div>
<div class="pdf-thumbnail__label">@pageNum</div>
</div>
}
</div>
</div>
<!-- Resizable Splitter -->
<div class="pdf-splitter @(_isResizing ? "resizing" : "")"
@onmousedown="OnSplitterMouseDown"
@onmousedown:preventDefault="true">
</div>
}
<div class="pdf-canvas-wrapper">
<div class="pdf-page-container">
<canvas id="pdf-canvas" class="pdf-canvas"></canvas>
<div id="pdf-text-layer" class="pdf-text-layer"></div>
</div>
</div>
</div>
</div>
} else {
<div class="error-container">
<div class="alert alert-warning shadow-lg">
<div class="d-flex align-items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" class="me-3" viewBox="0 0 16 16">
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
</svg>
<span class="fs-5">Dokument konnte nicht geladen werden.</span>
</div>
</div>
</div>
}
</div>
</div>
@code {
[Parameter] public string? EnvelopeKey { get; set; }
bool _isLoading = true;
string? _errorMessage;
string? _pdfDataUrl;
bool _pdfLoaded = false;
int _currentPage = 1;
int _totalPages = 0;
int _currentZoom = 150;
bool _showThumbnails = true;
DotNetObjectReference<EnvelopeViewer>? _dotNetRef;
// Resizable splitter state
int _thumbnailWidth = 260;
bool _isResizing = false;
int _resizeStartX = 0;
int _resizeStartWidth = 0;
const int MinThumbnailWidth = 150;
const int MaxThumbnailWidth = 400;
protected override async Task OnInitializedAsync() {
if (string.IsNullOrWhiteSpace(EnvelopeKey)) {
_errorMessage = "Envelope-Schlüssel fehlt.";
_isLoading = false;
return;
}
try {
var (pdfBytes, statusCode) = await DocumentService.GetDocumentAsync(EnvelopeKey);
if (pdfBytes is { Length: > 0 }) {
var base64 = Convert.ToBase64String(pdfBytes);
_pdfDataUrl = $"data:application/pdf;base64,{base64}";
} else {
_errorMessage = $"Dokument konnte nicht geladen werden. HTTP Status: {statusCode}";
}
var signatures = await SignatureService.GetAsync(EnvelopeKey);
await JSRuntime.InvokeVoidAsync("console.log", signatures);
} catch (Exception ex) {
_errorMessage = $"Fehler: {ex.Message}";
}
_isLoading = false;
await InvokeAsync(StateHasChanged);
}
protected override async Task OnAfterRenderAsync(bool firstRender) {
if (firstRender) {
// Load saved thumbnail width from localStorage
try {
var savedWidth = await JSRuntime.InvokeAsync<string>("localStorage.getItem", "envelopeViewer_thumbnailWidth");
if (!string.IsNullOrEmpty(savedWidth) && int.TryParse(savedWidth, out var width)) {
_thumbnailWidth = Math.Clamp(width, MinThumbnailWidth, MaxThumbnailWidth);
await InvokeAsync(StateHasChanged);
}
} catch {
// Ignore localStorage errors
}
}
if (!_pdfLoaded && !string.IsNullOrWhiteSpace(_pdfDataUrl)) {
await Task.Delay(500);
try {
_dotNetRef = DotNetObjectReference.Create(this);
// Send quality options to JavaScript
var options = PdfViewerOptions.Value;
await JSRuntime.InvokeVoidAsync("pdfViewer.setQualityOptions", new
{
options.ThumbnailBaseScale,
options.ThumbnailEnableHiDPI,
options.ThumbnailMaxDPR,
options.MainCanvasEnableHiDPI,
options.MainCanvasMaxDPR,
options.EnableSmoothZoom,
options.ZoomTransitionDuration,
options.RenderingOpacity,
options.ZoomStepPercentage
});
var success = await JSRuntime.InvokeAsync<bool>("pdfViewer.initialize", "pdf-canvas", _pdfDataUrl, _dotNetRef);
if (success) {
_pdfLoaded = true;
_totalPages = await JSRuntime.InvokeAsync<int>("pdfViewer.getTotalPages");
_currentPage = await JSRuntime.InvokeAsync<int>("pdfViewer.getCurrentPage");
// Attach resize listeners
await JSRuntime.InvokeVoidAsync("pdfViewer.attachResizeListeners", _dotNetRef);
await InvokeAsync(StateHasChanged);
// Wait for DOM to be ready, then render thumbnails
await Task.Delay(100);
await RenderThumbnailsAsync();
}
} catch (Exception ex) {
_errorMessage = $"PDF.js Fehler: {ex.Message}";
await InvokeAsync(StateHasChanged);
}
}
}
[JSInvokable]
public async Task OnZoomChanged(double scale)
{
_currentZoom = (int)(scale * 100);
await InvokeAsync(StateHasChanged);
}
async Task NextPage() {
if (await JSRuntime.InvokeAsync<bool>("pdfViewer.nextPage")) {
_currentPage = await JSRuntime.InvokeAsync<int>("pdfViewer.getCurrentPage");
}
}
async Task PreviousPage() {
if (await JSRuntime.InvokeAsync<bool>("pdfViewer.previousPage")) {
_currentPage = await JSRuntime.InvokeAsync<int>("pdfViewer.getCurrentPage");
}
}
async Task ZoomIn() {
if (_currentZoom >= 300) return;
await JSRuntime.InvokeVoidAsync("pdfViewer.zoomIn");
var scale = await JSRuntime.InvokeAsync<double>("pdfViewer.getScale");
_currentZoom = (int)(scale * 100);
}
async Task ZoomOut() {
if (_currentZoom <= 50) return;
await JSRuntime.InvokeVoidAsync("pdfViewer.zoomOut");
var scale = await JSRuntime.InvokeAsync<double>("pdfViewer.getScale");
_currentZoom = (int)(scale * 100);
}
async Task SetZoom(int percentage) {
var scale = percentage / 100.0;
await JSRuntime.InvokeVoidAsync("pdfViewer.setScale", scale);
_currentZoom = percentage;
}
async Task OnZoomSliderChanged(ChangeEventArgs e) {
if (int.TryParse(e.Value?.ToString(), out var zoom)) {
await SetZoom(zoom);
}
}
async Task OnPageInputChanged(ChangeEventArgs e) {
if (int.TryParse(e.Value?.ToString(), out var pageNum) && pageNum >= 1 && pageNum <= _totalPages) {
if (await JSRuntime.InvokeAsync<bool>("pdfViewer.goToPage", pageNum)) {
_currentPage = pageNum;
}
}
}
async Task FitToWidth() {
await JSRuntime.InvokeVoidAsync("pdfViewer.fitToWidth");
var scale = await JSRuntime.InvokeAsync<double>("pdfViewer.getScale");
_currentZoom = (int)(scale * 100);
}
async Task ToggleThumbnails() {
_showThumbnails = !_showThumbnails;
// Re-render thumbnails when showing them
if (_showThumbnails && _pdfLoaded) {
await InvokeAsync(StateHasChanged); // Force UI update first
await Task.Delay(150); // Wait for DOM to render canvas elements
await RenderThumbnailsAsync();
}
}
async Task GoToPageFromThumbnail(int pageNum) {
if (await JSRuntime.InvokeAsync<bool>("pdfViewer.goToPage", pageNum)) {
_currentPage = pageNum;
}
}
async Task RenderThumbnailsAsync() {
try {
var delay = PdfViewerOptions.Value.ThumbnailRenderDelay;
// Sequential rendering to avoid overwhelming the browser
for (int i = 1; i <= _totalPages; i++) {
await JSRuntime.InvokeVoidAsync("pdfViewer.renderThumbnail", i, $"thumb-canvas-{i}");
// Configurable delay between renders
if (i < _totalPages) {
await Task.Delay(delay);
}
}
} catch (Exception ex) {
// Thumbnail rendering is not critical
System.Diagnostics.Debug.WriteLine($"Thumbnail rendering error: {ex.Message}");
}
}
// Resizable splitter methods
void OnSplitterMouseDown(MouseEventArgs e) {
_isResizing = true;
_resizeStartX = (int)e.ClientX;
_resizeStartWidth = _thumbnailWidth;
// Add resizing class to body to prevent text selection
_ = JSRuntime.InvokeVoidAsync("eval", "document.body.classList.add('resizing')");
_ = JSRuntime.InvokeVoidAsync("pdfViewer.startResize");
}
[JSInvokable]
public async Task OnSplitterMouseMove(int clientX) {
if (!_isResizing) return;
var delta = clientX - _resizeStartX;
var newWidth = _resizeStartWidth + delta;
// Clamp to min/max
_thumbnailWidth = Math.Clamp(newWidth, MinThumbnailWidth, MaxThumbnailWidth);
await InvokeAsync(StateHasChanged);
}
[JSInvokable]
public async Task OnSplitterMouseUp() {
if (!_isResizing) return;
_isResizing = false;
// Remove resizing class from body
await JSRuntime.InvokeVoidAsync("eval", "document.body.classList.remove('resizing')");
// Save preference to localStorage
await JSRuntime.InvokeVoidAsync("localStorage.setItem", "envelopeViewer_thumbnailWidth", _thumbnailWidth.ToString());
await InvokeAsync(StateHasChanged);
}
public async ValueTask DisposeAsync() {
if (_pdfLoaded) {
try {
await JSRuntime.InvokeVoidAsync("pdfViewer.dispose");
} catch {
// Ignore errors during disposal
}
}
_dotNetRef?.Dispose();
}
}