Introduced functionality to render interactive signature buttons on the PDF viewer. Added support for fetching and displaying signature data (`SignatureDto`) dynamically based on the current page. - Added `@using` directives in `EnvelopeViewer.razor` for required namespaces. - Introduced `_signatures` field to store signature data. - Updated `OnInitializedAsync` to fetch and process signatures. - Implemented `RenderSignatureButtonsAsync` to dynamically render buttons. - Added `[JSInvokable]` method `OnSignatureButtonClick` for button events. - Updated CSS to style `pdf-signature-layer` and `signature-button`. - Enhanced `pdf-viewer.js` with methods to render and clear buttons. - Ensured buttons respond to zoom and page navigation changes. - Added error handling and logging for signature rendering. These changes improve user interaction by enabling signing functionality directly on the PDF viewer.
461 lines
22 KiB
Plaintext
461 lines
22 KiB
Plaintext
@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
|
||
@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 id="pdf-signature-layer" class="pdf-signature-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;
|
||
IReadOnlyList<SignatureDto> _signatures = [];
|
||
|
||
// 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);
|
||
|
||
} 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();
|
||
|
||
// 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<bool>("pdfViewer.nextPage")) {
|
||
_currentPage = await JSRuntime.InvokeAsync<int>("pdfViewer.getCurrentPage");
|
||
await RenderSignatureButtonsAsync();
|
||
}
|
||
}
|
||
|
||
async Task PreviousPage() {
|
||
if (await JSRuntime.InvokeAsync<bool>("pdfViewer.previousPage")) {
|
||
_currentPage = await JSRuntime.InvokeAsync<int>("pdfViewer.getCurrentPage");
|
||
await RenderSignatureButtonsAsync();
|
||
}
|
||
}
|
||
|
||
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;
|
||
await RenderSignatureButtonsAsync();
|
||
}
|
||
}
|
||
|
||
async Task RenderSignatureButtonsAsync() {
|
||
if (_signatures.Count == 0 || !_pdfLoaded) return;
|
||
|
||
try {
|
||
await JSRuntime.InvokeVoidAsync("pdfViewer.renderSignatureButtons", _signatures, _currentPage, _dotNetRef);
|
||
} catch (Exception ex) {
|
||
System.Diagnostics.Debug.WriteLine($"Signature button rendering error: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
[JSInvokable]
|
||
public void OnSignatureButtonClick(int signatureId) {
|
||
Console.WriteLine($"Signature #{signatureId} signed");
|
||
}
|
||
|
||
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();
|
||
}
|
||
}
|