@page "/envelope/{EnvelopeKey}" @using EnvelopeGenerator.ReceiverUI.Models @using EnvelopeGenerator.ReceiverUI.Models.Constants @using EnvelopeGenerator.ReceiverUI.Services @using Microsoft.Extensions.Options @using EnvelopeGenerator.ReceiverUI.Options @using Microsoft.JSInterop @using DevExpress.Blazor @inject DocumentService DocumentService @inject NavigationManager Navigation @inject IOptions AppOptions @inject IOptions PdfViewerOptions @inject IJSRuntime JSRuntime @inject SignatureService SignatureService @implements IAsyncDisposable
Dokumentenansicht
ID: @EnvelopeKey
@if (_isLoading) {
L�dt...

Dokument wird geladen...

} else if (_errorMessage is not null) {
Fehler beim Laden des Dokuments

@_errorMessage

} else if (!string.IsNullOrWhiteSpace(_pdfDataUrl)) {
@if (_pdfLoaded) {
/ @_totalPages
@(_currentZoom)%
@if (_totalSignatures > 0) {
@_signedSignatures  /  @_totalSignatures @if (_unsignedSignatures > 0) { @_unsignedSignatures offen } else { ✓ Komplett }
}
}
@if (_pdfLoaded && _showThumbnails) {
@for (int i = 1; i <= _totalPages; i++) { var pageNum = i;
@pageNum
}
}
} else {
Dokument konnte nicht geladen werden.
}
@if(_activeSignatureTab == SignatureTabDraw) {

Bitte unterschreiben Sie im folgenden Feld.

} else if(_activeSignatureTab == SignatureTabText) {

Geben Sie Ihre Unterschrift als Text ein und wählen Sie eine Schriftart.

} else {

Laden Sie ein Bild Ihrer Unterschrift hoch.

}
@if(!string.IsNullOrWhiteSpace(_popupValidationMessage)) {
@_popupValidationMessage
}
@code { // Signature tab constants const string SignatureTabDraw = "draw"; const string SignatureTabText = "text"; const string SignatureTabImage = "image"; const string DrawCanvasId = "envelope-signature-pad"; const string TypedCanvasId = "envelope-typed-signature-pad"; const string ImageInputId = "envelope-signature-image-input"; const string ImageCanvasId = "envelope-image-signature-pad"; readonly (string Text, string Value)[] TypedSignatureFonts = { ("Brush Script", "'Brush Script MT', cursive"), ("Segoe Script", "'Segoe Script', cursive"), ("Lucida Handwriting", "'Lucida Handwriting', cursive"), ("Comic Sans", "'Comic Sans MS', cursive"), ("Cursive", "cursive") }; [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? _dotNetRef; IReadOnlyList _signatures = []; // Signature navigation state int _totalSignatures = 0; int _signedSignatures = 0; int _unsignedSignatures = 0; // Signature state record SignatureCapture(string DataUrl, string FullName, string Position, string Place); SignatureCapture? _capturedSignature; bool _signaturePopupVisible = false; string? _popupValidationMessage; string _activeSignatureTab = SignatureTabDraw; string _typedSignatureText = string.Empty; string _typedSignatureFont = "'Brush Script MT', cursive"; string _signerFullName = string.Empty; string _signerPosition = string.Empty; string _signaturePlace = string.Empty; // 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); _signatures = signatures.Convert(UnitOfLength.Point); await JSRuntime.InvokeVoidAsync("console.log", "Loaded signatures:", _signatures); // Open signature popup on page load _activeSignatureTab = SignatureTabDraw; _signaturePopupVisible = true; _popupValidationMessage = null; } 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("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("pdfViewer.initialize", "pdf-canvas", _pdfDataUrl, _dotNetRef); if (success) { _pdfLoaded = true; _totalPages = await JSRuntime.InvokeAsync("pdfViewer.getTotalPages"); _currentPage = await JSRuntime.InvokeAsync("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(); // Render signature buttons await RenderSignatureButtonsAsync(); } } 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); // Re-render signature buttons when zoom changes await Task.Delay(100); await RenderSignatureButtonsAsync(); } async Task NextPage() { if (await JSRuntime.InvokeAsync("pdfViewer.nextPage")) { _currentPage = await JSRuntime.InvokeAsync("pdfViewer.getCurrentPage"); await RenderSignatureButtonsAsync(); } } async Task PreviousPage() { if (await JSRuntime.InvokeAsync("pdfViewer.previousPage")) { _currentPage = await JSRuntime.InvokeAsync("pdfViewer.getCurrentPage"); await RenderSignatureButtonsAsync(); } } async Task ZoomIn() { if (_currentZoom >= 300) return; await JSRuntime.InvokeVoidAsync("pdfViewer.zoomIn"); var scale = await JSRuntime.InvokeAsync("pdfViewer.getScale"); _currentZoom = (int)(scale * 100); } async Task ZoomOut() { if (_currentZoom <= 50) return; await JSRuntime.InvokeVoidAsync("pdfViewer.zoomOut"); var scale = await JSRuntime.InvokeAsync("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("pdfViewer.goToPage", pageNum)) { _currentPage = pageNum; } } } async Task FitToWidth() { await JSRuntime.InvokeVoidAsync("pdfViewer.fitToWidth"); var scale = await JSRuntime.InvokeAsync("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("pdfViewer.goToPage", pageNum)) { _currentPage = pageNum; await RenderSignatureButtonsAsync(); } } async Task RenderSignatureButtonsAsync() { if (_signatures.Count == 0 || !_pdfLoaded) return; try { await JSRuntime.InvokeVoidAsync("pdfViewer.renderSignatureButtons", _signatures, _currentPage, _dotNetRef); await UpdateSignatureCounterAsync(); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"Signature button rendering error: {ex.Message}"); } } [JSInvokable] public async Task OnSignatureButtonClick(int signatureId) { if (_capturedSignature == null) { // No signature captured yet - should not happen as popup is shown on page load return; } // Apply signature to PDF canvas await JSRuntime.InvokeVoidAsync("pdfViewer.applySignature", signatureId, _capturedSignature.DataUrl, _capturedSignature.FullName, _capturedSignature.Position, _capturedSignature.Place); // Update counter await UpdateSignatureCounterAsync(); } [JSInvokable] public async Task OnSignatureNavChanged() { await UpdateSignatureCounterAsync(); } async Task UpdateSignatureCounterAsync() { try { var state = await JSRuntime.InvokeAsync("pdfViewer.getSignatureNavState"); _totalSignatures = state.Total; _signedSignatures = state.Signed; _unsignedSignatures = state.Unsigned; await InvokeAsync(StateHasChanged); } catch { // Ignore errors during counter update } } async Task GoToPreviousSignature() { await JSRuntime.InvokeVoidAsync("pdfViewer.goToPreviousSignature", _dotNetRef); } async Task GoToNextSignature() { await JSRuntime.InvokeVoidAsync("pdfViewer.goToNextSignature", _dotNetRef); } record SignatureNavState(int Total, int Signed, int Unsigned, int CurrentIndex, bool CanGoPrev, bool CanGoNext); // Signature popup methods void OpenSignaturePopup() { _activeSignatureTab = SignatureTabDraw; _signaturePopupVisible = true; _popupValidationMessage = null; } async Task OnPopupShownAsync() { await InitializeActiveSignatureTabAsync(); } async Task SetSignatureTabAsync(string tab) { _activeSignatureTab = tab; _popupValidationMessage = null; await InvokeAsync(StateHasChanged); await Task.Delay(50); await InitializeActiveSignatureTabAsync(); } async Task InitializeActiveSignatureTabAsync() { if(_activeSignatureTab == SignatureTabDraw) { await JSRuntime.InvokeVoidAsync("receiverSignature.initialize", DrawCanvasId); } else if(_activeSignatureTab == SignatureTabText) { await JSRuntime.InvokeVoidAsync("receiverSignature.initializeTyped", TypedCanvasId); await RenderTypedSignatureAsync(); } else { await JSRuntime.InvokeVoidAsync("receiverSignature.initializeImage", ImageInputId, ImageCanvasId); } } async Task RenewSignatureAsync() { _popupValidationMessage = null; if(_activeSignatureTab == SignatureTabDraw) { await JSRuntime.InvokeVoidAsync("receiverSignature.clear", DrawCanvasId); } else if(_activeSignatureTab == SignatureTabText) { _typedSignatureText = string.Empty; await JSRuntime.InvokeVoidAsync("receiverSignature.clearTyped", TypedCanvasId); } else { await JSRuntime.InvokeVoidAsync("receiverSignature.clearImage", ImageInputId, ImageCanvasId); } } async Task OnTypedSignatureChanged(Microsoft.AspNetCore.Components.ChangeEventArgs args) { _typedSignatureText = args.Value?.ToString() ?? string.Empty; await RenderTypedSignatureAsync(); } async Task OnTypedSignatureFontChanged(Microsoft.AspNetCore.Components.ChangeEventArgs args) { _typedSignatureFont = args.Value?.ToString() ?? _typedSignatureFont; await RenderTypedSignatureAsync(); } async Task RenderTypedSignatureAsync() { await JSRuntime.InvokeVoidAsync("receiverSignature.renderTypedSignature", TypedCanvasId, _typedSignatureText, _typedSignatureFont); } async Task SaveSignatureAsync() { if (string.IsNullOrWhiteSpace(_signerFullName)) { _popupValidationMessage = "Bitte geben Sie Vor- und Nachname ein."; return; } if (string.IsNullOrWhiteSpace(_signaturePlace)) { _popupValidationMessage = "Bitte geben Sie den Ort ein."; return; } var signatureDataUrl = await GetActiveSignatureDataUrlAsync(); if (string.IsNullOrWhiteSpace(signatureDataUrl)) { _popupValidationMessage = "Die Unterschrift ist erforderlich."; return; } _popupValidationMessage = null; _capturedSignature = new(signatureDataUrl, _signerFullName.Trim(), _signerPosition.Trim(), _signaturePlace.Trim()); _signaturePopupVisible = false; await InvokeAsync(StateHasChanged); Console.WriteLine($"Signature saved: {_signerFullName}, {_signaturePlace}"); } async Task GetActiveSignatureDataUrlAsync() { if(_activeSignatureTab == SignatureTabDraw) return await JSRuntime.InvokeAsync("receiverSignature.getDataUrl", DrawCanvasId); if(_activeSignatureTab == SignatureTabText) { await RenderTypedSignatureAsync(); return await JSRuntime.InvokeAsync("receiverSignature.getTypedDataUrl", TypedCanvasId); } return await JSRuntime.InvokeAsync("receiverSignature.getImageDataUrl", ImageCanvasId); } 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(); } }