Compare commits
74 Commits
34b620e749
...
120485ee8d
| Author | SHA1 | Date | |
|---|---|---|---|
| 120485ee8d | |||
| ee3a142af0 | |||
| 17e2de7f45 | |||
| de60bd239d | |||
| 52e5fce7fd | |||
| e319d4e833 | |||
| 9aa01f8e9a | |||
| 9535c7dd6b | |||
| 63b47ddbf2 | |||
| f6c7918fc3 | |||
| 0aeeacc291 | |||
| 4fdbbc832f | |||
| dbe1ad3b53 | |||
| 0b15496adb | |||
| 6d9b4d98ae | |||
| 334fc35b26 | |||
| 28b8bebe61 | |||
| 656fc97e74 | |||
| 6da68cdc86 | |||
| 5bed9c932f | |||
| 7a7fc2f903 | |||
| 2cea284a9d | |||
| c76ddb7123 | |||
| 80690d3d54 | |||
| 465986b527 | |||
| 09ff237ecc | |||
| 3f52858fe9 | |||
| ce43ace3c2 | |||
| 9523766678 | |||
| 382aafc186 | |||
| 45bb982414 | |||
| 3123102244 | |||
| 89fb6f1452 | |||
| 2f73e4f6da | |||
| b888c85937 | |||
| db70bbcebf | |||
| 6d6e14fcb7 | |||
| e6f12f0c68 | |||
| 7e2631cb21 | |||
| 34f145305c | |||
| a3b104cd78 | |||
| 53004504bd | |||
| cdc53c0bf7 | |||
| 2f1777af4a | |||
| dec2b81afe | |||
| 11a5012ab7 | |||
| b9efc75d4f | |||
| 8dc561cb8f | |||
| 76ce8a44b3 | |||
| e52972ee9b | |||
| 17ee715b46 | |||
| 6d8cecc20b | |||
| d32050ce03 | |||
| fc267e1eb4 | |||
| 86b821739a | |||
| 0f5acb7cf5 | |||
| c4ef195e20 | |||
| 0faf1fba7e | |||
| 139b92ed8c | |||
| ca3b74f939 | |||
| a6014ae88c | |||
| 4b5cdbfccd | |||
| 64068c9c29 | |||
| b913d5a88a | |||
| 51ea93200e | |||
| 9fa8ef29d8 | |||
| fb02a1a359 | |||
| bd6ff4e67e | |||
| c6d5656fce | |||
| 0282c8e5d3 | |||
| 6024f5c040 | |||
| d9ab6b3eff | |||
| c26ad9e1c2 | |||
| 76945c9051 |
@@ -40,29 +40,67 @@ A digital document signing system. Senders upload PDFs and place signature annot
|
||||
|
||||
---
|
||||
|
||||
## AnnotationDto — Coordinate System
|
||||
## SignatureDto / AnnotationDto — Coordinate System
|
||||
|
||||
**Database Storage Format:** INCHES (GdPicture14 native unit)
|
||||
**Origin:** Top-left corner of page
|
||||
**Axes:** X increases rightward, Y increases downward
|
||||
|
||||
### Source Evidence (VB.NET Legacy Code)
|
||||
|
||||
```vb
|
||||
' From: EnvelopeGenerator.Form/frmFieldEditor.vb
|
||||
' GdPicture14.Annotations.AnnotationStickyNote
|
||||
|
||||
'Breite und Höhe in Inches (4,5*5cm)
|
||||
Private Const SIGNATURE_WIDTH As Single = 1.77 ' 1.77 inches = 4.5cm
|
||||
Private Const SIGNATURE_HEIGHT As Single = 1.96 ' 1.96 inches = 5cm
|
||||
|
||||
Sub LoadAnnotation(pElement As Signature, ...)
|
||||
oAnnotation.Left = CSng(pElement.X) ' Direct assignment ? INCHES
|
||||
oAnnotation.Top = CSng(pElement.Y)
|
||||
oAnnotation.Width = CSng(pElement.Width)
|
||||
oAnnotation.Height = CSng(pElement.Height)
|
||||
End Sub
|
||||
```
|
||||
|
||||
### Conversion Formulas
|
||||
|
||||
```
|
||||
Unit : 1/100 inch (DX units) — DevExpress XtraReports native
|
||||
Origin : Top-left corner of page
|
||||
X : increases rightward
|
||||
Y : increases downward
|
||||
Inches ? DevExpress (DX): x_DX = x_inches * 100.0
|
||||
y_DX = y_inches * 100.0
|
||||
|
||||
A4 in DX units: Width = 827, Height = 1169
|
||||
Inches ? PDF Points: x_pt = x_inches * 72.0
|
||||
y_pt = x_inches * 72.0
|
||||
|
||||
Conversions:
|
||||
PSPDFKit (pt, top-left): xDX = xPsPdf * (100/72)
|
||||
GDPicture (pt, bottom-left): yDX = (pageHeightPt - yGD - elemHeightPt) * (100/72)
|
||||
DX ? PDF points: pt = dx * (72/100)
|
||||
Inches ? PDF.js Canvas: normalize to [0,1], then scale to pixels
|
||||
x_norm = x_inches / pageWidth_inches
|
||||
y_norm = y_inches / pageHeight_inches
|
||||
x_px = x_norm * canvasWidth * scale * dpr
|
||||
y_px = y_norm * canvasHeight * scale * dpr
|
||||
```
|
||||
|
||||
### Unit Comparison Table
|
||||
|
||||
| System | Unit | Origin | Conversion from INCHES |
|
||||
|--------|------|--------|------------------------|
|
||||
| **GdPicture14** (Source) | **Inches** | Top-left | Database format (no conversion) |
|
||||
| DevExpress (LEGACY) | 1/100 inch (DX) | Top-left | `x_DX = x_inches * 100` |
|
||||
| PDF.js (NEW) | Pixels | Top-left | `normalize ? scale` |
|
||||
| PDF Points (iText7) | Points (1/72") | **Bottom-left** | `x_pt = x_inches * 72` + Y-flip |
|
||||
| PSPDFKit (Web) | Points (1/72") | Top-left | `x_pt = x_inches * 72` |
|
||||
|
||||
**A4 Page Dimensions:**
|
||||
- Width: 8.27 inches = 595 points = 827 DX units
|
||||
- Height: 11.69 inches = 842 points = 1169 DX units
|
||||
|
||||
---
|
||||
|
||||
## EnvelopeViewer (NEW) — PDF.js Read-Only Viewer
|
||||
|
||||
**Route:** `/envelope/{EnvelopeKey}`
|
||||
**Purpose:** Simple, modern PDF viewing without signing functionality.
|
||||
**Technology:** PDF.js 3.11.174 + custom JavaScript wrapper
|
||||
**Purpose:** Modern, high-performance PDF viewing without signing functionality.
|
||||
**Technology:** PDF.js 3.11.174 + custom JavaScript + configurable quality settings
|
||||
|
||||
### Architecture
|
||||
|
||||
@@ -70,30 +108,81 @@ Conversions:
|
||||
- Fetches PDF via `DocumentService.GetDocumentAsync(EnvelopeKey)`
|
||||
- Converts to base64 data URL: `data:application/pdf;base64,{base64}`
|
||||
- Initializes PDF.js viewer via JSInterop with `DotNetObjectReference` for callbacks
|
||||
- Displays controls: Zoom In/Out, Page Navigation, Zoom percentage
|
||||
- Quality settings loaded from `appsettings.json` via `IOptions<PdfViewerOptions>`
|
||||
- Thumbnail sidebar with resizable splitter (150px-400px, localStorage persistence)
|
||||
- CSS externalized to `envelope-viewer.css`
|
||||
|
||||
**JavaScript (`pdf-viewer.js`):**
|
||||
```javascript
|
||||
window.pdfViewer = {
|
||||
pdfDoc, canvas, ctx, scale, currentRenderTask,
|
||||
dotNetReference, wheelEventAttached,
|
||||
|
||||
qualityOptions, // Configurable from appsettings.json
|
||||
setQualityOptions(options), // Dynamic quality update
|
||||
initialize(canvasId, pdfDataUrl, dotNetRef),
|
||||
renderPage(num),
|
||||
attachWheelEvent(), // Global Ctrl+Wheel zoom
|
||||
zoomIn(), zoomOut(),
|
||||
nextPage(), previousPage(),
|
||||
renderThumbnail(pageNum, canvasId),
|
||||
attachWheelEvent(), // Ctrl+Wheel zoom (configurable step)
|
||||
zoomIn(), zoomOut(), // Configurable step percentage
|
||||
dispose()
|
||||
}
|
||||
```
|
||||
|
||||
**CSS (`envelope-viewer.css`):**
|
||||
- `.envelope-viewer-layout`: Full-height gradient background
|
||||
- `.envelope-action-bar`: Top bar with logo, title, controls (sticky)
|
||||
- `.pdf-frame`: Fixed-size white container (`calc(100vh - 200px)` × 90% width, max 1200px)
|
||||
- `.pdf-canvas`: `display: inline-block`, unlimited zoom, scrollable when exceeds frame
|
||||
- Modern glassmorphism design with gradients and shadows
|
||||
**Options (`PdfViewerOptions.cs`):**
|
||||
```csharp
|
||||
public class PdfViewerOptions {
|
||||
public double ThumbnailBaseScale { get; set; } = 0.75; // 0.2-1.5
|
||||
public bool ThumbnailEnableHiDPI { get; set; } = true;
|
||||
public double ThumbnailMaxDPR { get; set; } = 2.0; // 1.0-3.0
|
||||
public bool MainCanvasEnableHiDPI { get; set; } = true;
|
||||
public double MainCanvasMaxDPR { get; set; } = 2.0;
|
||||
public bool EnableSmoothZoom { get; set; } = true;
|
||||
public int ZoomTransitionDuration { get; set; } = 150; // ms
|
||||
public double RenderingOpacity { get; set; } = 0.85; // 0.0-1.0
|
||||
public int ThumbnailRenderDelay { get; set; } = 50; // ms
|
||||
public int ZoomStepPercentage { get; set; } = 5; // 1-50%
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration (appsettings.json)
|
||||
|
||||
**Location:** `EnvelopeGenerator.ReceiverUI/wwwroot/appsettings.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"PdfViewer": {
|
||||
"ThumbnailBaseScale": 0.75,
|
||||
"ThumbnailEnableHiDPI": true,
|
||||
"ThumbnailMaxDPR": 2.0,
|
||||
"MainCanvasEnableHiDPI": true,
|
||||
"MainCanvasMaxDPR": 2.0,
|
||||
"EnableSmoothZoom": true,
|
||||
"ZoomTransitionDuration": 150,
|
||||
"RenderingOpacity": 0.85,
|
||||
"ThumbnailRenderDelay": 50,
|
||||
"ZoomStepPercentage": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:** Edit file ? F5 (browser refresh) ? new settings applied
|
||||
|
||||
**Presets:**
|
||||
- **High Quality**: `ThumbnailBaseScale: 1.0, MaxDPR: 3.0` (powerful devices)
|
||||
- **Balanced**: Default values (recommended)
|
||||
- **Performance**: `ThumbnailBaseScale: 0.5, EnableHiDPI: false` (mobile/low-end)
|
||||
|
||||
---
|
||||
|
||||
### Features
|
||||
|
||||
1. **HiDPI/Retina Support** ? 4x quality on Retina displays
|
||||
2. **Configurable Quality** ? All parameters in appsettings.json
|
||||
3. **Unlimited Zoom** ? 50%-300%, configurable step (default 5%)
|
||||
4. **Global Ctrl+Wheel Zoom** ? Works anywhere on page
|
||||
5. **Thumbnail Sidebar** ? Resizable (150-400px), high-quality rendering
|
||||
6. **Smooth Transitions** ? Configurable fade effect
|
||||
7. **Responsive Design** ? Desktop/mobile adaptive layout
|
||||
|
||||
---
|
||||
|
||||
### Features
|
||||
|
||||
@@ -113,38 +202,65 @@ window.pdfViewer = {
|
||||
- Catches `RenderingCancelledException` to avoid console errors
|
||||
- Queue system (`pageNumPending`) for rapid page changes
|
||||
|
||||
4. **Responsive Design:**
|
||||
4. **Thumbnail Sidebar:**
|
||||
- Left panel with page previews (sequential rendering, 50ms delay)
|
||||
- Click to navigate to specific page
|
||||
- Active page highlighted with gradient border
|
||||
- No header/title (maximizes thumbnail space)
|
||||
- Toggle button in toolbar to show/hide
|
||||
|
||||
5. **Resizable Splitter:**
|
||||
- 4px draggable divider between thumbnails and canvas
|
||||
- Min width: 150px, Max width: 400px
|
||||
- Visual feedback: gradient on hover/active
|
||||
- User preference saved to localStorage (`envelopeViewer_thumbnailWidth`)
|
||||
- Global mouse events (works anywhere during drag)
|
||||
- `col-resize` cursor (?) for intuitive UX
|
||||
|
||||
6. **Flex Layout:**
|
||||
- Thumbnails and canvas in same container (`.pdf-frame`)
|
||||
- `display: flex, flex-direction: row, align-items: stretch`
|
||||
- Perfect vertical alignment (same top/bottom position)
|
||||
- Responsive: column layout on mobile (<768px)
|
||||
|
||||
7. **Responsive Design:**
|
||||
- Desktop: 90% width, 1200px max
|
||||
- Mobile: 95% width, adjusted heights
|
||||
- Mobile: 95% width, adjusted heights, thumbnails collapse to top
|
||||
- Adaptive padding and font sizes
|
||||
|
||||
### Flow
|
||||
### Initialization Flow
|
||||
|
||||
1. **Component Load:**
|
||||
```csharp
|
||||
OnInitializedAsync():
|
||||
- Fetch PDF bytes
|
||||
- Convert to base64 data URL
|
||||
- Set _isLoading = false
|
||||
1. Fetch PDF bytes from DocumentService
|
||||
2. Convert to base64 data URL
|
||||
3. Set _isLoading = false
|
||||
|
||||
OnAfterRenderAsync():
|
||||
- Create DotNetObjectReference
|
||||
- JSRuntime.InvokeAsync("pdfViewer.initialize", canvasId, pdfDataUrl, dotNetRef)
|
||||
- Update _totalPages, _currentPage, _pdfLoaded
|
||||
OnAfterRenderAsync(firstRender):
|
||||
1. Load saved thumbnail width from localStorage
|
||||
2. Create DotNetObjectReference
|
||||
3. Send PdfViewerOptions to JavaScript
|
||||
4. Initialize PDF.js viewer
|
||||
5. Attach splitter resize listeners
|
||||
6. Render thumbnails sequentially (configurable delay)
|
||||
```
|
||||
|
||||
2. **User Interaction:**
|
||||
- Button clicks ? `ZoomIn()`/`ZoomOut()` ? `JSRuntime.InvokeVoidAsync("pdfViewer.zoomIn")`
|
||||
- Ctrl+Wheel ? JS `attachWheelEvent()` ? `dotNetRef.invokeMethodAsync('OnZoomChanged')`
|
||||
- Page buttons ? `NextPage()`/`PreviousPage()` ? `JSRuntime.InvokeAsync("pdfViewer.nextPage")`
|
||||
**User Interactions:**
|
||||
- Zoom: Buttons/Ctrl+Wheel/Slider ? configurable step percentage
|
||||
- Pages: Buttons/Input/Thumbnails ? navigate
|
||||
- Sidebar: Toggle button ? show/hide thumbnails
|
||||
- Splitter: Drag ? resize sidebar (150-400px)
|
||||
|
||||
3. **Cleanup:**
|
||||
**Cleanup:**
|
||||
```csharp
|
||||
DisposeAsync():
|
||||
- JSRuntime.InvokeVoidAsync("pdfViewer.dispose")
|
||||
- _dotNetRef?.Dispose()
|
||||
- Dispose PDF.js viewer
|
||||
- Detach event listeners
|
||||
- Dispose DotNetObjectReference
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Key Differences from ReportViewer
|
||||
|
||||
| Feature | EnvelopeViewer (NEW) | ReportViewer (LEGACY) |
|
||||
@@ -256,17 +372,20 @@ return report;
|
||||
|
||||
## Mistakes History — Do NOT Repeat
|
||||
|
||||
| Mistake | Why Wrong |
|
||||
|---|---|
|
||||
| `BottomMarginBand` for per-page signatures | Repeats on every page; Y offset wrong |
|
||||
| `imageY = (page-1) * 1169 + ann.Y` | Inflates DetailBand; 35 pages ? 140 pages |
|
||||
| `e.Graph?.PrintingSystem` in BeforePrint | `Graph` not on `CancelEventArgs` |
|
||||
| `ctrl.Report?.PrintingSystem` | `PrintingSystem` not on `XtraReportBase` in WASM |
|
||||
| Adding stamp endpoint to `DocumentController` | Not needed; stamping is done client-side in ReceiverUI |
|
||||
| iText7 via API (server-side) | Unnecessary; iText7 runs fine in WASM directly |
|
||||
| **PDF.js: `display: flex` on `.pdf-frame`** | **Prevents left-edge scroll when canvas exceeds container** |
|
||||
| **PDF.js: `max-width: 100%` on canvas** | **Limits zoom; user expects unlimited zoom capability** |
|
||||
| **Mouse wheel on `.pdf-frame` only** | **Only works when mouse over PDF; should work anywhere on page** |
|
||||
| Mistake | Why Wrong | Session |
|
||||
|---|---|---|
|
||||
| `BottomMarginBand` for per-page signatures | Repeats on every page; Y offset wrong | 4 |
|
||||
| `imageY = (page-1) * 1169 + ann.Y` | Inflates DetailBand; 35 pages ? 140 pages | 8 |
|
||||
| `e.Graph?.PrintingSystem` in BeforePrint | `Graph` not on `CancelEventArgs` | 5 |
|
||||
| `ctrl.Report?.PrintingSystem` | `PrintingSystem` not on `XtraReportBase` in WASM | — |
|
||||
| Adding stamp endpoint to `DocumentController` | Not needed; stamping is done client-side in ReceiverUI | — |
|
||||
| iText7 via API (server-side) | Unnecessary; iText7 runs fine in WASM directly | 10 |
|
||||
| **PDF.js: Hardcoded quality values** | **Use appsettings.json for configurability** | **11** |
|
||||
| **PDF.js: Hardcoded zoom step (1%)** | **Too granular; use configurable percentage** | **11** |
|
||||
| **Toolbar: Complex left/center/right layout** | **User wants simple horizontal layout; failed multiple times to implement** | **11** |
|
||||
| **Zoom label: Badge style (gradient/border/padding)** | **Over-designed; user prefers simple text label** | **11** |
|
||||
| **Attempting to "improve" simple designs** | **User requests simplicity; AI keeps over-engineering** | **11** |
|
||||
| **Ignoring explicit "revert" instructions** | **User said revert toolbar, AI tried to fix CSS instead of reverting HTML structure** | **11** |
|
||||
|
||||
---
|
||||
|
||||
@@ -278,24 +397,550 @@ Our use case is **visual/image stamping** at specific page coordinates
|
||||
|
||||
---
|
||||
|
||||
## Change Log
|
||||
## Layout Architecture (EnvelopeViewer)
|
||||
|
||||
### HTML Structure
|
||||
```html
|
||||
<div class="pdf-viewer-container">
|
||||
<div class="pdf-toolbar">
|
||||
<!-- Zoom, page navigation, thumbnail toggle -->
|
||||
</div>
|
||||
|
||||
<div class="pdf-frame">
|
||||
@if (_showThumbnails) {
|
||||
<div class="pdf-thumbnails" style="width: @(_thumbnailWidth)px">
|
||||
<!-- Page previews -->
|
||||
</div>
|
||||
<div class="pdf-splitter" @onmousedown="OnSplitterMouseDown">
|
||||
<!-- Resizable divider -->
|
||||
</div>
|
||||
}
|
||||
<div class="pdf-canvas-wrapper">
|
||||
<canvas id="pdf-canvas" class="pdf-canvas"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### CSS Flexbox Layout
|
||||
```css
|
||||
.pdf-frame {
|
||||
display: flex;
|
||||
flex-direction: row; /* Side-by-side */
|
||||
align-items: stretch; /* Same height */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pdf-thumbnails {
|
||||
flex-shrink: 0; /* Fixed width */
|
||||
width: 260px; /* Dynamic via inline style */
|
||||
border-right: none; /* Seamless join with splitter */
|
||||
}
|
||||
|
||||
.pdf-splitter {
|
||||
width: 4px;
|
||||
cursor: col-resize;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pdf-canvas-wrapper {
|
||||
flex: 1; /* Fill remaining space */
|
||||
overflow: auto; /* Scrollable for zoom */
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
```
|
||||
|
||||
### Resizable Splitter Workflow
|
||||
```
|
||||
1. User hovers splitter ? cursor: col-resize (?)
|
||||
2. Mouse down ? OnSplitterMouseDown(e)
|
||||
- _isResizing = true
|
||||
- Store start position (clientX) and width
|
||||
- Add 'resizing' class to body (prevent text selection)
|
||||
- Call pdfViewer.startResize()
|
||||
|
||||
3. Mouse move (global) ? OnSplitterMouseMove(clientX)
|
||||
- Calculate delta = clientX - startX
|
||||
- newWidth = startWidth + delta
|
||||
- Clamp to 150-400px range
|
||||
- Update _thumbnailWidth
|
||||
- StateHasChanged() for reactive UI
|
||||
|
||||
4. Mouse up (global) ? OnSplitterMouseUp()
|
||||
- _isResizing = false
|
||||
- Remove 'resizing' class
|
||||
- Save to localStorage("envelopeViewer_thumbnailWidth")
|
||||
```
|
||||
|
||||
### Responsive Behavior
|
||||
- **Desktop (>768px)**: Flex row, side-by-side
|
||||
- **Mobile (?768px)**: Flex column, thumbnails on top
|
||||
- **Thumbnail toggle**: Controlled by `@if (_showThumbnails)` in Razor markup
|
||||
|
||||
---
|
||||
|
||||
## Signature Buttons in EnvelopeViewer — Interactive Overlay System
|
||||
|
||||
**Purpose:** Render clickable "Unterschreiben" (Sign) buttons on PDF canvas at signature field positions fetched from database.
|
||||
|
||||
### Architecture
|
||||
|
||||
**Blazor Component (`EnvelopeViewer.razor`):**
|
||||
```csharp
|
||||
IReadOnlyList<SignatureDto> _signatures = [];
|
||||
|
||||
protected override async Task OnInitializedAsync() {
|
||||
var signatures = await SignatureService.GetAsync(EnvelopeKey);
|
||||
_signatures = signatures.Convert(UnitOfLength.Point); // INCHES ? POINTS
|
||||
}
|
||||
|
||||
async Task RenderSignatureButtonsAsync() {
|
||||
await JSRuntime.InvokeVoidAsync("pdfViewer.renderSignatureButtons",
|
||||
_signatures, _currentPage, _dotNetRef);
|
||||
}
|
||||
|
||||
[JSInvokable]
|
||||
public void OnSignatureButtonClick(int signatureId) {
|
||||
Console.WriteLine($"Signature #{signatureId} signed");
|
||||
}
|
||||
```
|
||||
|
||||
**JavaScript (`pdf-viewer.js`):**
|
||||
```javascript
|
||||
renderSignatureButtons(signatures, currentPageNum, dotNetRef) {
|
||||
this.clearSignatureButtons(); // Remove old buttons
|
||||
|
||||
const pageSignatures = signatures.filter(sig => sig.page === currentPageNum);
|
||||
const signatureLayer = document.getElementById('pdf-signature-layer');
|
||||
|
||||
pageSignatures.forEach(sig => {
|
||||
// Convert POINTS to display pixels
|
||||
const xPx = sig.x * this.scale; // sig.x already in PDF POINTS
|
||||
const yPx = sig.y * this.scale;
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.className = 'signature-button';
|
||||
button.style.left = `${xPx}px`;
|
||||
button.style.top = `${yPx}px`;
|
||||
button.style.width = '150px';
|
||||
button.style.height = '60px';
|
||||
|
||||
// German text + pen icon
|
||||
button.innerHTML = `
|
||||
<div>Unterschreiben</div>
|
||||
<svg>...</svg>
|
||||
`;
|
||||
|
||||
button.addEventListener('click', () => {
|
||||
this.dotNetReference.invokeMethodAsync('OnSignatureButtonClick', sig.id);
|
||||
});
|
||||
|
||||
signatureLayer.appendChild(button);
|
||||
this.signatureButtons.push(button);
|
||||
});
|
||||
}
|
||||
|
||||
clearSignatureButtons() {
|
||||
this.signatureButtons.forEach(btn => btn.parentNode?.removeChild(btn));
|
||||
this.signatureButtons = [];
|
||||
}
|
||||
```
|
||||
|
||||
**HTML Structure:**
|
||||
```html
|
||||
<div class="pdf-page-container">
|
||||
<canvas id="pdf-canvas"></canvas>
|
||||
<div id="pdf-text-layer"></div>
|
||||
<div id="pdf-signature-layer"></div> <!-- NEW -->
|
||||
</div>
|
||||
```
|
||||
|
||||
**CSS (`envelope-viewer.css`):**
|
||||
```css
|
||||
.pdf-signature-layer {
|
||||
position: absolute;
|
||||
left: 0; top: 0; right: 0; bottom: 0;
|
||||
overflow: visible;
|
||||
pointer-events: none; /* Pass clicks through to canvas */
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.signature-button {
|
||||
pointer-events: auto; /* Re-enable for buttons */
|
||||
position: absolute;
|
||||
width: 150px; height: 60px;
|
||||
background: linear-gradient(135deg, #4F46E5 0%, #4338CA 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
/* Hover: scale(1.05), shadow, darker gradient */
|
||||
}
|
||||
```
|
||||
|
||||
### Rendering Triggers
|
||||
|
||||
1. **Initial Load:** After PDF renders (`OnAfterRenderAsync`)
|
||||
2. **Page Change:** `NextPage()`, `PreviousPage()`, `GoToPageFromThumbnail()`
|
||||
3. **Zoom Change:** `OnZoomChanged()` (re-calculates pixel positions)
|
||||
|
||||
### Coordinate Conversion Flow
|
||||
|
||||
```
|
||||
Database (INCHES)
|
||||
? SignatureService.GetAsync()
|
||||
SignatureDto.X/Y (INCHES)
|
||||
? .Convert(UnitOfLength.Point)
|
||||
SignatureDto.X/Y (PDF POINTS, × 72)
|
||||
? JavaScript
|
||||
Display Pixels (× this.scale)
|
||||
? CSS
|
||||
button.style.left/top
|
||||
```
|
||||
|
||||
**Example Calculation:**
|
||||
- Database: `X = 1.5 inches, Y = 2.0 inches`
|
||||
- After conversion: `X = 108 points, Y = 144 points` (× 72)
|
||||
- At scale 1.5: `X = 162px, Y = 216px`
|
||||
- Button positioned at `left: 162px, top: 216px`
|
||||
|
||||
### Button Design
|
||||
|
||||
**Visual Spec:**
|
||||
- **Size:** 150px × 60px
|
||||
- **Background:** Purple gradient `#4F46E5` ? `#4338CA` (hover: darker)
|
||||
- **Text:** "Unterschreiben" (18px, bold, white)
|
||||
- **Icon:** Pen SVG (24px white)
|
||||
- **Effects:**
|
||||
- Hover: `scale(1.05)` + shadow `0 4px 12px rgba(79, 70, 229, 0.4)`
|
||||
- Active: `scale(0.98)`
|
||||
- Focus: `2px solid #7e22ce` outline
|
||||
|
||||
**Accessibility:**
|
||||
- `tabindex="0"` for keyboard navigation
|
||||
- Focus outline for keyboard users
|
||||
- Click handler with semantic button element
|
||||
|
||||
---
|
||||
|
||||
## Signature Workflow in EnvelopeViewer — NEW Implementation (Session 13-14)
|
||||
|
||||
**IMPORTANT: iText7 NOT USED in EnvelopeViewer**
|
||||
- **Reason:** GPL license incompatibility (requires source code sharing)
|
||||
- **Alternative:** Client-side signature overlay system (HTML + Canvas API)
|
||||
- **Export:** Signatures are visual overlays only, NOT stamped on PDF bytes
|
||||
- **Future:** Consider PSPDFKit or commercial PDF library for actual PDF stamping
|
||||
|
||||
### Signature Data Structure
|
||||
|
||||
**Captured Signature (`SignatureCaptureDto`):**
|
||||
```csharp
|
||||
// Model: EnvelopeGenerator.ReceiverUI/Models/SignatureCaptureDto.cs
|
||||
public sealed record SignatureCaptureDto
|
||||
{
|
||||
public required string DataUrl { get; init; } // base64 PNG: "data:image/png;base64,iVBORw0KG..."
|
||||
public required string FullName { get; init; } // Required: "Max Mustermann"
|
||||
public string Position { get; init; } = string.Empty; // Optional: "Geschäftsführer"
|
||||
public required string Place { get; init; } // Required: "Berlin"
|
||||
}
|
||||
|
||||
// Usage in components:
|
||||
SignatureCaptureDto? _capturedSignature;
|
||||
|
||||
// Initialization with object initializer (required properties):
|
||||
_capturedSignature = new SignatureCaptureDto
|
||||
{
|
||||
DataUrl = signatureDataUrl,
|
||||
FullName = _signerFullName.Trim(),
|
||||
Position = _signerPosition.Trim(),
|
||||
Place = _signaturePlace.Trim()
|
||||
};
|
||||
```
|
||||
|
||||
**Applied Signature (HTML Overlay):**
|
||||
```html
|
||||
<div class="applied-signature" data-signature-id="42" style="left: 162px; top: 216px;">
|
||||
<img src="data:image/png;base64,..." /> <!-- Signature image (max 70px height) -->
|
||||
<div style="border-top: 1px solid #495057;"></div> <!-- Separator line -->
|
||||
<div style="font-size: 9px; color: #495057;">
|
||||
<strong>Max Mustermann</strong> <!-- Name (bold, #212529) -->
|
||||
<br>Geschäftsführer <!-- Position (optional) -->
|
||||
<br>Berlin, 26.01.2025 <!-- Place, Date (dd.MM.yyyy) -->
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Complete Workflow (Session 14 Update)
|
||||
|
||||
**Step 1: Page Load & Automatic Popup**
|
||||
```csharp
|
||||
protected override async Task OnInitializedAsync() {
|
||||
// ... load PDF and signatures ...
|
||||
|
||||
// Open signature popup automatically
|
||||
_activeSignatureTab = SignatureTabDraw;
|
||||
_signaturePopupVisible = true;
|
||||
_popupValidationMessage = null;
|
||||
}
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Opens automatically on page load (no manual trigger needed)
|
||||
- Cannot be closed manually (no X button, ESC disabled, no outside-click)
|
||||
- User MUST create signature before viewing PDF
|
||||
|
||||
**Step 2: Signature Creation Popup (DxPopup)**
|
||||
|
||||
**Tabs:**
|
||||
1. **Zeichnen (Draw):** Canvas-based signature pad (`receiver-signature.js`)
|
||||
- Touch-friendly: `touch-action: none`
|
||||
- Line width: 2.5px, black (`#111`)
|
||||
- Canvas: 560×180px, rounded corners, shadow
|
||||
|
||||
2. **Text:** Type signature with font selection
|
||||
- Fonts: Brush Script, Segoe Script, Lucida Handwriting, Comic Sans, Cursive
|
||||
- Real-time preview on canvas
|
||||
|
||||
3. **Bild (Image):** Upload PNG/JPG/WebP
|
||||
- File input with preview
|
||||
- Auto-resize to fit canvas
|
||||
|
||||
**Required Fields:**
|
||||
- ? **Vor- und Nachname** (Full Name) — Red asterisk `*`
|
||||
- ? **Ort** (Place) — Red asterisk `*`
|
||||
- ? **Position** — Optional, gray "(optional)" label
|
||||
|
||||
**Validation:**
|
||||
```csharp
|
||||
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;
|
||||
}
|
||||
|
||||
// Save to session state
|
||||
_capturedSignature = new(signatureDataUrl, _signerFullName.Trim(),
|
||||
_signerPosition.Trim(), _signaturePlace.Trim());
|
||||
_signaturePopupVisible = false;
|
||||
}
|
||||
```
|
||||
|
||||
**Design (Modern & Clean):**
|
||||
- **Tabs:** Purple active state (`#4F46E5`), 3px bottom border
|
||||
- **Inputs:** 2px solid border (`#e9ecef`), 6px border-radius, consistent padding
|
||||
- **Canvas:** Light shadow (`0 1px 3px rgba(0,0,0,0.1)`), rounded corners
|
||||
- **Buttons:**
|
||||
- **Erneuern:** Outline secondary with refresh icon
|
||||
- **Speichern:** Purple gradient + checkmark icon + shadow
|
||||
- **Error:** Red left border (`4px solid #dc3545`), light red background (`#fee`)
|
||||
|
||||
**Step 3: PDF Viewing with Signature Buttons**
|
||||
|
||||
After popup closes:
|
||||
```csharp
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender) {
|
||||
// ... PDF initialization ...
|
||||
|
||||
await RenderSignatureButtonsAsync(); // Render "Unterschreiben" buttons
|
||||
}
|
||||
```
|
||||
|
||||
**Buttons appear at signature field positions:**
|
||||
- Purple gradient background
|
||||
- "Unterschreiben" text + pen icon
|
||||
- Hover: scale(1.05) + darker color
|
||||
- Positioned using PDF POINTS ? display pixels conversion
|
||||
|
||||
**Step 4: Apply Signature (Click "Unterschreiben")**
|
||||
|
||||
**C# Handler:**
|
||||
```csharp
|
||||
[JSInvokable]
|
||||
public async Task OnSignatureButtonClick(int signatureId) {
|
||||
if (_capturedSignature == null) return;
|
||||
|
||||
await JSRuntime.InvokeVoidAsync("pdfViewer.applySignature",
|
||||
signatureId,
|
||||
_capturedSignature.DataUrl,
|
||||
_capturedSignature.FullName,
|
||||
_capturedSignature.Position,
|
||||
_capturedSignature.Place);
|
||||
}
|
||||
```
|
||||
|
||||
**JavaScript Implementation:**
|
||||
```javascript
|
||||
async applySignature(signatureId, signatureDataUrl, fullName, position, place) {
|
||||
// 1. Find and remove button
|
||||
const button = this.signatureButtons.find(btn =>
|
||||
btn.getAttribute('data-signature-id') == signatureId);
|
||||
button.parentNode.removeChild(button);
|
||||
|
||||
// 2. Create signature container (German standard format)
|
||||
const signatureContainer = document.createElement('div');
|
||||
signatureContainer.className = 'applied-signature';
|
||||
signatureContainer.style.position = 'absolute';
|
||||
signatureContainer.style.left = button.style.left; // Same position as button
|
||||
signatureContainer.style.top = button.style.top;
|
||||
signatureContainer.style.width = '230px';
|
||||
signatureContainer.style.backgroundColor = '#f8f9fa';
|
||||
signatureContainer.style.border = '1px solid #dee2e6';
|
||||
signatureContainer.style.borderRadius = '6px';
|
||||
signatureContainer.style.padding = '12px';
|
||||
|
||||
// 3. Add signature image
|
||||
const img = document.createElement('img');
|
||||
img.src = signatureDataUrl;
|
||||
img.style.width = '100%';
|
||||
img.style.maxHeight = '70px';
|
||||
img.style.objectFit = 'contain';
|
||||
|
||||
// 4. Add separator line (German standard)
|
||||
const separator = document.createElement('div');
|
||||
separator.style.borderTop = '1px solid #495057';
|
||||
separator.style.marginTop = '6px';
|
||||
separator.style.marginBottom = '8px';
|
||||
|
||||
// 5. Add text information
|
||||
const today = new Date();
|
||||
const dateStr = today.toLocaleDateString('de-DE', {
|
||||
day: '2-digit', month: '2-digit', year: 'numeric'
|
||||
}); // "26.01.2025"
|
||||
|
||||
const infoHtml = [
|
||||
`<strong>${this.escapeHtml(fullName)}</strong>`,
|
||||
position ? this.escapeHtml(position) : null,
|
||||
`${this.escapeHtml(place)}, ${dateStr}`
|
||||
].filter(x => x).join('<br>');
|
||||
|
||||
const info = document.createElement('div');
|
||||
info.style.fontSize = '9px';
|
||||
info.style.color = '#495057';
|
||||
info.innerHTML = infoHtml;
|
||||
|
||||
// 6. Assemble and add to layer
|
||||
signatureContainer.appendChild(img);
|
||||
signatureContainer.appendChild(separator);
|
||||
signatureContainer.appendChild(info);
|
||||
|
||||
document.getElementById('pdf-signature-layer').appendChild(signatureContainer);
|
||||
}
|
||||
```
|
||||
|
||||
**German Standard Layout:**
|
||||
```
|
||||
???????????????????????????????
|
||||
? [Signature Image] ? ? Base64 PNG, max 70px height
|
||||
? ?
|
||||
??????????????????????????????? ? 1px separator (#495057)
|
||||
? ?
|
||||
? Max Mustermann (Bold) ? ? Name (font-weight: 600, #212529)
|
||||
? Geschäftsführer ? ? Position (optional, normal weight)
|
||||
? Berlin, 26.01.2025 ? ? Place, Date (dd.MM.yyyy)
|
||||
? ?
|
||||
???????????????????????????????
|
||||
```
|
||||
|
||||
**Step 5: Persistence & Re-rendering**
|
||||
|
||||
- **Zoom/Page Change:** Applied signatures re-render automatically
|
||||
- **Session State:** `_capturedSignature` stored in Blazor component
|
||||
- **Limitation:** Lost on page refresh (no server-side storage)
|
||||
- **Future:** Export to PDF with actual byte stamping (requires non-GPL library)
|
||||
|
||||
**Security:**
|
||||
```javascript
|
||||
escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text; // Browser auto-escapes
|
||||
return div.innerHTML;
|
||||
}
|
||||
```
|
||||
Protects against XSS attacks from malicious input in Name/Position/Place fields.
|
||||
|
||||
---
|
||||
|
||||
### Popup Design Specification
|
||||
|
||||
**DxPopup Properties:**
|
||||
```razor
|
||||
<DxPopup @bind-Visible="_signaturePopupVisible"
|
||||
HeaderText="Unterschrift erstellen"
|
||||
Width="620px"
|
||||
MaxWidth="95vw"
|
||||
ShowFooter="true" <!-- REQUIRED for buttons to appear -->
|
||||
CloseOnOutsideClick="false"
|
||||
ShowCloseButton="false" <!-- No X button -->
|
||||
CloseOnEscape="false"> <!-- ESC disabled -->
|
||||
```
|
||||
|
||||
**Tab Design:**
|
||||
- **Active Tab:** `border-bottom: 3px solid #4F46E5; color: #4F46E5; font-weight: 600;`
|
||||
- **Inactive Tab:** `color: #6c757d;`
|
||||
- **Tab Bar:** `border-bottom: 2px solid #e9ecef;`
|
||||
|
||||
**Input Styling:**
|
||||
```css
|
||||
input, select {
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 6px;
|
||||
padding: 0.625rem;
|
||||
}
|
||||
```
|
||||
|
||||
**Canvas Styling:**
|
||||
```css
|
||||
canvas {
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
background: white;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
```
|
||||
|
||||
**Button Styling:**
|
||||
```css
|
||||
/* Erneuern (Renew) */
|
||||
.btn-outline-secondary {
|
||||
border-radius: 6px;
|
||||
padding: 0.625rem 1.25rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Speichern (Save) */
|
||||
.btn-primary {
|
||||
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);
|
||||
}
|
||||
```
|
||||
|
||||
**Future Enhancement Required:**
|
||||
- Replace iText7 with commercial PDF library (e.g., PSPDFKit, Syncfusion)
|
||||
- Or use server-side stamping with Azure PDF Services
|
||||
- Or accept GPL license and open-source the stamping code
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
| Session | Date | Change |
|
||||
|---|---|---|
|
||||
| 1–3 | — | Core infrastructure: services, YARP proxy, JS overlay, signature pad |
|
||||
| 4 | — | `AddSignatureAtAnnotation` with BottomMarginBand — ? repeated on all pages |
|
||||
| 5 | — | `BeforePrint` + `e.Graph?.PrintingSystem` — ? compile error |
|
||||
| 6 | — | BeforePrint counter — ? correct pattern, wrong band |
|
||||
| 7 | — | Switched to DetailBand — ? correct band |
|
||||
| 8 | — | `(page-1)*1169+Y` offset — ? 35?140 page inflation |
|
||||
| 9 | — | Fixed: `BoundsF.Y = ann.Y` + counter; created COPILOT_CONTEXT.md |
|
||||
| 10 | — | Investigated DevExpress article — not applicable to our case |
|
||||
| 10 | — | Added iText7 to ReceiverUI; implemented `StampSignaturesOnPdf` — ? deterministic coordinates, no page count side effects |
|
||||
| 10 | — | Split COPILOT_CONTEXT.md into COPILOT_CONTEXT_EN.md and COPILOT_CONTEXT_TR.md |
|
||||
| **11** | **2025-01-XX** | **Created EnvelopeViewer.razor (`/envelope/{key}`) with PDF.js 3.11.174** |
|
||||
| **11** | **2025-01-XX** | **Implemented `pdf-viewer.js`: canvas rendering, zoom, pagination, render task cancellation** |
|
||||
| **11** | **2025-01-XX** | **Externalized CSS to `envelope-viewer.css`: modern glassmorphism design** |
|
||||
| **11** | **2025-01-XX** | **Fixed scroll issues: removed `display: flex`, used `text-align: center` + `inline-block`** |
|
||||
| **11** | **2025-01-XX** | **Removed canvas `max-width` restriction for unlimited zoom** |
|
||||
| **11** | **2025-01-XX** | **Added global mouse wheel zoom: `Ctrl+Wheel` on `document.body`, JSInterop callback to Blazor** |
|
||||
| **11** | **2025-01-XX** | **Updated COPILOT_CONTEXT_EN.md: EnvelopeViewer replaces ReportViewer for read-only viewing** |
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
# EnvelopeGenerator — Copilot Ba?lam Notlar? (Türkçe)
|
||||
|
||||
## Projenin Amac?
|
||||
Dijital belge imzalama sistemi. Göndericiler PDF yükleyip PSPDFKit üzerinden imza alan? (annotation) yerle?tirir. Al?c?lar Blazor WASM viewer'da belgeyi görür, annotation konumlar?nda checkbox overlay ile imza alanlar?n? onaylar, imzalar?n? olu?turur ve imzal? PDF'i export eder.
|
||||
|
||||
---
|
||||
|
||||
## Çözüm Yap?s?
|
||||
|
||||
| Proje | Hedef | Aç?klama |
|
||||
|---|---|---|
|
||||
| `EnvelopeGenerator.API` | net8.0 | Web API. Receiver auth (cookie), annotation okuma, PDF sunma. |
|
||||
| `EnvelopeGenerator.ReceiverUI` | net8.0 WASM | Blazor WebAssembly. Al?c? arayüzü. YARP proxy ile API'ye ba?lan?r. |
|
||||
| `EnvelopeGenerator.Web` | net7/8/9 | Razor Pages. Gönderen UI + PSPDFKit ile annotation yerle?tirme. |
|
||||
| `EnvelopeGenerator.Application` | multi | MediatR CQRS handler'lar?. |
|
||||
| `EnvelopeGenerator.Domain` | multi | Domain modelleri, sabitler, arayüzler. |
|
||||
| `EnvelopeGenerator.Infrastructure` | multi | EF Core repo'lar?, DB context. |
|
||||
| `EnvelopeGenerator.PdfEditor` | multi | iText7 PDF yard?mc?lar?. ReceiverUI ak???nda KULLANILMIYOR. |
|
||||
| `EnvelopeGenerator.DependencyInjection` | multi | DI kay?t yard?mc?lar?. |
|
||||
| VB.NET projeleri (Service/Form/BBTests) | net462 | Eski legacy. DOKUNMA. |
|
||||
|
||||
---
|
||||
|
||||
## Önemli Dosyalar
|
||||
|
||||
| Dosya | Amaç |
|
||||
|---|---|
|
||||
| `ReceiverUI/Pages/ReportViewer.razor` | Ana al?c? sayfas?. Tüm imzalama mant??? burada. |
|
||||
| `ReceiverUI/wwwroot/js/receiver-signature.js` | JS: checkbox overlay, imza pad (çizim/yaz?/resim). |
|
||||
| `ReceiverUI/wwwroot/fake-data/annotations.json` | Dev modda sahte annotation konumlar?. YARP proxy bu dosyaya yönlendirir. |
|
||||
| `ReceiverUI/Models/AnnotationDto.cs` | Annotation pozisyon modeli. Tüm property'ler non-nullable. |
|
||||
| `ReceiverUI/Services/AnnotationService.cs` | `List<AnnotationDto>` döner; gerçek modda API'den, dev modda fake-data'dan. |
|
||||
| `ReceiverUI/Services/DocumentService.cs` | PDF byte'lar?n? API'den al?r. |
|
||||
| `ReceiverUI/Services/AuthService.cs` | Al?c? session cookie'sini yönetir. |
|
||||
| `ReceiverUI/wwwroot/appsettings.json` | `ForceToUseFakeDocument: true` ? gerçek PDF yüklenmez, ?ablon rapor kullan?l?r. |
|
||||
| `API/Controllers/AnnotationController.cs` | GET `api/Annotation/{key}` ? annotation listesi. |
|
||||
| `API/Controllers/DocumentController.cs` | GET `api/Document/{key}` ? PDF byte'lar?. |
|
||||
|
||||
---
|
||||
|
||||
## AnnotationDto Koordinat Sistemi
|
||||
|
||||
```
|
||||
Birim : 1/100 inch (DX units) — DevExpress XtraReports'un yerel koordinat sistemi
|
||||
Köken : Sol-üst kö?e
|
||||
X artar : sa?a do?ru
|
||||
Y artar : a?a??ya do?ru
|
||||
|
||||
A4 boyutlar? DX units cinsinden: Geni?lik = 827, Yükseklik = 1169
|
||||
|
||||
Dönü?ümler:
|
||||
PSPDFKit (pt, sol-üst): xDX = xPsPdf * (100/72)
|
||||
GDPicture (pt, sol-alt): yDX = (pageHeightPt - yGD - elemHeightPt) * (100/72)
|
||||
DX ? PDF points: pt = dx * (72/100)
|
||||
PDF Y ekseni çevirme: imgBottomY = sayfaYüksekli?iPt - ann.Y*(72/100) - elemanYüksekli?iPt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ReceiverUI ?mzalama Ak??? (ReportViewer.razor)
|
||||
|
||||
### Sayfa Yüklenince (`OnInitializedAsync`)
|
||||
1. `AuthService.CheckEnvelopeAccessAsync` ? yetkisizse login sayfas?na yönlendir
|
||||
2. `AnnotationService.GetAnnotationsAsync` ? `_annotations` listesi dolar
|
||||
3. `DocumentService.GetDocumentAsync` ? `_basePdfBytes` dolar (gerçek mod)
|
||||
4. `BuildFreshBaseReport()` ? `XtraReport` olu?turulur, `DxReportViewer`'a verilir
|
||||
|
||||
### `BuildFreshBaseReport()` Mant???
|
||||
```
|
||||
_basePdfBytes dolu ? XtraReport + DetailBand + XRPdfContent { GenerateOwnPages = true }
|
||||
_basePdfBytes bo? (ForceToUseFakeDocument=true) ? ReportStorage'dan LargeDatasetReport ?ablonu (XtraReport, designer'dan geldi?i gibi)
|
||||
```
|
||||
|
||||
> NOT: `_basePdfBytes` dal? korunur (gerçek PDF modu). Dev ve test sunucusunda `PredefinedReport` (LargeDatasetReport) `XtraReport` olarak do?rudan kullan?l?r — PDF'e export ED?LMEZ.
|
||||
|
||||
### ?mza Popup'? ("Unterschrift erstellen")
|
||||
- Sekmeler: Çizim / Yaz? / Resim
|
||||
- Alanlar: Ad soyad (zorunlu), pozisyon (opsiyonel), yer (zorunlu)
|
||||
- `_capturedSignature` record'una kaydedilir
|
||||
- Annotation varsa popup kapan?r ? JS checkbox overlay kurulur
|
||||
|
||||
### JS Checkbox Overlay (`receiver-signature.js`)
|
||||
- `receiverSignature.installAnnotationCheckboxes(annotations, checkedIds, dotNetRef)` C#'tan ça?r?l?r
|
||||
- Her annotation için `.annot-sig-cb-wrapper` div'i, viewer scroll container'?na absolute olarak yerle?tirilir
|
||||
- **Koordinat hesab?:** `left = pageRect.left + ann.x * scaleX`, `top = pageRect.top + ann.y * scaleY`
|
||||
- `scaleX = sayfaPixelGeni?li?i / 827`, `scaleY = sayfaPixelYüksekli?i / 1169`
|
||||
- Bu koordinatlar sayfa-relatif ve do?ru çal???yor
|
||||
- T?klan?nca `dotNetRef.invokeMethodAsync('OnAnnotationToggled', id, checked)` ça?r?l?r
|
||||
|
||||
### ?mza Uygulama ("Unterschriften anwenden" — `SubmitSignaturesAsync`)
|
||||
|
||||
**Tek ortak yol (her iki mod):**
|
||||
- `SubmitSignaturesAsync` ? `BuildFreshBaseReport()` + `WireAnnotationSignatures(report, _capturedSignature)`
|
||||
- `WireAnnotationSignatures` ? `report.AfterPrint` olay?na abone olur
|
||||
- Belge olu?unca `report.PrintingSystem.Pages` dolu olur; her annotation için `Pages[ann.Page-1]` sayfas?na:
|
||||
- `ImageBrick { Image = imza görseli, Rect = (ann.X, ann.Y, 230, 70), SizeMode = ZoomImage }`
|
||||
- `TextBrick { Text = bilgi metni, Rect = (ann.X, ann.Y+75, 230, 65) }`
|
||||
- `Page.AddBrick(brick)` ile bas?l?r
|
||||
- Brick `Rect`'leri annotation `X`/`Y` (1/100 inch) ? checkbox overlay ile birebir ayn? koordinat, do?ru sayfa+konum
|
||||
- `ViewerKey++` ile viewer yenilenir
|
||||
|
||||
**Gerçek PDF modu (`_basePdfBytes` dolu):** Yukar?daki ile ayn?; rapor `XRPdfContent`'ten olu?ur, brick'ler yine `AfterPrint` ile `PrintingSystem.Pages` üzerine bas?l?r.
|
||||
|
||||
---
|
||||
|
||||
## ReceiverUI'daki NuGet Paketleri
|
||||
|
||||
| Paket | Versiyon | Amaç |
|
||||
|---|---|---|
|
||||
| `DevExpress.Blazor.Reporting.Viewer` | 25.2.3 | DxReportViewer bile?eni |
|
||||
| `DevExpress.Blazor.PdfViewer` | 25.2.3 | PDF görüntüleyici |
|
||||
| `DevExpress.Drawing.Skia` | 25.2.3 | Çizim backend'i |
|
||||
| `SkiaSharp.*` | 3.119.1 | WASM native render |
|
||||
|
||||
> Not: iText7, ReceiverUI imzalama ak???nda KULLANILMIYOR. ?mza yerle?tirme tamamen DevExpress XtraReports brick mekanizmas?yla (`PrintingSystem.Pages[i].AddBrick`) yap?l?r.
|
||||
|
||||
---
|
||||
|
||||
## GÖREV 1: ?mza Konum Hatas? (BUG) — ÇÖZÜLDÜ
|
||||
|
||||
### Kullan?c?n?n ?ste?i
|
||||
Annotation'lardan okunan sayfa ve X/Y koordinatlar?na göre, t?pk? checkbox overlay'ler gibi, imzalar do?ru sayfa ve konumda görünsün. `_basePdfBytes` dal? korunsun; dev/test'te designer ile olu?turulan `PredefinedReport` `XtraReport` olarak do?rudan kullan?lmaya devam etsin (PDF'e export yok).
|
||||
|
||||
### ÇÖZÜM (Oturum 12) ?
|
||||
`WireAnnotationSignatures` metodu, `report.AfterPrint` olay?nda `report.PrintingSystem.Pages[ann.Page-1].AddBrick(...)` ça??rarak imza görselini (`ImageBrick`) ve bilgi metnini (`TextBrick`) do?rudan hedef sayfaya, annotation `X`/`Y` (1/100 inch) konumunda basar.
|
||||
|
||||
**Neden çal???r:**
|
||||
- `AfterPrint`, belge tamamen olu?tuktan sonra tetiklenir; `PrintingSystem.Pages` art?k gerçek/nihai sayfalar? içerir.
|
||||
- Sayfa indeksleme (`Pages[ann.Page-1]`) band veya veri-sat?r? tekrar?ndan **ba??ms?zd?r** ? `LargeDatasetReport`'un veri-ba?l? `detailBand1` sorununu tamamen atlar.
|
||||
- Brick `Rect` koordinatlar? raporun yerel 1/100 inch sistemindedir ? checkbox overlay ile birebir ayn?, do?ru konum.
|
||||
- Yeni sayfa eklenmedi?i için sayfa say?s? katlanmaz (35 sayfa ? 35 sayfa).
|
||||
|
||||
**Derleme s?ras?nda ö?renilen API gerçekleri:**
|
||||
- `PrintOnPage` olay? `e.Page` VERMEZ (yaln?zca `PageIndex`/`PageCount`) ? brick eklenemez. Do?ru olay `AfterPrint` + `PrintingSystem.Pages`.
|
||||
- `Page` tipinde `InsertBrick` YOK; do?ru metot `Page.AddBrick(brick)` (brick'in `Rect`'i konumu belirler).
|
||||
- `ImageBrick.BorderStyle` tipi `BrickBorderStyle`'dir (`BorderDashStyle` de?il). Border için `Sides` + `BorderColor` kullan?ld?.
|
||||
|
||||
### Denenen Eski Çözümler (ba?ar?s?z — referans)
|
||||
|
||||
| Deneme | Yakla??m | Sonuç | Ba?ar?s?zl?k Sebebi |
|
||||
|---|---|---|---|
|
||||
| 1 | `BottomMarginBand` + `XRPictureBox`/`XRLabel` | Her sayfan?n alt?na ç?kt? | Band her sayfada tekrarlan?r, sayfa filtresi yok |
|
||||
| 2 | `BeforePrint` + `e.Graph?.PrintingSystem` | Derleme hatas? | `CancelEventArgs`'ta `Graph` yok |
|
||||
| 3–6 | `DetailBand` + `BeforePrint` counter | Yanl?? sayfa/konum | ?ablonun `detailBand1`'i veri sat?r? ba??na tetiklenir, sayfa ba??na de?il |
|
||||
| 7 | iText7 export/reload döngüsü | 35 ? 70 sayfa | Margin uyu?mazl???, `GenerateOwnPages` sayfalar? böldü |
|
||||
| 8 | Fake modda `BottomMarginBand` fallback | Her sayfan?n alt?nda | Koordinat yanl?? |
|
||||
|
||||
---
|
||||
|
||||
## YAPILMAMASI GEREKENLER
|
||||
|
||||
| Hata | Neden Yanl?? |
|
||||
|---|---|
|
||||
| `BottomMarginBand`/`DetailBand` ile sayfa-spesifik imza | Band veri-sat?r?/sayfa ba??na tekrarlan?r, koordinat kayar |
|
||||
| `BeforePrint` counter ile sayfa filtresi | Veri-ba?l? raporda sat?r ba??na tetiklenir, güvenilmez |
|
||||
| `PrintOnPage` ile brick ekleme | `e.Page` yok; brick eklenemez |
|
||||
| `Page.InsertBrick(...)` | Yok; do?ru metot `Page.AddBrick(...)` |
|
||||
| iText7 export+reload döngüsü | Margin uyu?mazl???ndan sayfa say?s? katlan?r |
|
||||
| ?ablonu PDF'e export edip `XRPdfContent`'e yükleme | ?stenmiyor; designer raporu do?rudan kullan?lmal? |
|
||||
| Stamplama için API endpoint ekleme | Gereksiz; brick'ler client'ta bas?l?r |
|
||||
|
||||
---
|
||||
|
||||
## BEKLEYEN D??ER GÖREVLER (Sonraki Chat'te Yap?lacak)
|
||||
|
||||
### 2. ?mza Arka Plan? Özelli?i
|
||||
?mza görselinin ve bilgilerinin kaplad??? alan kadar, yar? saydam hafif gri opak dikdörtgen arka plan ekle. Böylece imza ve bilgiler arka plandaki metinlerden etkilenmez ve okunur kal?r. (Art?k brick tabanl?: `ImageBrick`/`TextBrick`'in arkas?na bir arka plan `Brick` dikdörtgeni eklenebilir.)
|
||||
|
||||
### 3. Checkbox Renk ve Stil ?yile?tirmesi
|
||||
Mevcut checkbox'lar?n rengi ve kenarl?klar? çok dikkat çekici. Koyu füme tonlar?nda, desenli, sade ve profesyonel görünümlü bir stil olsun. (`receiver-signature.js` ve ilgili CSS.)
|
||||
|
||||
### 4. Sayfa Aç?l???nda Otomatik ?mza Popup'?
|
||||
Sayfa aç?l?r aç?lmaz imza popup'? ç?ks?n. "Kay?tl? hiç bir imzan?z yok, tan?mlay?n?z" mesaj? gösterilsin. Kullan?c? imzas?n? tan?mlamadan ilerleyemesin. Mevcut "Unterschrift erstellen" butonu "?mzay? de?i?tir" olarak güncellensin.
|
||||
|
||||
### 5. Otomatik ?mza Uygulama
|
||||
Kullan?c? tüm checkbox'lar? onaylad??? anda imzalar otomatik olarak uygulanmaya ba?las?n (butona t?klamaya gerek kalmas?n). Sayfan?n üstünde imza say?s? ve imzalanmas? gereken sayfalar hakk?nda bilgi gösterilsin.
|
||||
|
||||
### 6. Checkbox - DevExpress Toolbar Pozisyon Uyumsuzlu?u (BUG)
|
||||
- Checkbox'lar browser'?n boyut/konumuna neredeyse anl?k tepki veriyor
|
||||
- DevExpress toolbar de?i?ikliklerine geç tepki gösteriyor
|
||||
- Zoom de?i?ince bazen 2-3 PDF yan yana gelebiliyor; checkbox o konumda do?ru görünüyor ama iki PDF'in yan yana gelmesi kald?r?lmal?
|
||||
- `DocumentViewer.razor`'daki `DxDocumentViewer` + `DxDocumentViewerTabPanelSettings` bile?enleri daha uygun olabilir mi? De?erlendirmesi yap?lacak.
|
||||
|
||||
---
|
||||
|
||||
## De?i?iklik Günlü?ü
|
||||
|
||||
| Oturum | De?i?iklik |
|
||||
|---|---|
|
||||
| 1–3 | Temel altyap?: servisler, YARP proxy, JS overlay, imza pad |
|
||||
| 4 | `AddSignatureAtAnnotation` + BottomMarginBand ? ? her sayfada tekrar |
|
||||
| 5 | `BeforePrint` + `e.Graph?.PrintingSystem` ? ? derleme hatas? |
|
||||
| 6 | BeforePrint counter ? ? do?ru yakla??m, yanl?? band |
|
||||
| 7 | DetailBand'e geçi? ? ? do?ru band, koordinat hâlâ yanl?? |
|
||||
| 8 | `(page-1)*1169+Y` ? ? sayfa ?i?mesi (35?140) |
|
||||
| 9 | `BoundsF.Y = ann.Y` + counter ? (?ablon raporda çal??m?yor) |
|
||||
| 10 | iText7 `StampSignaturesOnPdf` gerçek modda ?, fake modda export+reload ? (35?70), BottomMarginBand fallback ? |
|
||||
| 11 | COPILOT_CONTEXT_TR.md ve COPILOT_CONTEXT_EN.md ayr? dosyalar olarak yeniden olu?turuldu |
|
||||
| **12** | **GÖREV 1 ÇÖZÜLDÜ ?** — `WireAnnotationSignatures`: `report.AfterPrint` + `PrintingSystem.Pages[ann.Page-1].AddBrick(ImageBrick/TextBrick)`. Sayfa hedefleme band'dan ba??ms?z, sayfa say?s? katlanm?yor. iText7 ReceiverUI ak???ndan ç?kar?ld?. `AddSignatureAtAnnotation`/`RemoveExistingSignatureById` kald?r?ld?. Derleme ba?ar?l?. |
|
||||
@@ -1,16 +1,19 @@
|
||||
using DigitalData.Core.Abstraction.Application.DTO;
|
||||
using DigitalData.Core.Exceptions;
|
||||
using EnvelopeGenerator.API.Extensions;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Application.Common.Extensions;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
using EnvelopeGenerator.Application.Common.Notifications.DocSigned;
|
||||
using EnvelopeGenerator.Application.Documents.Queries;
|
||||
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
||||
using EnvelopeGenerator.Application.Histories.Queries;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.API.Extensions;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.API.Controllers;
|
||||
|
||||
57
EnvelopeGenerator.API/Controllers/SignatureController.cs
Normal file
57
EnvelopeGenerator.API/Controllers/SignatureController.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using EnvelopeGenerator.API.Extensions;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Application.Common.Extensions;
|
||||
using EnvelopeGenerator.Application.Documents.Queries;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class SignatureController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="SignatureController"/>.
|
||||
/// </summary>
|
||||
public SignatureController(IMediator mediator)
|
||||
{
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
//TODO: update to use signature query
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="envelopeKey"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||
[HttpGet("{envelopeKey}")]
|
||||
public async Task<IActionResult> GetAnnotsOfReceiver(string envelopeKey, CancellationToken cancel)
|
||||
{
|
||||
int envelopeId = User.GetEnvelopeIdOfReceiver();
|
||||
|
||||
int receiverId = User.GetReceiverIdOfReceiver();
|
||||
|
||||
var doc = await _mediator.Send(new ReadDocumentQuery() { EnvelopeId = envelopeId }, cancel);
|
||||
|
||||
if (doc.Elements is not IEnumerable<SignatureDto> docSignatures)
|
||||
return NotFound("Document is empty.");
|
||||
|
||||
var rcvSignatures = docSignatures.Where(s => s.ReceiverId == receiverId).ToList();
|
||||
|
||||
if (rcvSignatures is null)
|
||||
return NotFound("No signatures found for the current receiver.");
|
||||
else
|
||||
return Ok(rcvSignatures);
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,9 @@
|
||||
<Authors>Digital Data GmbH</Authors>
|
||||
<Company>Digital Data GmbH</Company>
|
||||
<Product>EnvelopeGenerator.GeneratorAPI</Product>
|
||||
<Version>1.3.1</Version>
|
||||
<FileVersion>1.3.1</FileVersion>
|
||||
<AssemblyVersion>1.3.1</AssemblyVersion>
|
||||
<Version>1.4.0</Version>
|
||||
<FileVersion>1.4.0</FileVersion>
|
||||
<AssemblyVersion>1.4.0</AssemblyVersion>
|
||||
<PackageOutputPath>Copyright © 2025 Digital Data GmbH. All rights reserved.</PackageOutputPath>
|
||||
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace EnvelopeGenerator.API.Extensions;
|
||||
public static class ReceiverClaimExtensions
|
||||
{
|
||||
private static readonly string[] EnvelopeIdClaimTypes = [EnvelopeClaimTypes.Id, "envelope_id", "EnvelopeId"];
|
||||
private static readonly string[] ReceiverIdClaimTypes = ["receiver_id", "ReceiverId"];
|
||||
private static readonly string[] EnvelopeUuidClaimTypes = [ClaimTypes.NameIdentifier, "envelope_uuid", "EnvelopeUuid"];
|
||||
private static readonly string[] ReceiverSignatureClaimTypes = [ClaimTypes.Hash, "receiver_sig", "ReceiverSignature"];
|
||||
|
||||
@@ -81,12 +82,29 @@ public static class ReceiverClaimExtensions
|
||||
var envIdStr = user.GetRequiredClaimOfReceiver(EnvelopeIdClaimTypes);
|
||||
if (!int.TryParse(envIdStr, out var envId))
|
||||
{
|
||||
throw new InvalidOperationException($"Claim '{EnvelopeClaimTypes.Id}' is not a valid integer.");
|
||||
throw new InvalidOperationException($"Claim '{"envelope_id"}' is not a valid integer.");
|
||||
}
|
||||
|
||||
return envId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
public static int GetReceiverIdOfReceiver(this ClaimsPrincipal user)
|
||||
{
|
||||
var rcvIdStr = user.GetRequiredClaimOfReceiver(ReceiverIdClaimTypes);
|
||||
if (!int.TryParse(rcvIdStr, out var rcvId))
|
||||
{
|
||||
throw new InvalidOperationException($"Claim '{"receiver_id"}' is not a valid integer.");
|
||||
}
|
||||
|
||||
return rcvId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signs in an envelope receiver using cookie authentication and attaches envelope claims.
|
||||
/// </summary>
|
||||
|
||||
@@ -36,11 +36,27 @@
|
||||
{ "PathSet": "/index.html" }
|
||||
]
|
||||
},
|
||||
"receiver-ui-static-assets": {
|
||||
"receiver-ui-blazor-framework": {
|
||||
"ClusterId": "receiver-ui",
|
||||
"Order": 999,
|
||||
"Order": 50,
|
||||
"Match": {
|
||||
"Path": "{**catch-all}",
|
||||
"Path": "/_framework/{**catch-all}",
|
||||
"Methods": [ "GET", "HEAD" ]
|
||||
}
|
||||
},
|
||||
"receiver-ui-blazor-content": {
|
||||
"ClusterId": "receiver-ui",
|
||||
"Order": 50,
|
||||
"Match": {
|
||||
"Path": "/_content/{**catch-all}",
|
||||
"Methods": [ "GET", "HEAD" ]
|
||||
}
|
||||
},
|
||||
"receiver-ui-static-css": {
|
||||
"ClusterId": "receiver-ui",
|
||||
"Order": 200,
|
||||
"Match": {
|
||||
"Path": "/css/{**catch-all}",
|
||||
"Methods": [ "GET", "HEAD" ]
|
||||
},
|
||||
"Transforms": [
|
||||
@@ -48,29 +64,71 @@
|
||||
"ResponseHeader": "Cache-Control",
|
||||
"Set": "no-cache, no-store, must-revalidate",
|
||||
"When": "Always"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ResponseHeader": "Pragma",
|
||||
"Set": "no-cache",
|
||||
"When": "Always"
|
||||
"receiver-ui-static-js": {
|
||||
"ClusterId": "receiver-ui",
|
||||
"Order": 200,
|
||||
"Match": {
|
||||
"Path": "/js/{**catch-all}",
|
||||
"Methods": [ "GET", "HEAD" ]
|
||||
},
|
||||
"Transforms": [
|
||||
{
|
||||
"ResponseHeader": "Expires",
|
||||
"Set": "0",
|
||||
"ResponseHeader": "Cache-Control",
|
||||
"Set": "no-cache, no-store, must-revalidate",
|
||||
"When": "Always"
|
||||
}
|
||||
]
|
||||
},
|
||||
"receiver-ui-annotation-fake": {
|
||||
"receiver-ui-fake-data": {
|
||||
"ClusterId": "receiver-ui",
|
||||
"Order": 10,
|
||||
"Order": 200,
|
||||
"Match": {
|
||||
"Path": "/api/Annotation/{envelopeKey}",
|
||||
"Path": "/fake-data/{**catch-all}",
|
||||
"Methods": [ "GET", "HEAD" ]
|
||||
}
|
||||
},
|
||||
"Transforms": [
|
||||
{ "PathSet": "/fake-data/annotations.json" }
|
||||
]
|
||||
"receiver-ui-appsettings": {
|
||||
"ClusterId": "receiver-ui",
|
||||
"Order": 50,
|
||||
"Match": {
|
||||
"Path": "/appsettings.json",
|
||||
"Methods": [ "GET", "HEAD" ]
|
||||
}
|
||||
},
|
||||
"receiver-ui-appsettings-dev": {
|
||||
"ClusterId": "receiver-ui",
|
||||
"Order": 50,
|
||||
"Match": {
|
||||
"Path": "/appsettings.Development.json",
|
||||
"Methods": [ "GET", "HEAD" ]
|
||||
}
|
||||
},
|
||||
"receiver-ui-styles": {
|
||||
"ClusterId": "receiver-ui",
|
||||
"Order": 50,
|
||||
"Match": {
|
||||
"Path": "/EnvelopeGenerator.ReceiverUI.styles.css",
|
||||
"Methods": [ "GET", "HEAD" ]
|
||||
}
|
||||
},
|
||||
"receiver-ui-fonts": {
|
||||
"ClusterId": "receiver-ui",
|
||||
"Order": 200,
|
||||
"Match": {
|
||||
"Path": "/fonts/{**catch-all}",
|
||||
"Methods": [ "GET", "HEAD" ]
|
||||
}
|
||||
},
|
||||
"receiver-ui-images": {
|
||||
"ClusterId": "receiver-ui",
|
||||
"Order": 200,
|
||||
"Match": {
|
||||
"Path": "/images/{**catch-all}",
|
||||
"Methods": [ "GET", "HEAD" ]
|
||||
}
|
||||
},
|
||||
"auth-login": {
|
||||
"ClusterId": "auth-hub",
|
||||
|
||||
@@ -37,26 +37,29 @@ public record AnnotationCreateDto
|
||||
/// <summary>
|
||||
/// Horizontal position of the signature field on the page.
|
||||
/// <br/><br/>
|
||||
/// <b>DevExpress unit:</b> Hundredths of an inch (1/100 inch ≈ 2.83 PDF points), origin at the <b>top-left</b> corner of the page, X increases to the right.
|
||||
/// <b>Unit:</b> INCHES (GdPicture14 native), origin at the <b>top-left</b> corner of the page, X increases to the right.
|
||||
/// <br/>
|
||||
/// <b>Difference from PSPDFKit:</b> PSPDFKit also uses top-left origin but measures in PDF points (1/72 inch).
|
||||
/// To convert: <c>xDevExpress = xPsPdfKit * (100.0 / 72.0)</c>
|
||||
/// <b>Conversion to DevExpress:</b> Multiply by 100 (DX uses 1/100 inch).
|
||||
/// Convert: <c>xDX = xInches * 100.0</c>
|
||||
/// <br/>
|
||||
/// <b>Difference from GDPicture:</b> GDPicture uses PDF points with <b>bottom-left</b> origin (standard PDF coordinate system).
|
||||
/// The X axis is the same direction, only unit conversion is needed: <c>xDevExpress = xGdPicture * (100.0 / 72.0)</c>
|
||||
/// <b>Conversion to PDF Points:</b> Multiply by 72 (PSPDFKit, iText7 use 1/72 inch).
|
||||
/// Convert: <c>xPt = xInches * 72.0</c>
|
||||
/// </summary>
|
||||
public double? X { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Vertical position of the signature field on the page.
|
||||
/// <br/><br/>
|
||||
/// <b>DevExpress unit:</b> Hundredths of an inch (1/100 inch ≈ 2.83 PDF points), origin at the <b>top-left</b> corner of the page, Y increases downward.
|
||||
/// <b>Unit:</b> INCHES (GdPicture14 native), origin at the <b>top-left</b> corner of the page, Y increases downward.
|
||||
/// <br/>
|
||||
/// <b>Difference from PSPDFKit:</b> PSPDFKit also uses top-left origin and Y increases downward, but measures in PDF points (1/72 inch).
|
||||
/// To convert: <c>yDevExpress = yPsPdfKit * (100.0 / 72.0)</c>
|
||||
/// <b>Conversion to DevExpress:</b> Multiply by 100 (DX uses 1/100 inch).
|
||||
/// Convert: <c>yDX = yInches * 100.0</c>
|
||||
/// <br/>
|
||||
/// <b>Difference from GDPicture:</b> GDPicture uses PDF points with <b>bottom-left</b> origin, so Y increases <b>upward</b> (PDF standard).
|
||||
/// To convert: <c>yDevExpress = (pageHeightInPt - yGdPicture - elementHeightInPt) * (100.0 / 72.0)</c>
|
||||
/// <b>Conversion to PDF Points (top-left origin):</b> Multiply by 72.
|
||||
/// Convert: <c>yPt = yInches * 72.0</c>
|
||||
/// <br/>
|
||||
/// <b>Conversion to PDF Points (bottom-left origin - iText7):</b> Y-flip required.
|
||||
/// Convert: <c>yPt = (pageHeightInches - yInches - elemHeightInches) * 72.0</c>
|
||||
/// </summary>
|
||||
public double? Y { get; init; }
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using EnvelopeGenerator.Domain.Interfaces;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Domain.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
@@ -93,4 +94,14 @@ public class SignatureDto : ISignature
|
||||
/// Gets or sets the left position of the element (in layout terms).
|
||||
/// </summary>
|
||||
public double Left => X;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public IEnumerable<AnnotationDto>? Annotations { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public SenderAppType SenderAppType { get; set; } = SenderAppType.LegacyFormApp;
|
||||
}
|
||||
@@ -53,14 +53,17 @@ public class ReadDocumentQueryHandler : IRequestHandler<ReadDocumentQuery, Docum
|
||||
/// </exception>
|
||||
public async Task<DocumentDto> Handle(ReadDocumentQuery query, CancellationToken cancel)
|
||||
{
|
||||
var docQuery = _repo.Query.Include(doc => doc.Elements).ThenInclude(e => e.Annotations);
|
||||
|
||||
if (query.Id is not null)
|
||||
{
|
||||
var doc = await _repo.Query.Where(d => d.Id == query.Id).FirstOrDefaultAsync(cancel);
|
||||
var doc = await docQuery.Where(d => d.Id == query.Id).FirstOrDefaultAsync(cancel);
|
||||
|
||||
return _mapper.Map<DocumentDto>(doc);
|
||||
}
|
||||
else if (query.EnvelopeId is not null)
|
||||
{
|
||||
var doc = await _repo.Query.Where(d => d.EnvelopeId == query.EnvelopeId).FirstOrDefaultAsync(cancel);
|
||||
var doc = await docQuery.Where(d => d.EnvelopeId == query.EnvelopeId).FirstOrDefaultAsync(cancel);
|
||||
return _mapper.Map<DocumentDto>(doc);
|
||||
}
|
||||
|
||||
|
||||
8
EnvelopeGenerator.Domain/Constants/SenderAppType.cs
Normal file
8
EnvelopeGenerator.Domain/Constants/SenderAppType.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace EnvelopeGenerator.Domain.Constants
|
||||
{
|
||||
public enum SenderAppType
|
||||
{
|
||||
LegacyFormApp = 0,
|
||||
ReceiverUIBlazorApp = 1
|
||||
}
|
||||
}
|
||||
@@ -14,11 +14,11 @@
|
||||
<PackageIcon>Assets\icon.ico</PackageIcon>
|
||||
<PackageTags>digital data envelope generator web</PackageTags>
|
||||
<Description>EnvelopeGenerator.ReceiverUI is a Blazor WebAssembly application developed to manage signing processes. It uses Entity Framework Core (EF Core) for database operations. The user interface for signing processes is developed with Razor View Engine (.cshtml files) and JavaScript under wwwroot, integrated with PSPDFKit. This integration allows users to view and sign documents seamlessly.</Description>
|
||||
<Version>1.3.0</Version>
|
||||
<Version>1.4.1</Version>
|
||||
<!-- NuGet package version -->
|
||||
<AssemblyVersion>1.3.0.0</AssemblyVersion>
|
||||
<AssemblyVersion>1.4.1.0</AssemblyVersion>
|
||||
<!-- Assembly version for API compatibility -->
|
||||
<FileVersion>1.3.0.0</FileVersion>
|
||||
<FileVersion>1.4.1.0</FileVersion>
|
||||
<!-- Windows file version -->
|
||||
<Copyright>Copyright © 2026 Digital Data GmbH. All rights reserved.</Copyright>
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
<PackageReference Include="DevExpress.Blazor.Reporting.Viewer" Version="25.2.3" />
|
||||
<PackageReference Include="DevExpress.Drawing.Skia" Version="25.2.3" />
|
||||
<PackageReference Include="HarfBuzzSharp.NativeAssets.WebAssembly" Version="8.3.1.2" />
|
||||
<PackageReference Include="itext" Version="8.0.5" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" Version="3.119.1" />
|
||||
<PackageReference Include="SkiaSharp.Views.Blazor" Version="3.119.1" />
|
||||
<NativeFileReference Include="$(HarfBuzzSharpStaticLibraryPath)\2.0.23\*.a" />
|
||||
|
||||
@@ -3,16 +3,19 @@ namespace EnvelopeGenerator.ReceiverUI.Models;
|
||||
/// <summary>
|
||||
/// Represents a pre-assigned signature annotation position on a specific page.
|
||||
/// <br/><br/>
|
||||
/// <b>Coordinate unit (X, Y):</b> Hundredths of an inch (1/100 inch ? 2.83 PDF points),
|
||||
/// <b>Coordinate unit (X, Y):</b> Inches (GdPicture14 native unit),
|
||||
/// origin at the <b>top-left</b> corner of the page, both axes increase downward/rightward.
|
||||
/// This matches the DevExpress XtraReports coordinate system (<see cref="System.Drawing.RectangleF"/>).
|
||||
/// <br/><br/>
|
||||
/// <b>Difference from PSPDFKit:</b> Same origin (top-left) and direction, but PSPDFKit uses PDF points (1/72 inch).
|
||||
/// Convert: <c>xDX = xPsPdf * (100.0 / 72.0)</c>
|
||||
/// <b>Conversion to DevExpress:</b> Multiply by 100 (DX uses 1/100 inch).
|
||||
/// Convert: <c>xDX = xInches * 100.0</c>
|
||||
/// <br/>
|
||||
/// <b>Difference from GDPicture:</b> GDPicture uses PDF points with <b>bottom-left</b> origin (PDF standard); Y is flipped.
|
||||
/// Convert: <c>yDX = (pageHeightPt - yGD - elemHeightPt) * (100.0 / 72.0)</c>
|
||||
/// <b>Conversion to PDF Points:</b> Multiply by 72 (1 inch = 72 points).
|
||||
/// Convert: <c>xPt = xInches * 72.0</c>
|
||||
/// <br/>
|
||||
/// <b>Y-axis for PDF (bottom-left origin):</b> Flip required for iText7.
|
||||
/// Convert: <c>yPt = (pageHeightInches - yInches - elemHeightInches) * 72.0</c>
|
||||
/// </summary>
|
||||
[Obsolete("Use SignatureDto with SignatureService.")]
|
||||
public record AnnotationDto
|
||||
{
|
||||
/// <summary>Unique identifier of the annotation.</summary>
|
||||
@@ -21,9 +24,9 @@ public record AnnotationDto
|
||||
/// <summary>1-based page number within the document.</summary>
|
||||
public int Page { get; init; }
|
||||
|
||||
/// <summary>Horizontal position in hundredths of an inch from the left edge of the page.</summary>
|
||||
/// <summary>Horizontal position in INCHES from the left edge of the page.</summary>
|
||||
public double X { get; init; }
|
||||
|
||||
/// <summary>Vertical position in hundredths of an inch from the top edge of the page.</summary>
|
||||
/// <summary>Vertical position in INCHES from the top edge of the page.</summary>
|
||||
public double Y { get; init; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace EnvelopeGenerator.ReceiverUI.Models.Constants
|
||||
{
|
||||
public enum SenderAppType
|
||||
{
|
||||
LegacyFormApp = 0,
|
||||
ReceiverUIBlazorApp = 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
namespace EnvelopeGenerator.ReceiverUI.Models.Constants;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the unit of measurement for coordinate values in signature positioning.
|
||||
/// Used for converting coordinates between different systems (GdPicture14, PDF.js, iText7).
|
||||
/// </summary>
|
||||
public enum UnitOfLength
|
||||
{
|
||||
/// <summary>
|
||||
/// Inch unit (1 inch = 25.4 mm).
|
||||
/// This is the native unit used by GdPicture14 (EnvelopeGenerator.Form - Legacy VB.NET app).
|
||||
/// Database stores all coordinates (X, Y, Width, Height) in INCHES.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Source:</b> GdPicture14.Annotations.AnnotationStickyNote uses INCHES natively.
|
||||
/// <br/>
|
||||
/// <b>Evidence:</b> VB.NET code directly assigns database values to annotation properties without conversion:
|
||||
/// <code>
|
||||
/// oAnnotation.Left = CSng(pElement.X) ' Direct assignment → INCHES
|
||||
/// oAnnotation.Top = CSng(pElement.Y)
|
||||
/// </code>
|
||||
/// <b>Standard Page Dimensions:</b>
|
||||
/// <list type="bullet">
|
||||
/// <item>A4: 8.27" × 11.69" (210mm × 297mm)</item>
|
||||
/// <item>Letter: 8.5" × 11"</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
Inch = 0,
|
||||
|
||||
/// <summary>
|
||||
/// PDF Point unit (1 point = 1/72 inch).
|
||||
/// This is the standard unit used by PDF specification and PDF.js viewer.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Definition:</b> According to PDF specification and Microsoft documentation:
|
||||
/// <br/>
|
||||
/// <i>"PDF pages are sized in point units. 1 pt == 1/72 inch"</i>
|
||||
/// <br/><br/>
|
||||
/// <b>Conversion Formula:</b>
|
||||
/// <code>
|
||||
/// points = inches * 72.0
|
||||
/// inches = points / 72.0
|
||||
/// </code>
|
||||
/// <b>Important:</b> Point ≠ Pixel!
|
||||
/// <list type="bullet">
|
||||
/// <item><b>Point (pt):</b> Device-independent unit (always 1/72 inch)</item>
|
||||
/// <item><b>Pixel (px):</b> Device-dependent unit (varies with screen DPI)</item>
|
||||
/// <item>At 72 DPI: 1 point = 1 pixel (coincidence)</item>
|
||||
/// <item>At 96 DPI: 1 point ≈ 1.33 pixels</item>
|
||||
/// <item>At 300 DPI: 1 point ≈ 4.17 pixels</item>
|
||||
/// </list>
|
||||
/// <b>Standard Page Dimensions (in points):</b>
|
||||
/// <list type="bullet">
|
||||
/// <item>A4: 595 × 842 points (8.27" × 11.69" × 72)</item>
|
||||
/// <item>Letter: 612 × 792 points (8.5" × 11" × 72)</item>
|
||||
/// </list>
|
||||
/// <b>Usage in EnvelopeGenerator:</b>
|
||||
/// <list type="bullet">
|
||||
/// <item>PDF.js viewer expects coordinates in points</item>
|
||||
/// <item>iText7 library uses points for PDF manipulation</item>
|
||||
/// <item>PSPDFKit (Web) uses points for annotation placement</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
Point
|
||||
}
|
||||
62
EnvelopeGenerator.ReceiverUI/Models/SignatureCaptureDto.cs
Normal file
62
EnvelopeGenerator.ReceiverUI/Models/SignatureCaptureDto.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
namespace EnvelopeGenerator.ReceiverUI.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a captured signature with metadata created by the receiver in the signature popup.
|
||||
/// This model holds the signature image (as base64 data URL) along with signer information
|
||||
/// used for rendering applied signatures on the PDF canvas.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Used in:</b> EnvelopeViewer.razor signature popup workflow
|
||||
/// <br/>
|
||||
/// <b>Creation:</b> User draws/types/uploads signature and fills required fields
|
||||
/// <br/>
|
||||
/// <b>Storage:</b> Session-only (Blazor component state, lost on page refresh)
|
||||
/// <br/>
|
||||
/// <b>Rendering:</b> Applied signatures display: Image + Separator + Name/Position/Place/Date
|
||||
/// </remarks>
|
||||
public sealed record SignatureCaptureDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Base64-encoded data URL of the signature image.
|
||||
/// <br/>
|
||||
/// <b>Format:</b> <c>data:image/png;base64,iVBORw0KG...</c>
|
||||
/// <br/>
|
||||
/// <b>Source:</b> Canvas.toDataURL() from signature pad (draw/text/image tabs)
|
||||
/// <br/>
|
||||
/// <b>Usage:</b> Set as <c>img.src</c> in applied signature overlay
|
||||
/// </summary>
|
||||
public required string DataUrl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Full name of the signer (first and last name).
|
||||
/// <br/>
|
||||
/// <b>Required:</b> Yes (validated in popup)
|
||||
/// <br/>
|
||||
/// <b>Display:</b> Bold text in applied signature block
|
||||
/// <br/>
|
||||
/// <b>Example:</b> "Max Mustermann"
|
||||
/// </summary>
|
||||
public required string FullName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Job title or position of the signer.
|
||||
/// <br/>
|
||||
/// <b>Required:</b> No (optional field)
|
||||
/// <br/>
|
||||
/// <b>Display:</b> Normal weight text between name and place/date
|
||||
/// <br/>
|
||||
/// <b>Example:</b> "Geschäftsführer" or empty string
|
||||
/// </summary>
|
||||
public string Position { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Location/place where the signature was created.
|
||||
/// <br/>
|
||||
/// <b>Required:</b> Yes (validated in popup)
|
||||
/// <br/>
|
||||
/// <b>Display:</b> Shown with current date in German format (dd.MM.yyyy)
|
||||
/// <br/>
|
||||
/// <b>Example:</b> "Berlin" ? rendered as "Berlin, 26.01.2025"
|
||||
/// </summary>
|
||||
public required string Place { get; init; }
|
||||
}
|
||||
101
EnvelopeGenerator.ReceiverUI/Models/SignatureDto.cs
Normal file
101
EnvelopeGenerator.ReceiverUI/Models/SignatureDto.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using EnvelopeGenerator.ReceiverUI.Models.Constants;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a signature position on a PDF page.
|
||||
/// Coordinates stored in INCHES (GdPicture14 native unit).
|
||||
/// Origin: Top-left corner, X increases right, Y increases down.
|
||||
/// </summary>
|
||||
public class SignatureDto
|
||||
{
|
||||
/// <summary>Unique identifier.</summary>
|
||||
public int Id { get; init; }
|
||||
|
||||
private double _x;
|
||||
private double _y;
|
||||
|
||||
/// <summary>Horizontal position in INCHES from left edge.</summary>
|
||||
public double X
|
||||
{
|
||||
get => _x * Factor;
|
||||
init => _x = value;
|
||||
}
|
||||
|
||||
/// <summary>Vertical position in INCHES from top edge.</summary>
|
||||
public double Y
|
||||
{
|
||||
get => _y * Factor;
|
||||
init => _y = value;
|
||||
}
|
||||
|
||||
/// <summary>1-based page number.</summary>
|
||||
public int Page { get; init; }
|
||||
|
||||
/// <summary>Sender application type that created this signature.</summary>
|
||||
public SenderAppType SenderAppType { get; init; }
|
||||
|
||||
private UnitOfLength _unitOfLength;
|
||||
|
||||
public SignatureDto Convert(UnitOfLength unitOfLength)
|
||||
{
|
||||
_unitOfLength = unitOfLength;
|
||||
return this;
|
||||
}
|
||||
|
||||
public double Factor
|
||||
{
|
||||
get
|
||||
{
|
||||
if (SenderAppType != SenderAppType.LegacyFormApp)
|
||||
{
|
||||
throw new NotImplementedException(
|
||||
$"SenderAppType '{SenderAppType}' is not yet implemented. " +
|
||||
$"Currently, only '{nameof(SenderAppType.LegacyFormApp)}' is supported. " +
|
||||
$"Future implementations will handle '{nameof(SenderAppType.ReceiverUIBlazorApp)}' and other types.");
|
||||
}
|
||||
|
||||
// LegacyFormApp uses GdPicture14 with INCHES
|
||||
return _unitOfLength switch
|
||||
{
|
||||
UnitOfLength.Inch => 1.0, // No conversion needed: INCHES → INCHES
|
||||
UnitOfLength.Point => 72.0, // INCHES → PDF Points: 1 inch = 72 points (PDF standard, NOT pixels!)
|
||||
_ => throw new InvalidOperationException(
|
||||
$"Unknown UnitOfLength: {_unitOfLength}. Expected '{nameof(UnitOfLength.Inch)}' or '{nameof(UnitOfLength.Point)}'.")
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class SignatureDtoExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts all signatures in the collection to the specified unit of length.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of the collection (IEnumerable, List, etc.)</typeparam>
|
||||
/// <param name="signatures">Collection of SignatureDto objects to convert.</param>
|
||||
/// <param name="unitOfLength">Target unit of measurement (Inch or Point).</param>
|
||||
/// <returns>The same collection with all signatures converted to the specified unit.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when signatures collection is null.</exception>
|
||||
/// <remarks>
|
||||
/// <b>Usage:</b>
|
||||
/// <code>
|
||||
/// var signatures = await SignatureService.GetAsync(envelopeKey);
|
||||
/// var convertedSignatures = signatures.ConvertAll(UnitOfLength.Point);
|
||||
/// </code>
|
||||
/// <b>Note:</b> This method modifies each SignatureDto object in place and returns the same collection.
|
||||
/// </remarks>
|
||||
public static T Convert<T>(this T signatures, UnitOfLength unitOfLength)
|
||||
where T : IEnumerable<SignatureDto>
|
||||
{
|
||||
if (signatures == null)
|
||||
throw new ArgumentNullException(nameof(signatures));
|
||||
|
||||
foreach (var signature in signatures)
|
||||
{
|
||||
signature.Convert(unitOfLength);
|
||||
}
|
||||
|
||||
return signatures;
|
||||
}
|
||||
}
|
||||
71
EnvelopeGenerator.ReceiverUI/Options/PdfViewerOptions.cs
Normal file
71
EnvelopeGenerator.ReceiverUI/Options/PdfViewerOptions.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
namespace EnvelopeGenerator.ReceiverUI.Options;
|
||||
|
||||
public class PdfViewerOptions
|
||||
{
|
||||
public const string SectionName = "PdfViewer";
|
||||
|
||||
/// <summary>
|
||||
/// Base scale for thumbnail rendering (0.2 - 1.5 recommended)
|
||||
/// Higher values = better quality but slower rendering
|
||||
/// Default: 0.75
|
||||
/// </summary>
|
||||
public double ThumbnailBaseScale { get; set; } = 0.75;
|
||||
|
||||
/// <summary>
|
||||
/// Enable HiDPI/Retina support for thumbnails
|
||||
/// Default: true
|
||||
/// </summary>
|
||||
public bool ThumbnailEnableHiDPI { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum device pixel ratio multiplier for thumbnails (1.0 - 3.0)
|
||||
/// Caps DPR to avoid excessive memory usage on 4K+ displays
|
||||
/// Default: 2.0
|
||||
/// </summary>
|
||||
public double ThumbnailMaxDPR { get; set; } = 2.0;
|
||||
|
||||
/// <summary>
|
||||
/// Enable HiDPI/Retina support for main PDF canvas
|
||||
/// Default: true
|
||||
/// </summary>
|
||||
public bool MainCanvasEnableHiDPI { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum device pixel ratio multiplier for main canvas (1.0 - 3.0)
|
||||
/// Default: 2.0
|
||||
/// </summary>
|
||||
public double MainCanvasMaxDPR { get; set; } = 2.0;
|
||||
|
||||
/// <summary>
|
||||
/// Enable smooth zoom transition (fade effect)
|
||||
/// Default: true
|
||||
/// </summary>
|
||||
public bool EnableSmoothZoom { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Zoom transition duration in milliseconds (50 - 500)
|
||||
/// Default: 150
|
||||
/// </summary>
|
||||
public int ZoomTransitionDuration { get; set; } = 150;
|
||||
|
||||
/// <summary>
|
||||
/// Opacity during rendering (0.0 - 1.0)
|
||||
/// Lower values = more visible fade effect
|
||||
/// Default: 0.85
|
||||
/// </summary>
|
||||
public double RenderingOpacity { get; set; } = 0.85;
|
||||
|
||||
/// <summary>
|
||||
/// Delay between thumbnail renders in milliseconds (10 - 200)
|
||||
/// Higher values = less browser stress, slower initial load
|
||||
/// Default: 50
|
||||
/// </summary>
|
||||
public int ThumbnailRenderDelay { get; set; } = 50;
|
||||
|
||||
/// <summary>
|
||||
/// Zoom step percentage (1 - 50)
|
||||
/// Controls how much zoom changes per click or scroll
|
||||
/// Default: 5 (5% per step)
|
||||
/// </summary>
|
||||
public int ZoomStepPercentage { get; set; } = 5;
|
||||
}
|
||||
@@ -1,61 +1,139 @@
|
||||
@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
|
||||
@inject AppVersionService AppVersion
|
||||
@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="@AppVersion.GetVersionedUrl("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="@AppVersion.GetVersionedUrl("js/pdf-viewer.js")"></script>
|
||||
<script src="@AppVersion.GetVersionedUrl("js/receiver-signature.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"/>
|
||||
<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>
|
||||
</div>
|
||||
<div>
|
||||
<div class="envelope-title">Dokumentenansicht</div>
|
||||
<div class="envelope-key">ID: @EnvelopeKey</div>
|
||||
</div>
|
||||
</div>
|
||||
@if (_pdfLoaded) {
|
||||
<div class="d-flex align-items-center gap-2 ms-auto">
|
||||
<div class="pdf-controls">
|
||||
<button class="btn btn-sm btn-outline-primary" @onclick="ZoomOut" title="Zoom Out">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M6.5 1A5.5 5.5 0 0 0 1 6.5v3A5.5 5.5 0 0 0 6.5 15h3a5.5 5.5 0 0 0 5.5-5.5v-3A5.5 5.5 0 0 0 9.5 1h-3zM4 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 4 8z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<span class="zoom-level">@(_currentZoom)%</span>
|
||||
<button class="btn btn-sm btn-outline-primary" @onclick="ZoomIn" title="Zoom In">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M6.5 1A5.5 5.5 0 0 0 1 6.5v3A5.5 5.5 0 0 0 6.5 15h3a5.5 5.5 0 0 0 5.5-5.5v-3A5.5 5.5 0 0 0 9.5 1h-3zM8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
|
||||
@_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 class="pdf-navigation">
|
||||
<button class="btn btn-sm btn-primary" @onclick="PreviousPage" disabled="@(_currentPage <= 1)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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>
|
||||
<span class="page-info">Seite @_currentPage / @_totalPages</span>
|
||||
<button class="btn btn-sm btn-primary" @onclick="NextPage" disabled="@(_currentPage >= _totalPages)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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>
|
||||
|
||||
@* 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>
|
||||
@@ -88,8 +166,144 @@
|
||||
</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 class="pdf-toolbar__divider"></div>
|
||||
|
||||
@* Reset button - only show when signatures are signed *@
|
||||
@if (_signedSignatures > 0) {
|
||||
<div class="pdf-toolbar__section">
|
||||
<button class="pdf-toolbar__btn pdf-toolbar__btn--reset"
|
||||
@onclick="RestartSigning"
|
||||
title="Unterschriften zurücksetzen">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" 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>
|
||||
</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 {
|
||||
@@ -107,7 +321,169 @@
|
||||
</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;
|
||||
@@ -117,7 +493,44 @@
|
||||
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
|
||||
SignatureCaptureDto? _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)) {
|
||||
@@ -126,6 +539,13 @@
|
||||
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);
|
||||
|
||||
@@ -135,6 +555,19 @@
|
||||
} 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}";
|
||||
}
|
||||
@@ -144,18 +577,59 @@
|
||||
}
|
||||
|
||||
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}";
|
||||
@@ -169,30 +643,316 @@
|
||||
{
|
||||
_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 ZoomOut() {
|
||||
await JSRuntime.InvokeVoidAsync("pdfViewer.zoomOut");
|
||||
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);
|
||||
}
|
||||
|
||||
void RestartSigning() {
|
||||
// Force page reload to reset all signatures and state
|
||||
Navigation.NavigateTo(Navigation.Uri, forceLoad: true);
|
||||
}
|
||||
|
||||
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 SignatureCaptureDto
|
||||
{
|
||||
DataUrl = signatureDataUrl,
|
||||
FullName = _signerFullName.Trim(),
|
||||
Position = _signerPosition.Trim(),
|
||||
Place = _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() {
|
||||
|
||||
@@ -146,7 +146,7 @@
|
||||
var result = await AuthService.LoginEnvelopeReceiverAsync(EnvelopeKey, AccessCode.Trim());
|
||||
|
||||
if (result == EnvelopeLoginResult.Success) {
|
||||
Navigation.NavigateTo($"/receiver/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true);
|
||||
Navigation.NavigateTo($"/envelope/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@page "/receiver/{EnvelopeKey}"
|
||||
@page "/report-viewer/{EnvelopeKey}"
|
||||
@using System.Drawing
|
||||
@using DevExpress.Blazor
|
||||
@using DevExpress.Drawing
|
||||
@@ -320,6 +321,12 @@ Shown="OnPopupShownAsync">
|
||||
|
||||
protected override async Task OnInitializedAsync() {
|
||||
|
||||
// ? REDIRECT: /receiver/{key} -> /envelope/{key} (NEW PDF.js viewer)
|
||||
if (!string.IsNullOrWhiteSpace(EnvelopeKey)) {
|
||||
Navigation.NavigateTo($"/envelope/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(EnvelopeKey)) {
|
||||
var hasAccess = await AuthService.CheckEnvelopeAccessAsync(EnvelopeKey);
|
||||
if (!hasAccess) {
|
||||
|
||||
@@ -14,10 +14,14 @@ builder.RootComponents.Add<HeadOutlet>("head::after");
|
||||
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
||||
builder.Services.Configure<ApiOptions>(opts =>
|
||||
builder.Configuration.GetSection(ApiOptions.SectionName).Bind(opts));
|
||||
builder.Services.Configure<PdfViewerOptions>(opts =>
|
||||
builder.Configuration.GetSection(PdfViewerOptions.SectionName).Bind(opts));
|
||||
builder.Services.AddScoped<EnvelopeGenerator.ReceiverUI.Services.DocumentService>();
|
||||
builder.Services.AddScoped<EnvelopeGenerator.ReceiverUI.Services.AuthService>();
|
||||
builder.Services.AddScoped<EnvelopeGenerator.ReceiverUI.Services.AnnotationService>();
|
||||
builder.Services.AddScoped<EnvelopeGenerator.ReceiverUI.Services.EnvelopeReceiverService>();
|
||||
builder.Services.AddScoped<EnvelopeGenerator.ReceiverUI.Services.SignatureService>();
|
||||
builder.Services.AddSingleton<EnvelopeGenerator.ReceiverUI.Services.AppVersionService>();
|
||||
|
||||
builder.Services.AddDevExpressWebAssemblyBlazorReportViewer();
|
||||
builder.Services.AddDevExpressWebAssemblyBlazorPdfViewer();
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace EnvelopeGenerator.ReceiverUI.Services;
|
||||
/// <c>fake-data/annotations.json</c>. To switch to real data, update the
|
||||
/// YARP route in <c>yarp.json</c> — no code change required.
|
||||
/// </summary>
|
||||
[Obsolete("Use SignatureService.")]
|
||||
public class AnnotationService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
||||
{
|
||||
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
||||
|
||||
26
EnvelopeGenerator.ReceiverUI/Services/AppVersionService.cs
Normal file
26
EnvelopeGenerator.ReceiverUI/Services/AppVersionService.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
namespace EnvelopeGenerator.ReceiverUI.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Provides application version for cache busting static assets.
|
||||
/// Version is automatically incremented on each build via AssemblyVersion.
|
||||
/// </summary>
|
||||
public class AppVersionService
|
||||
{
|
||||
/// <summary>
|
||||
/// Current application version (e.g., "1.0.0.0")
|
||||
/// </summary>
|
||||
public string Version { get; }
|
||||
|
||||
public AppVersionService()
|
||||
{
|
||||
// Get version from assembly metadata
|
||||
Version = typeof(AppVersionService).Assembly.GetName().Version?.ToString() ?? "1.0.0.0";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates versioned URL for static assets (cache busting)
|
||||
/// </summary>
|
||||
/// <param name="path">Asset path (e.g., "css/envelope-viewer.css")</param>
|
||||
/// <returns>Versioned URL (e.g., "css/envelope-viewer.css?v=1.0.0.0")</returns>
|
||||
public string GetVersionedUrl(string path) => $"{path}?v={Version}";
|
||||
}
|
||||
24
EnvelopeGenerator.ReceiverUI/Services/SignatureService.cs
Normal file
24
EnvelopeGenerator.ReceiverUI/Services/SignatureService.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using EnvelopeGenerator.ReceiverUI.Models;
|
||||
using EnvelopeGenerator.ReceiverUI.Options;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.Services;
|
||||
|
||||
public class SignatureService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
||||
{
|
||||
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
||||
|
||||
public async Task<IReadOnlyList<SignatureDto>> GetAsync(string envelopeKey, CancellationToken cancel = default)
|
||||
{
|
||||
var url = $"{apiOptions.Value.BaseUrl}/api/Signature/{Uri.EscapeDataString(envelopeKey)}";
|
||||
var response = await http.GetAsync(url, cancel);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
throw new HttpRequestException($"Failed to retrieve signatures for envelope {envelopeKey}: {response.StatusCode} {response.ReasonPhrase}");
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<List<SignatureDto>>(_jsonOptions, cancel);
|
||||
return result ?? [];
|
||||
}
|
||||
}
|
||||
@@ -2,5 +2,17 @@
|
||||
"Api": {
|
||||
"BaseUrl": "",
|
||||
"ForceToUseFakeDocument": false
|
||||
},
|
||||
"PdfViewer": {
|
||||
"ThumbnailBaseScale": 0.75,
|
||||
"ThumbnailEnableHiDPI": true,
|
||||
"ThumbnailMaxDPR": 2.0,
|
||||
"MainCanvasEnableHiDPI": true,
|
||||
"MainCanvasMaxDPR": 2.0,
|
||||
"EnableSmoothZoom": true,
|
||||
"ZoomTransitionDuration": 900,
|
||||
"RenderingOpacity": 0.85,
|
||||
"ThumbnailRenderDelay": 50,
|
||||
"ZoomStepPercentage": 5
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,20 +43,6 @@
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.pdf-controls, .pdf-navigation {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.zoom-level, .page-info {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
min-width: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.envelope-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
@@ -68,9 +54,434 @@
|
||||
.pdf-viewer-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.pdf-thumbnails {
|
||||
position: relative;
|
||||
width: 260px;
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 16px 0 0 16px;
|
||||
box-shadow:
|
||||
0 8px 32px rgba(0, 0, 0, 0.12),
|
||||
0 0 0 1px rgba(126, 34, 206, 0.1);
|
||||
border: 1px solid rgba(126, 34, 206, 0.15);
|
||||
border-right: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pdf-thumbnails__content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.pdf-thumbnails__content::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.pdf-thumbnails__content::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.pdf-thumbnails__content::-webkit-scrollbar-thumb {
|
||||
background: linear-gradient(135deg, #7e22ce 0%, #2a5298 100%);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.pdf-thumbnails__content::-webkit-scrollbar-thumb:hover {
|
||||
background: linear-gradient(135deg, #6b1cb0 0%, #1e3a72 100%);
|
||||
}
|
||||
|
||||
.pdf-splitter {
|
||||
width: 4px;
|
||||
background: transparent;
|
||||
cursor: col-resize;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
transition: background 0.2s ease;
|
||||
z-index: 10;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.pdf-splitter::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -4px;
|
||||
right: -4px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
/* Enlarged hitbox for easier grabbing */
|
||||
}
|
||||
|
||||
.pdf-splitter:hover,
|
||||
.pdf-splitter.resizing {
|
||||
background: linear-gradient(90deg,
|
||||
rgba(126, 34, 206, 0.4) 0%,
|
||||
rgba(42, 82, 152, 0.4) 100%);
|
||||
}
|
||||
|
||||
.pdf-splitter:active {
|
||||
background: linear-gradient(90deg,
|
||||
rgba(126, 34, 206, 0.6) 0%,
|
||||
rgba(42, 82, 152, 0.6) 100%);
|
||||
}
|
||||
|
||||
/* Prevent text selection during resize */
|
||||
body.resizing {
|
||||
user-select: none;
|
||||
cursor: col-resize !important;
|
||||
}
|
||||
|
||||
.pdf-thumbnail {
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
border: 2px solid transparent;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.pdf-thumbnail:hover {
|
||||
border-color: rgba(126, 34, 206, 0.3);
|
||||
box-shadow: 0 4px 16px rgba(126, 34, 206, 0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.pdf-thumbnail--active {
|
||||
border-color: #7e22ce;
|
||||
box-shadow:
|
||||
0 4px 16px rgba(126, 34, 206, 0.3),
|
||||
0 0 0 3px rgba(126, 34, 206, 0.1);
|
||||
}
|
||||
|
||||
.pdf-thumbnail__preview {
|
||||
width: 100%;
|
||||
aspect-ratio: 210 / 297;
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pdf-thumbnail__canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
object-position: center;
|
||||
image-rendering: -webkit-optimize-contrast;
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
|
||||
.pdf-thumbnail__label {
|
||||
padding: 0.5rem;
|
||||
text-align: center;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
background: rgba(126, 34, 206, 0.03);
|
||||
border-top: 1px solid rgba(126, 34, 206, 0.1);
|
||||
}
|
||||
|
||||
.pdf-thumbnail--active .pdf-thumbnail__label {
|
||||
background: linear-gradient(135deg, rgba(126, 34, 206, 0.1) 0%, rgba(42, 82, 152, 0.1) 100%);
|
||||
color: #7e22ce;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.pdf-toolbar__btn--toggle {
|
||||
background: linear-gradient(135deg, rgba(126, 34, 206, 0.08) 0%, rgba(42, 82, 152, 0.08) 100%);
|
||||
border-color: rgba(126, 34, 206, 0.25);
|
||||
}
|
||||
|
||||
.pdf-toolbar__btn--toggle:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, rgba(126, 34, 206, 0.15) 0%, rgba(42, 82, 152, 0.15) 100%);
|
||||
border-color: rgba(126, 34, 206, 0.5);
|
||||
}
|
||||
|
||||
.pdf-toolbar {
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 12px;
|
||||
padding: 0.75rem 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1.5rem;
|
||||
box-shadow:
|
||||
0 4px 16px rgba(0, 0, 0, 0.1),
|
||||
0 0 0 1px rgba(126, 34, 206, 0.1);
|
||||
border: 1px solid rgba(126, 34, 206, 0.15);
|
||||
flex-shrink: 0;
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.pdf-toolbar__section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pdf-toolbar__zoom-section {
|
||||
gap: 0.75rem;
|
||||
flex: 1;
|
||||
max-width: 400px;
|
||||
min-width: 280px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pdf-toolbar__divider {
|
||||
width: 1px;
|
||||
height: 32px;
|
||||
background: linear-gradient(180deg, transparent 0%, rgba(126, 34, 206, 0.2) 50%, transparent 100%);
|
||||
}
|
||||
|
||||
.pdf-toolbar__btn {
|
||||
background: linear-gradient(135deg, rgba(126, 34, 206, 0.05) 0%, rgba(42, 82, 152, 0.05) 100%);
|
||||
border: 1px solid rgba(126, 34, 206, 0.2);
|
||||
border-radius: 8px;
|
||||
padding: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #1e293b;
|
||||
min-width: 34px;
|
||||
min-height: 34px;
|
||||
}
|
||||
|
||||
.pdf-toolbar__btn:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, rgba(126, 34, 206, 0.1) 0%, rgba(42, 82, 152, 0.1) 100%);
|
||||
border-color: rgba(126, 34, 206, 0.4);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(126, 34, 206, 0.2);
|
||||
}
|
||||
|
||||
.pdf-toolbar__btn:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 6px rgba(126, 34, 206, 0.15);
|
||||
}
|
||||
|
||||
.pdf-toolbar__btn:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
border-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.pdf-toolbar__btn--preset {
|
||||
padding: 0.5rem 0.875rem;
|
||||
font-size: 0.813rem;
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
min-width: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pdf-toolbar__btn--preset svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pdf-toolbar__page-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
background: white;
|
||||
border: 1px solid rgba(126, 34, 206, 0.2);
|
||||
border-radius: 8px;
|
||||
padding: 0.25rem 0.625rem;
|
||||
}
|
||||
|
||||
.pdf-toolbar__page-input {
|
||||
width: 48px;
|
||||
border: none;
|
||||
outline: none;
|
||||
text-align: center;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
background: transparent;
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
.pdf-toolbar__page-input::-webkit-outer-spin-button,
|
||||
.pdf-toolbar__page-input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.pdf-toolbar__page-total {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.pdf-toolbar__zoom-slider-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.pdf-toolbar__zoom-slider {
|
||||
-webkit-appearance: none;
|
||||
width: 100%;
|
||||
min-width: 180px;
|
||||
max-width: 350px;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
background: linear-gradient(90deg,
|
||||
rgba(126, 34, 206, 0.1) 0%,
|
||||
rgba(126, 34, 206, 0.2) 50%,
|
||||
rgba(126, 34, 206, 0.1) 100%);
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pdf-toolbar__zoom-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #7e22ce 0%, #2a5298 100%);
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 8px rgba(126, 34, 206, 0.3);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.pdf-toolbar__zoom-slider::-webkit-slider-thumb:hover {
|
||||
transform: scale(1.15);
|
||||
box-shadow: 0 4px 12px rgba(126, 34, 206, 0.4);
|
||||
}
|
||||
|
||||
.pdf-toolbar__zoom-slider::-moz-range-thumb {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #7e22ce 0%, #2a5298 100%);
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
box-shadow: 0 2px 8px rgba(126, 34, 206, 0.3);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.pdf-toolbar__zoom-slider::-moz-range-thumb:hover {
|
||||
transform: scale(1.15);
|
||||
box-shadow: 0 4px 12px rgba(126, 34, 206, 0.4);
|
||||
}
|
||||
|
||||
.pdf-toolbar__zoom-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: #7e22ce;
|
||||
letter-spacing: 0.025em;
|
||||
min-width: 45px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Signature Navigation Styles */
|
||||
.pdf-toolbar__signature-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
background: linear-gradient(135deg, rgba(126, 34, 206, 0.05) 0%, rgba(42, 82, 152, 0.05) 100%);
|
||||
border: 1px solid rgba(126, 34, 206, 0.2);
|
||||
border-radius: 10px;
|
||||
padding: 0.25rem 0.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pdf-toolbar__btn--signature-nav {
|
||||
min-width: 30px;
|
||||
min-height: 30px;
|
||||
padding: 0.25rem;
|
||||
background: white;
|
||||
border: 1px solid rgba(126, 34, 206, 0.25);
|
||||
}
|
||||
|
||||
.pdf-toolbar__btn--signature-nav:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, #7e22ce 0%, #2a5298 100%);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.pdf-toolbar__btn--signature-nav:hover:not(:disabled) svg {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pdf-toolbar__signature-counter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0 0.375rem;
|
||||
}
|
||||
|
||||
.pdf-toolbar__signature-counter svg {
|
||||
color: #7e22ce;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pdf-toolbar__signature-counter-text {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pdf-toolbar__signature-badge {
|
||||
font-size: 0.625rem;
|
||||
font-weight: 700;
|
||||
padding: 0.1875rem 0.5rem;
|
||||
border-radius: 5px;
|
||||
background: linear-gradient(135deg, rgba(126, 34, 206, 0.1) 0%, rgba(42, 82, 152, 0.1) 100%);
|
||||
color: #7e22ce;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pdf-toolbar__signature-badge--complete {
|
||||
background: linear-gradient(135deg, rgba(16, 185, 129, 0.1) 0%, rgba(5, 150, 105, 0.1) 100%);
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
/* Reset Button Styles */
|
||||
.pdf-toolbar__btn--reset {
|
||||
background: linear-gradient(135deg, rgba(239, 68, 68, 0.08) 0%, rgba(220, 38, 38, 0.08) 100%);
|
||||
border-color: rgba(239, 68, 68, 0.3);
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.pdf-toolbar__btn--reset:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||
border-color: transparent;
|
||||
color: white;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.pdf-toolbar__btn--reset svg {
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.pdf-toolbar__btn--reset:hover:not(:disabled) svg {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pdf-frame {
|
||||
@@ -79,13 +490,13 @@
|
||||
box-shadow:
|
||||
0 25px 50px -12px rgba(0, 0, 0, 0.25),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.1);
|
||||
overflow: auto;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
padding: 2rem;
|
||||
height: calc(100vh - 200px);
|
||||
width: 90%;
|
||||
max-width: 1200px;
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
width: 95%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.pdf-frame::before {
|
||||
@@ -100,10 +511,89 @@
|
||||
border-radius: 16px 16px 0 0;
|
||||
}
|
||||
|
||||
.pdf-canvas {
|
||||
.pdf-canvas-wrapper {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.pdf-page-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.pdf-canvas {
|
||||
display: block;
|
||||
vertical-align: top;
|
||||
image-rendering: -webkit-optimize-contrast;
|
||||
image-rendering: crisp-edges;
|
||||
transition: opacity 0.15s ease-out;
|
||||
}
|
||||
|
||||
.pdf-canvas.rendering {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.pdf-text-layer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
opacity: 1;
|
||||
line-height: 1.0;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.pdf-text-layer > span {
|
||||
color: transparent;
|
||||
position: absolute;
|
||||
white-space: pre;
|
||||
cursor: text;
|
||||
transform-origin: 0% 0%;
|
||||
}
|
||||
|
||||
.pdf-text-layer ::selection {
|
||||
background: rgba(126, 34, 206, 0.3);
|
||||
}
|
||||
|
||||
.pdf-text-layer ::-moz-selection {
|
||||
background: rgba(126, 34, 206, 0.3);
|
||||
}
|
||||
|
||||
.pdf-signature-layer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow: visible;
|
||||
pointer-events: none;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.pdf-signature-layer .signature-button {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.signature-button {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
.signature-button:focus {
|
||||
outline: 2px solid #7e22ce;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.signature-button:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.error-container {
|
||||
@@ -142,11 +632,50 @@
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.pdf-thumbnails {
|
||||
width: 180px;
|
||||
border-radius: 0 0 0 16px;
|
||||
}
|
||||
|
||||
.pdf-thumbnails__content {
|
||||
padding: 0.75rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.pdf-toolbar {
|
||||
flex-wrap: wrap;
|
||||
padding: 0.625rem 1rem;
|
||||
gap: 0.75rem;
|
||||
width: 98%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pdf-toolbar__divider {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pdf-toolbar__zoom-section {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.pdf-toolbar__zoom-slider {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.pdf-toolbar__btn--preset {
|
||||
padding: 0.425rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.pdf-frame {
|
||||
border-radius: 12px;
|
||||
width: 98%;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.pdf-canvas-wrapper {
|
||||
padding: 1rem;
|
||||
height: calc(100vh - 180px);
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.envelope-action-bar {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -216,18 +216,6 @@ window.receiverSignature = (() => {
|
||||
overlayButtons.set(annotationId, { btn: wrapper, signed: isChecked });
|
||||
}
|
||||
|
||||
function debugDumpViewerDom() {
|
||||
const wrapper = document.querySelector(VIEWER_WRAPPER_SEL);
|
||||
if (!wrapper) { console.warn('[annot] .receiver-viewer-wrapper not found'); return; }
|
||||
console.group('[annot] viewer DOM snapshot');
|
||||
const cs = new Set();
|
||||
wrapper.querySelectorAll('*').forEach(el => el.classList.forEach(c => cs.add(c)));
|
||||
console.log('classes:', [...cs].sort().join(', '));
|
||||
console.log('scroll container:', document.querySelector(SCROLL_CONTAINER_SEL));
|
||||
console.log('page images:', document.querySelectorAll(PAGE_IMG_SEL));
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
// ?? Signature Pad ???????????????????????????????????????????????????????
|
||||
|
||||
function _pos(canvas, event) {
|
||||
@@ -332,7 +320,6 @@ window.receiverSignature = (() => {
|
||||
return {
|
||||
startTyped: startTyped,
|
||||
installAnnotationCheckboxes: installAnnotationCheckboxes,
|
||||
debugDumpViewerDom: debugDumpViewerDom,
|
||||
initialize: initialize,
|
||||
initializeTyped: initializeTyped,
|
||||
initializeImage: initializeImage,
|
||||
|
||||
Reference in New Issue
Block a user