feat: implement signature drag-and-drop functionality and enhance PDF page overlays
This commit is contained in:
parent
2043bfc14c
commit
69d5a9a248
|
|
@ -396,75 +396,6 @@
|
|||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "P2kfltnFMgp1Hpns5eRsk",
|
||||
"type": "text",
|
||||
"x": 109.57327992864577,
|
||||
"y": 337.2651308292386,
|
||||
"width": 88.30944720085046,
|
||||
"height": 24.379859477817877,
|
||||
"angle": 0,
|
||||
"strokeColor": "#374151",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [
|
||||
"nQmqS53zA9IffPy8AAZwV"
|
||||
],
|
||||
"frameId": null,
|
||||
"index": "a9",
|
||||
"roundness": null,
|
||||
"seed": 1154314520,
|
||||
"version": 112,
|
||||
"versionNonce": 1095921782,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 1756647235527,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "Page view:",
|
||||
"fontSize": 18.059155168753982,
|
||||
"fontFamily": 6,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top",
|
||||
"containerId": null,
|
||||
"originalText": "Page view:",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.35
|
||||
},
|
||||
{
|
||||
"id": "vmM82c6vkYHi9E8_orBEx",
|
||||
"type": "rectangle",
|
||||
"x": 233.72997171382946,
|
||||
"y": 328.23555324486165,
|
||||
"width": 338.60915941413714,
|
||||
"height": 36.118310337507964,
|
||||
"angle": 0,
|
||||
"strokeColor": "#6b7280",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [
|
||||
"nQmqS53zA9IffPy8AAZwV"
|
||||
],
|
||||
"frameId": null,
|
||||
"index": "aA",
|
||||
"roundness": null,
|
||||
"seed": 288329240,
|
||||
"version": 110,
|
||||
"versionNonce": 128154090,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 1756647235527,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "Q0v5ejctIV2msui0iDFEg",
|
||||
"type": "rectangle",
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ Refs:
|
|||
## Welcome / First screen
|
||||
|
||||
Purpose: let the user open a PDF quickly via drag & drop or file picker.
|
||||
|
||||
Route: root
|
||||
|
||||
Design notes:
|
||||
|
|
@ -29,8 +30,8 @@ Purpose: provide basic configuration before/after opening a PDF.
|
|||
Route: root --> settings
|
||||
|
||||
Design notes:
|
||||
- Opened via "Configure" button in the top bar.
|
||||
- Modal with simple sections (e.g., General, Display).
|
||||
- Opened via "Configure" button in the right of top bar.
|
||||
- Model with simple sections (e.g., General, Display).
|
||||
- Primary action to save, secondary to cancel.
|
||||
|
||||
Illustration:
|
||||
|
|
@ -61,6 +62,7 @@ Design notes:
|
|||
- There is an empty card with "new signature" prompt and 2 buttons: "from file" and "draw".
|
||||
- "from file" opens a file picker to select an image as a signature card.
|
||||
- "draw" opens a simple drawing interface (draw canvas) to create a signature card.
|
||||
- There is a button at bottom to export PDF with placed signatures.
|
||||
- Interaction: drag a signature card from the right drawer onto the currently visible page to place it.
|
||||
|
||||
Signature controls (after placing on page):
|
||||
|
|
|
|||
|
|
@ -247,7 +247,7 @@ void main() {
|
|||
expect(container.read(pdfViewModelProvider).currentPage, 2);
|
||||
});
|
||||
|
||||
testWidgets('PDF View: scroll thumbnails to reveal and select last page', (
|
||||
testWidgets('PDF View: scroll thumb to reveal and select last page', (
|
||||
tester,
|
||||
) async {
|
||||
final pdfBytes =
|
||||
|
|
@ -308,6 +308,4 @@ void main() {
|
|||
await tester.pumpAndSettle();
|
||||
expect(container.read(pdfViewModelProvider).currentPage, 3);
|
||||
});
|
||||
|
||||
//TODO: Scroll Thumbs
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ class _PdfMockContinuousListState extends ConsumerState<PdfMockContinuousList> {
|
|||
return Container(
|
||||
color:
|
||||
candidateData.isNotEmpty
|
||||
? Colors.blue.withOpacity(0.3)
|
||||
? Colors.blue.withValues(alpha: 0.3)
|
||||
: Colors.grey.shade200,
|
||||
child: Center(
|
||||
child: Builder(
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import 'package:pdf_signature/data/repositories/document_repository.dart';
|
|||
|
||||
import '../../../../domain/models/model.dart';
|
||||
import 'signature_overlay.dart';
|
||||
import '../../signature/widgets/signature_drag_data.dart';
|
||||
import '../../signature/view_model/dragging_signature_view_model.dart';
|
||||
|
||||
/// Builds all overlays for a given page: placed signatures and the active one.
|
||||
class PdfPageOverlays extends ConsumerWidget {
|
||||
|
|
@ -37,6 +39,61 @@ class PdfPageOverlays extends ConsumerWidget {
|
|||
final activeRect = pdfViewModel.activeRect;
|
||||
final widgets = <Widget>[];
|
||||
|
||||
// Base DragTarget filling the whole page to accept drops from signature cards.
|
||||
widgets.add(
|
||||
// Use a Positioned.fill inside a LayoutBuilder to compute normalized coordinates.
|
||||
Positioned.fill(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isDragging = ref.watch(isDraggingSignatureViewModelProvider);
|
||||
// Only activate DragTarget hit tests while dragging to preserve wheel scrolling.
|
||||
final target = DragTarget<SignatureDragData>(
|
||||
onAcceptWithDetails: (details) {
|
||||
final box = context.findRenderObject() as RenderBox?;
|
||||
if (box == null) return;
|
||||
final local = box.globalToLocal(details.offset);
|
||||
final w = constraints.maxWidth;
|
||||
final h = constraints.maxHeight;
|
||||
if (w <= 0 || h <= 0) return;
|
||||
final nx = (local.dx / w).clamp(0.0, 1.0);
|
||||
final ny = (local.dy / h).clamp(0.0, 1.0);
|
||||
// Default size of the placed signature in normalized units
|
||||
const defW = 0.2;
|
||||
const defH = 0.1;
|
||||
final left = (nx - defW / 2).clamp(0.0, 1.0 - defW);
|
||||
final top = (ny - defH / 2).clamp(0.0, 1.0 - defH);
|
||||
final rect = Rect.fromLTWH(left, top, defW, defH);
|
||||
|
||||
final d = details.data;
|
||||
ref
|
||||
.read(pdfViewModelProvider.notifier)
|
||||
.addPlacement(
|
||||
page: pageNumber,
|
||||
rect: rect,
|
||||
asset: d.card?.asset,
|
||||
rotationDeg: d.card?.rotationDeg ?? 0.0,
|
||||
graphicAdjust: d.card?.graphicAdjust,
|
||||
);
|
||||
},
|
||||
builder: (context, candidateData, rejectedData) {
|
||||
// Visual hint when hovering a draggable over the page.
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
candidateData.isNotEmpty
|
||||
? Colors.blue.withValues(alpha: 0.12)
|
||||
: Colors.transparent,
|
||||
),
|
||||
child: const SizedBox.expand(),
|
||||
);
|
||||
},
|
||||
);
|
||||
return IgnorePointer(ignoring: !isDragging, child: target);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
for (int i = 0; i < placed.length; i++) {
|
||||
// Stored as UI-space rects (SignatureCardStateNotifier.pageSize).
|
||||
final p = placed[i];
|
||||
|
|
@ -47,6 +104,7 @@ class PdfPageOverlays extends ConsumerWidget {
|
|||
rect: uiRect,
|
||||
placement: p,
|
||||
placedIndex: i,
|
||||
pageNumber: pageNumber,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,15 +117,6 @@ class _PdfViewerWidgetState extends ConsumerState<PdfViewerWidget> {
|
|||
},
|
||||
viewerOverlayBuilder: (context, size, handle) {
|
||||
return [
|
||||
PdfPageOverlays(
|
||||
pageSize: widget.pageSize,
|
||||
pageNumber: pdfViewModel.currentPage,
|
||||
onDragSignature: widget.onDragSignature,
|
||||
onResizeSignature: widget.onResizeSignature,
|
||||
onConfirmSignature: widget.onConfirmSignature,
|
||||
onClearActiveOverlay: widget.onClearActiveOverlay,
|
||||
onSelectPlaced: widget.onSelectPlaced,
|
||||
),
|
||||
// Vertical scroll thumb on the right
|
||||
PdfViewerScrollThumb(
|
||||
controller: widget.controller,
|
||||
|
|
@ -166,6 +157,20 @@ class _PdfViewerWidgetState extends ConsumerState<PdfViewerWidget> {
|
|||
),
|
||||
];
|
||||
},
|
||||
// Per-page overlays to enable page-specific drag targets and placed signatures
|
||||
pageOverlaysBuilder: (context, pageRect, page) {
|
||||
return [
|
||||
PdfPageOverlays(
|
||||
pageSize: Size(pageRect.width, pageRect.height),
|
||||
pageNumber: page.pageNumber,
|
||||
onDragSignature: widget.onDragSignature,
|
||||
onResizeSignature: widget.onResizeSignature,
|
||||
onConfirmSignature: widget.onConfirmSignature,
|
||||
onClearActiveOverlay: widget.onClearActiveOverlay,
|
||||
onSelectPlaced: widget.onSelectPlaced,
|
||||
),
|
||||
];
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_box_transform/flutter_box_transform.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../../domain/models/model.dart';
|
||||
import '../../signature/widgets/rotated_signature_image.dart';
|
||||
import '../../signature/view_model/signature_view_model.dart';
|
||||
import '../view_model/pdf_view_model.dart';
|
||||
|
||||
/// Minimal overlay widget for rendering a placed signature.
|
||||
class SignatureOverlay extends ConsumerWidget {
|
||||
|
|
@ -12,12 +14,14 @@ class SignatureOverlay extends ConsumerWidget {
|
|||
required this.rect,
|
||||
required this.placement,
|
||||
required this.placedIndex,
|
||||
required this.pageNumber,
|
||||
});
|
||||
|
||||
final Size pageSize; // not used directly, kept for API symmetry
|
||||
final Rect rect; // normalized 0..1 values (left, top, width, height)
|
||||
final SignaturePlacement placement;
|
||||
final int placedIndex;
|
||||
final int pageNumber;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
|
|
@ -26,22 +30,52 @@ class SignatureOverlay extends ConsumerWidget {
|
|||
.getProcessedBytes(placement.asset, placement.graphicAdjust);
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final left = rect.left * constraints.maxWidth;
|
||||
final top = rect.top * constraints.maxHeight;
|
||||
final width = rect.width * constraints.maxWidth;
|
||||
final height = rect.height * constraints.maxHeight;
|
||||
final pageW = constraints.maxWidth;
|
||||
final pageH = constraints.maxHeight;
|
||||
final rectPx = Rect.fromLTWH(
|
||||
rect.left * pageW,
|
||||
rect.top * pageH,
|
||||
rect.width * pageW,
|
||||
rect.height * pageH,
|
||||
);
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
TransformableBox(
|
||||
key: Key('placed_signature_$placedIndex'),
|
||||
left: left,
|
||||
top: top,
|
||||
width: width,
|
||||
height: height,
|
||||
child: DecoratedBox(
|
||||
rect: rectPx,
|
||||
flip: Flip.none,
|
||||
// Keep the box within page bounds
|
||||
clampingRect: Rect.fromLTWH(0, 0, pageW, pageH),
|
||||
// Disable flips for signatures to avoid mirrored signatures
|
||||
allowFlippingWhileResizing: false,
|
||||
allowContentFlipping: false,
|
||||
onChanged: (result, details) {
|
||||
final r = result.rect;
|
||||
// Persist as normalized rect (0..1)
|
||||
final newRect = Rect.fromLTWH(
|
||||
(r.left / pageW).clamp(0.0, 1.0),
|
||||
(r.top / pageH).clamp(0.0, 1.0),
|
||||
(r.width / pageW).clamp(0.0, 1.0),
|
||||
(r.height / pageH).clamp(0.0, 1.0),
|
||||
);
|
||||
ref
|
||||
.read(pdfViewModelProvider.notifier)
|
||||
.updatePlacementRect(
|
||||
page: pageNumber,
|
||||
index: placedIndex,
|
||||
rect: newRect,
|
||||
);
|
||||
},
|
||||
// Keep default handles; you can customize later if needed
|
||||
contentBuilder:
|
||||
(context, boxRect, flip) => DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.red, width: 2),
|
||||
),
|
||||
child: SizedBox(
|
||||
width: boxRect.width,
|
||||
height: boxRect.height,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.contain,
|
||||
child: RotatedSignatureImage(
|
||||
|
|
@ -51,6 +85,7 @@ class SignatureOverlay extends ConsumerWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
/// Global flag indicating whether a signature card is currently being dragged.
|
||||
final isDraggingSignatureViewModelProvider = StateProvider<bool>(
|
||||
(ref) => false,
|
||||
);
|
||||
|
|
@ -5,6 +5,7 @@ import 'signature_drag_data.dart';
|
|||
import 'rotated_signature_image.dart';
|
||||
import 'package:pdf_signature/l10n/app_localizations.dart';
|
||||
import '../view_model/signature_view_model.dart';
|
||||
import '../view_model/dragging_signature_view_model.dart';
|
||||
|
||||
class SignatureCard extends ConsumerWidget {
|
||||
const SignatureCard({
|
||||
|
|
@ -163,6 +164,12 @@ class SignatureCard extends ConsumerWidget {
|
|||
graphicAdjust: graphicAdjust,
|
||||
),
|
||||
),
|
||||
onDragStarted: () {
|
||||
ref.read(isDraggingSignatureViewModelProvider.notifier).state = true;
|
||||
},
|
||||
onDragEnd: (_) {
|
||||
ref.read(isDraggingSignatureViewModelProvider.notifier).state = false;
|
||||
},
|
||||
feedback: Opacity(
|
||||
opacity: 0.9,
|
||||
child: ConstrainedBox(
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ dependencies:
|
|||
logging: ^1.3.0
|
||||
riverpod_annotation: ^2.6.1
|
||||
colorfilter_generator: ^0.0.8
|
||||
flutter_box_transform: ^0.4.7
|
||||
# ml_linalg: ^13.12.6
|
||||
|
||||
dev_dependencies:
|
||||
|
|
|
|||
Loading…
Reference in New Issue