Reduced delay in `OnZoomChanged` to improve responsiveness when rendering signature buttons. Added calls to `RenderSignatureButtonsAsync` in zoom-related methods to ensure signature overlays update dynamically. Refactored `pdf-viewer.js` to introduce `appliedSignatureElements` for better management of applied signatures. Added `scaleAppliedSignature` and `updateAppliedSignaturePositions` methods to dynamically scale and position applied signatures based on zoom level and page. Enhanced signature button rendering by scaling dimensions (width, height, font size, icon size) proportionally with zoom. Added attributes to store base values for applied signature containers to facilitate scaling. Improved handling of applied signatures to ensure proper scaling, positioning, and visibility during zoom and page navigation. These changes enhance user experience and maintain consistency across zoom levels.
942 lines
49 KiB
Plaintext
942 lines
49 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
|
||
@using DevExpress.Blazor
|
||
@inject DocumentService DocumentService
|
||
@inject NavigationManager Navigation
|
||
@inject IOptions<ApiOptions> AppOptions
|
||
@inject IOptions<PdfViewerOptions> PdfViewerOptions
|
||
@inject IJSRuntime JSRuntime
|
||
@inject SignatureService SignatureService
|
||
@inject EnvelopeGenerator.ReceiverUI.Services.AuthService AuthService
|
||
@inject EnvelopeGenerator.ReceiverUI.Services.EnvelopeReceiverService EnvelopeReceiverService
|
||
@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>
|
||
<script src="js/receiver-signature.js"></script>
|
||
|
||
<div class="envelope-viewer-layout">
|
||
<div class="envelope-action-bar">
|
||
<div class="envelope-action-bar__inner" style="flex-direction: column; align-items: stretch; padding: 0.35rem 1.5rem; gap: 0.35rem;">
|
||
@* Row 1: Title + Sender + Badges + Logout *@
|
||
<div style="display: flex; align-items: center; justify-content: space-between; gap: 1rem;">
|
||
@* Left: Title + Sender *@
|
||
<div style="flex: 0 1 auto; min-width: 0; display: flex; align-items: center; gap: 0.75rem;">
|
||
@if (_envelopeReceiver is not null) {
|
||
<div style="font-size: 0.9rem; font-weight: 600; color: #1f2937; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
||
@(_envelopeReceiver.Envelope?.Title ?? "Dokument")
|
||
</div>
|
||
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.FullName) || !string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.Email)) {
|
||
<span style="font-size: 0.7rem; color: #6b7280; white-space: nowrap;">
|
||
Von
|
||
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.FullName)) {
|
||
<span style="font-weight: 500; color: #374151;">@_envelopeReceiver.Envelope.User.FullName</span>
|
||
}
|
||
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.Email)) {
|
||
<span><@_envelopeReceiver.Envelope.User.Email></span>
|
||
}
|
||
@if (_envelopeReceiver.Envelope?.AddedWhen != null) {
|
||
<span> · @_envelopeReceiver.Envelope.AddedWhen.ToString("dd.MM.yyyy")</span>
|
||
}
|
||
</span>
|
||
}
|
||
} else {
|
||
<div style="font-size: 0.9rem; font-weight: 600; color: #1f2937;">Dokumentenansicht</div>
|
||
}
|
||
</div>
|
||
|
||
@* Right: Badges + Logout *@
|
||
<div class="d-flex align-items-center" style="gap: 0.75rem; flex: 0 0 auto;">
|
||
@if (_envelopeReceiver is not null) {
|
||
<div class="d-flex flex-wrap align-items-center" style="gap: 0.3rem; font-size: 0.7rem;">
|
||
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Name)) {
|
||
<span style="display: inline-flex; align-items: center; padding: 0.125rem 0.4rem; background: #f3f4f6; border-radius: 0.25rem; color: #374151; white-space: nowrap;">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="9" height="9" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||
<path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0Zm4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4Z"/>
|
||
</svg>
|
||
@_envelopeReceiver.Name
|
||
</span>
|
||
}
|
||
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.FullName)) {
|
||
<span style="display: inline-flex; align-items-center; padding: 0.15rem 0.5rem; background: #f3f4f6; border-radius: 0.25rem; color: #6b7280; white-space: nowrap;">
|
||
Von @_envelopeReceiver.Envelope.User.FullName
|
||
</span>
|
||
}
|
||
@{
|
||
int sigCount = _signatures.Count;
|
||
}
|
||
@if (sigCount > 0) {
|
||
<span style="display: inline-flex; align-items: center; padding: 0.125rem 0.4rem; background: #ede9fe; border-radius: 0.25rem; color: #6d28d9; font-weight: 500; white-space: nowrap;">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="9" height="9" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
||
</svg>
|
||
@sigCount
|
||
</span>
|
||
}
|
||
@if (_envelopeReceiver.Envelope?.UseAccessCode == true) {
|
||
<span style="display: inline-flex; align-items: center; padding: 0.125rem 0.4rem; background: #fef3c7; border-radius: 0.25rem; color: #92400e; font-weight: 500; white-space: nowrap;">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="9" height="9" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z"/>
|
||
</svg>
|
||
Code
|
||
</span>
|
||
}
|
||
@if (_envelopeReceiver.Envelope?.TFAEnabled == true) {
|
||
<span style="display: inline-flex; align-items: center; padding: 0.125rem 0.4rem; background: #dbeafe; border-radius: 0.25rem; color: #1e40af; font-weight: 500; white-space: nowrap;">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="9" height="9" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||
<path d="M5.338 1.59a61.44 61.44 0 0 0-2.837.856.481.481 0 0 0-.328.39c-.554 4.157.726 7.19 2.253 9.188a10.725 10.725 0 0 0 2.287 2.233c.346.244.652.42.893.533.12.057.218.095.293.118a.55.55 0 0 0 .101.025.615.615 0 0 0 .1-.025c.076-.023.174-.061.294-.118.24-.113.547-.29.893-.533a10.726 10.726 0 0 0 2.287-2.233c1.527-1.997 2.807-5.031 2.253-9.188a.48.48 0 0 0-.328-.39c-.651-.213-1.75-.56-2.837-.855C9.552 1.29 8.531 1.067 8 1.067c-.53 0-1.552.223-2.662.524zM5.072.56C6.157.265 7.31 0 8 0s1.843.265 2.928.56c1.11.3 2.229.655 2.887.87a1.54 1.54 0 0 1 1.044 1.262c.596 4.477-.787 7.795-2.465 9.99a11.775 11.775 0 0 1-2.517 2.453 7.159 7.159 0 0 1-1.048.625c-.28.132-.581.24-.829.24s-.548-.108-.829-.24a7.158 7.158 0 0 1-1.048-.625 11.777 11.777 0 0 1-2.517-2.453C1.928 10.487.545 7.169 1.141 2.692A1.54 1.54 0 0 1 2.185 1.43 62.456 62.456 0 0 1 5.072.56z"/>
|
||
<path d="M10.854 5.146a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 1 1 .708-.708L7.5 7.793l2.646-2.647a.5.5 0 0 1 .708 0z"/>
|
||
</svg>
|
||
2FA
|
||
</span>
|
||
}
|
||
</div>
|
||
|
||
}
|
||
|
||
@* Logout button *@
|
||
@if (!string.IsNullOrWhiteSpace(EnvelopeKey)) {
|
||
<button class="pdf-toolbar__btn" @onclick="LogoutAsync" disabled="@_isLoggingOut" title="Abmelden" style="flex-shrink: 0;">
|
||
@if (_isLoggingOut) {
|
||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="width: 14px; height: 14px;"></span>
|
||
} else {
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||
<path fill-rule="evenodd" d="M10 12.5a.5.5 0 0 1-.5.5h-8a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 .5.5v2a.5.5 0 0 0 1 0v-2A1.5 1.5 0 0 0 9.5 2h-8A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h8a1.5 1.5 0 0 0 1.5-1.5v-2a.5.5 0 0 0-1 0v2z"/>
|
||
<path fill-rule="evenodd" d="M15.854 8.354a.5.5 0 0 0 0-.708l-3-3a.5.5 0 0 0-.708.708L14.293 7.5H5.5a.5.5 0 0 0 0 1h8.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3z"/>
|
||
</svg>
|
||
}
|
||
</button>
|
||
}
|
||
</div>
|
||
</div>
|
||
|
||
@* Row 2: Messages (visible text) *@
|
||
@if (_envelopeReceiver is not null && (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.Message) || !string.IsNullOrWhiteSpace(_envelopeReceiver.PrivateMessage))) {
|
||
<div style="display: flex; align-items: flex-start; gap: 0.5rem; font-size: 0.7rem; padding-top: 0.15rem; border-top: 1px solid #e5e7eb;">
|
||
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.Message)) {
|
||
<div style="flex: 1; min-width: 0; padding: 0.2rem 0.4rem; background: #f9fafb; border-radius: 0.25rem; border-left: 2px solid #9ca3af; display: flex; align-items: flex-start; gap: 0.25rem;">
|
||
<span style="font-weight: 500; color: #374151; flex-shrink: 0;">📧</span>
|
||
<span style="color: #6b7280; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">@_envelopeReceiver.Envelope.Message</span>
|
||
</div>
|
||
}
|
||
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.PrivateMessage)) {
|
||
<div style="flex: 1; min-width: 0; padding: 0.2rem 0.4rem; background: #fef3c7; border-radius: 0.25rem; border-left: 2px solid #f59e0b; display: flex; align-items: flex-start; gap: 0.25rem;">
|
||
<span style="font-weight: 500; color: #92400e; flex-shrink: 0;">🔒</span>
|
||
<span style="color: #92400e; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">@_envelopeReceiver.PrivateMessage</span>
|
||
</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>
|
||
|
||
@if (_totalSignatures > 0) {
|
||
<div class="pdf-toolbar__section pdf-toolbar__signature-nav">
|
||
<button class="pdf-toolbar__btn pdf-toolbar__btn--signature-nav"
|
||
@onclick="GoToPreviousSignature"
|
||
disabled="@(_totalSignatures == 0)"
|
||
title="Vorherige Unterschrift">
|
||
<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__signature-counter">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
||
</svg>
|
||
<span class="pdf-toolbar__signature-counter-text">
|
||
@if (_currentSignatureIndex > 0) {
|
||
<span style="color: #4F46E5; font-weight: 600;">#@_currentSignatureIndex</span>
|
||
<span style="opacity: 0.4; margin: 0 0.35rem;">|</span>
|
||
}
|
||
<strong style="color: @(_unsignedSignatures > 0 ? "#4F46E5" : "#10b981");">@_signedSignatures</strong>
|
||
<span style="opacity: 0.6;"> / </span>
|
||
<span>@_totalSignatures</span>
|
||
</span>
|
||
@if (_unsignedSignatures > 0) {
|
||
<span class="pdf-toolbar__signature-badge">@_unsignedSignatures offen</span>
|
||
} else {
|
||
<span class="pdf-toolbar__signature-badge pdf-toolbar__signature-badge--complete">✓ Komplett</span>
|
||
}
|
||
</div>
|
||
|
||
<button class="pdf-toolbar__btn pdf-toolbar__btn--signature-nav"
|
||
@onclick="GoToNextSignature"
|
||
disabled="@(_totalSignatures == 0)"
|
||
title="Nächste Unterschrift">
|
||
<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>
|
||
}
|
||
<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>
|
||
|
||
<DxPopup @bind-Visible="_signaturePopupVisible"
|
||
HeaderText="Unterschrift erstellen"
|
||
Width="620px"
|
||
MaxWidth="95vw"
|
||
ShowFooter="true"
|
||
CloseOnOutsideClick="false"
|
||
ShowCloseButton="false"
|
||
CloseOnEscape="false"
|
||
Shown="OnPopupShownAsync">
|
||
<BodyContentTemplate>
|
||
<ul class="nav nav-tabs mb-3" style="border-bottom: 2px solid #e9ecef;">
|
||
<li class="nav-item">
|
||
<button type="button"
|
||
class="nav-link @(_activeSignatureTab == SignatureTabDraw ? "active" : "")"
|
||
style="@(_activeSignatureTab == SignatureTabDraw ? "border-bottom: 3px solid #4F46E5; color: #4F46E5; font-weight: 600;" : "color: #6c757d;")"
|
||
@onclick="() => SetSignatureTabAsync(SignatureTabDraw)">
|
||
Zeichnen
|
||
</button>
|
||
</li>
|
||
<li class="nav-item">
|
||
<button type="button"
|
||
class="nav-link @(_activeSignatureTab == SignatureTabText ? "active" : "")"
|
||
style="@(_activeSignatureTab == SignatureTabText ? "border-bottom: 3px solid #4F46E5; color: #4F46E5; font-weight: 600;" : "color: #6c757d;")"
|
||
@onclick="() => SetSignatureTabAsync(SignatureTabText)">
|
||
Text
|
||
</button>
|
||
</li>
|
||
<li class="nav-item">
|
||
<button type="button"
|
||
class="nav-link @(_activeSignatureTab == SignatureTabImage ? "active" : "")"
|
||
style="@(_activeSignatureTab == SignatureTabImage ? "border-bottom: 3px solid #4F46E5; color: #4F46E5; font-weight: 600;" : "color: #6c757d;")"
|
||
@onclick="() => SetSignatureTabAsync(SignatureTabImage)">
|
||
Bild
|
||
</button>
|
||
</li>
|
||
</ul>
|
||
|
||
@if(_activeSignatureTab == SignatureTabDraw) {
|
||
<p style="color: #6c757d; font-size: 0.875rem; margin-bottom: 0.75rem;">Bitte unterschreiben Sie im folgenden Feld.</p>
|
||
<canvas id="envelope-signature-pad"
|
||
width="560"
|
||
height="180"
|
||
style="border: 2px solid #e9ecef; border-radius: 8px; background: white; width: 100%; max-width: 560px; touch-action: none; box-shadow: 0 1px 3px rgba(0,0,0,0.1);"></canvas>
|
||
} else if(_activeSignatureTab == SignatureTabText) {
|
||
<p style="color: #6c757d; font-size: 0.875rem; margin-bottom: 0.75rem;">Geben Sie Ihre Unterschrift als Text ein und wählen Sie eine Schriftart.</p>
|
||
<div class="row g-3 mb-3">
|
||
<div class="col-12 col-md-7">
|
||
<input class="form-control"
|
||
placeholder="Ihre Unterschrift"
|
||
value="@_typedSignatureText"
|
||
@oninput="OnTypedSignatureChanged"
|
||
style="border: 2px solid #e9ecef; border-radius: 6px; padding: 0.625rem;" />
|
||
</div>
|
||
<div class="col-12 col-md-5">
|
||
<select class="form-select"
|
||
value="@_typedSignatureFont"
|
||
@onchange="OnTypedSignatureFontChanged"
|
||
style="border: 2px solid #e9ecef; border-radius: 6px; padding: 0.625rem;">
|
||
@foreach(var font in TypedSignatureFonts) {
|
||
<option value="@font.Value" style="font-family: @font.Value">@font.Text</option>
|
||
}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<canvas id="envelope-typed-signature-pad"
|
||
width="560"
|
||
height="180"
|
||
style="border: 2px solid #e9ecef; border-radius: 8px; background: white; width: 100%; max-width: 560px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);"></canvas>
|
||
} else {
|
||
<p style="color: #6c757d; font-size: 0.875rem; margin-bottom: 0.75rem;">Laden Sie ein Bild Ihrer Unterschrift hoch.</p>
|
||
<input id="envelope-signature-image-input"
|
||
class="form-control mb-3"
|
||
type="file"
|
||
accept="image/png,image/jpeg,image/webp"
|
||
style="border: 2px solid #e9ecef; border-radius: 6px; padding: 0.625rem;" />
|
||
<canvas id="envelope-image-signature-pad"
|
||
width="560"
|
||
height="180"
|
||
style="border: 2px solid #e9ecef; border-radius: 8px; background: white; width: 100%; max-width: 560px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);"></canvas>
|
||
}
|
||
|
||
<div style="border-top: 2px solid #e9ecef; margin-top: 1.5rem; padding-top: 1.5rem;">
|
||
<div class="row g-3">
|
||
<div class="col-12 col-md-6">
|
||
<label class="form-label" for="envelope-signer-name" style="font-size: 0.875rem; font-weight: 500; color: #495057;">
|
||
Vor- und Nachname <span style="color: #dc3545;">*</span>
|
||
</label>
|
||
<input id="envelope-signer-name"
|
||
class="form-control"
|
||
value="@_signerFullName"
|
||
@oninput="args => _signerFullName = args.Value?.ToString() ?? string.Empty"
|
||
style="border: 2px solid #e9ecef; border-radius: 6px; padding: 0.625rem;" />
|
||
</div>
|
||
<div class="col-12 col-md-6">
|
||
<label class="form-label" for="envelope-signer-position" style="font-size: 0.875rem; font-weight: 500; color: #495057;">
|
||
Position <span style="color: #6c757d; font-weight: 400;">(optional)</span>
|
||
</label>
|
||
<input id="envelope-signer-position"
|
||
class="form-control"
|
||
value="@_signerPosition"
|
||
@oninput="args => _signerPosition = args.Value?.ToString() ?? string.Empty"
|
||
style="border: 2px solid #e9ecef; border-radius: 6px; padding: 0.625rem;" />
|
||
</div>
|
||
<div class="col-12 col-md-6">
|
||
<label class="form-label" for="envelope-signature-place" style="font-size: 0.875rem; font-weight: 500; color: #495057;">
|
||
Ort <span style="color: #dc3545;">*</span>
|
||
</label>
|
||
<input id="envelope-signature-place"
|
||
class="form-control"
|
||
value="@_signaturePlace"
|
||
@oninput="args => _signaturePlace = args.Value?.ToString() ?? string.Empty"
|
||
style="border: 2px solid #e9ecef; border-radius: 6px; padding: 0.625rem;" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
@if(!string.IsNullOrWhiteSpace(_popupValidationMessage)) {
|
||
<div style="background: #fee; border-left: 4px solid #dc3545; padding: 0.75rem 1rem; margin-top: 1rem; border-radius: 4px;">
|
||
<span style="color: #dc3545; font-size: 0.875rem; font-weight: 500;">@_popupValidationMessage</span>
|
||
</div>
|
||
}
|
||
</BodyContentTemplate>
|
||
<FooterContentTemplate>
|
||
<div class="d-flex gap-2 justify-content-between w-100" style="padding: 0.5rem 0;">
|
||
<button class="btn btn-outline-secondary"
|
||
@onclick="RenewSignatureAsync"
|
||
style="border-radius: 6px; padding: 0.625rem 1.25rem; font-weight: 500;">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/>
|
||
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/>
|
||
</svg>
|
||
Erneuern
|
||
</button>
|
||
<button class="btn btn-primary"
|
||
@onclick="SaveSignatureAsync"
|
||
style="background: linear-gradient(135deg, #4F46E5 0%, #4338CA 100%); border: none; border-radius: 6px; padding: 0.625rem 2rem; font-weight: 600; box-shadow: 0 2px 4px rgba(79, 70, 229, 0.3);">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/>
|
||
</svg>
|
||
Speichern
|
||
</button>
|
||
</div>
|
||
</FooterContentTemplate>
|
||
</DxPopup>
|
||
|
||
@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;
|
||
bool _isLoggingOut = false;
|
||
DotNetObjectReference<EnvelopeViewer>? _dotNetRef;
|
||
IReadOnlyList<SignatureDto> _signatures = [];
|
||
EnvelopeReceiverDto? _envelopeReceiver;
|
||
|
||
// Signature navigation state
|
||
int _totalSignatures = 0;
|
||
int _signedSignatures = 0;
|
||
int _unsignedSignatures = 0;
|
||
int _currentSignatureIndex = 0; // Şu an hangi imzada (1-based)
|
||
|
||
// 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;
|
||
|
||
async Task LogoutAsync() {
|
||
if (string.IsNullOrWhiteSpace(EnvelopeKey) || _isLoggingOut) return;
|
||
_isLoggingOut = true;
|
||
await InvokeAsync(StateHasChanged);
|
||
await AuthService.LogoutEnvelopeReceiverAsync(EnvelopeKey);
|
||
Navigation.NavigateTo($"/login/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true);
|
||
}
|
||
|
||
protected override async Task OnInitializedAsync() {
|
||
if (string.IsNullOrWhiteSpace(EnvelopeKey)) {
|
||
_errorMessage = "Envelope-Schlüssel fehlt.";
|
||
_isLoading = false;
|
||
return;
|
||
}
|
||
|
||
// Check authentication
|
||
var hasAccess = await AuthService.CheckEnvelopeAccessAsync(EnvelopeKey);
|
||
if (!hasAccess) {
|
||
Navigation.NavigateTo($"/login/{Uri.EscapeDataString(EnvelopeKey)}");
|
||
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);
|
||
|
||
_envelopeReceiver = await EnvelopeReceiverService.GetAsync(EnvelopeKey);
|
||
|
||
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<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);
|
||
|
||
// Small delay for canvas render to complete (reduced from 100ms to 10ms)
|
||
await Task.Delay(10);
|
||
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);
|
||
|
||
// Update signature overlay positions after zoom
|
||
await RenderSignatureButtonsAsync();
|
||
}
|
||
|
||
async Task ZoomOut() {
|
||
if (_currentZoom <= 50) return;
|
||
await JSRuntime.InvokeVoidAsync("pdfViewer.zoomOut");
|
||
var scale = await JSRuntime.InvokeAsync<double>("pdfViewer.getScale");
|
||
_currentZoom = (int)(scale * 100);
|
||
|
||
// Update signature overlay positions after zoom
|
||
await RenderSignatureButtonsAsync();
|
||
}
|
||
|
||
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);
|
||
|
||
// Update signature overlay positions after zoom
|
||
await RenderSignatureButtonsAsync();
|
||
}
|
||
}
|
||
|
||
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);
|
||
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();
|
||
}
|
||
|
||
[JSInvokable]
|
||
public async Task OnPageChangedBySignatureNav(int newPage) {
|
||
_currentPage = newPage;
|
||
await RenderSignatureButtonsAsync();
|
||
}
|
||
|
||
async Task UpdateSignatureCounterAsync() {
|
||
try {
|
||
var state = await JSRuntime.InvokeAsync<SignatureNavState>("pdfViewer.getSignatureNavState");
|
||
_totalSignatures = state.Total;
|
||
_signedSignatures = state.Signed;
|
||
_unsignedSignatures = state.Unsigned;
|
||
_currentSignatureIndex = state.CurrentIndex; // Şu an hangi imzada
|
||
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<string?> GetActiveSignatureDataUrlAsync() {
|
||
if(_activeSignatureTab == SignatureTabDraw)
|
||
return await JSRuntime.InvokeAsync<string?>("receiverSignature.getDataUrl", DrawCanvasId);
|
||
|
||
if(_activeSignatureTab == SignatureTabText) {
|
||
await RenderTypedSignatureAsync();
|
||
return await JSRuntime.InvokeAsync<string?>("receiverSignature.getTypedDataUrl", TypedCanvasId);
|
||
}
|
||
|
||
return await JSRuntime.InvokeAsync<string?>("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();
|
||
}
|
||
}
|