feat: adjust app pdf view for easy signature confirm
This commit is contained in:
parent
5b71b294ac
commit
98798123ae
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
"yzhang.markdown-all-in-one"
|
"yzhang.markdown-all-in-one",
|
||||||
|
"alexkrechik.cucumberautocomplete"
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -10,6 +10,8 @@ class PdfState {
|
||||||
final int? signedPage;
|
final int? signedPage;
|
||||||
// Multiple signature placements per page, stored as UI-space rects (e.g., 400x560)
|
// Multiple signature placements per page, stored as UI-space rects (e.g., 400x560)
|
||||||
final Map<int, List<Rect>> placementsByPage;
|
final Map<int, List<Rect>> placementsByPage;
|
||||||
|
// UI state: selected placement index on the current page (if any)
|
||||||
|
final int? selectedPlacementIndex;
|
||||||
const PdfState({
|
const PdfState({
|
||||||
required this.loaded,
|
required this.loaded,
|
||||||
required this.pageCount,
|
required this.pageCount,
|
||||||
|
@ -18,6 +20,7 @@ class PdfState {
|
||||||
this.pickedPdfBytes,
|
this.pickedPdfBytes,
|
||||||
this.signedPage,
|
this.signedPage,
|
||||||
this.placementsByPage = const {},
|
this.placementsByPage = const {},
|
||||||
|
this.selectedPlacementIndex,
|
||||||
});
|
});
|
||||||
factory PdfState.initial() => const PdfState(
|
factory PdfState.initial() => const PdfState(
|
||||||
loaded: false,
|
loaded: false,
|
||||||
|
@ -26,6 +29,7 @@ class PdfState {
|
||||||
pickedPdfBytes: null,
|
pickedPdfBytes: null,
|
||||||
signedPage: null,
|
signedPage: null,
|
||||||
placementsByPage: {},
|
placementsByPage: {},
|
||||||
|
selectedPlacementIndex: null,
|
||||||
);
|
);
|
||||||
PdfState copyWith({
|
PdfState copyWith({
|
||||||
bool? loaded,
|
bool? loaded,
|
||||||
|
@ -35,6 +39,7 @@ class PdfState {
|
||||||
Uint8List? pickedPdfBytes,
|
Uint8List? pickedPdfBytes,
|
||||||
int? signedPage,
|
int? signedPage,
|
||||||
Map<int, List<Rect>>? placementsByPage,
|
Map<int, List<Rect>>? placementsByPage,
|
||||||
|
int? selectedPlacementIndex,
|
||||||
}) => PdfState(
|
}) => PdfState(
|
||||||
loaded: loaded ?? this.loaded,
|
loaded: loaded ?? this.loaded,
|
||||||
pageCount: pageCount ?? this.pageCount,
|
pageCount: pageCount ?? this.pageCount,
|
||||||
|
@ -43,6 +48,10 @@ class PdfState {
|
||||||
pickedPdfBytes: pickedPdfBytes ?? this.pickedPdfBytes,
|
pickedPdfBytes: pickedPdfBytes ?? this.pickedPdfBytes,
|
||||||
signedPage: signedPage ?? this.signedPage,
|
signedPage: signedPage ?? this.signedPage,
|
||||||
placementsByPage: placementsByPage ?? this.placementsByPage,
|
placementsByPage: placementsByPage ?? this.placementsByPage,
|
||||||
|
selectedPlacementIndex:
|
||||||
|
selectedPlacementIndex == null
|
||||||
|
? this.selectedPlacementIndex
|
||||||
|
: selectedPlacementIndex,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,6 +63,9 @@ class SignatureState {
|
||||||
final double brightness;
|
final double brightness;
|
||||||
final List<List<Offset>> strokes;
|
final List<List<Offset>> strokes;
|
||||||
final Uint8List? imageBytes;
|
final Uint8List? imageBytes;
|
||||||
|
// When true, the active signature overlay is movable/resizable and should not be exported.
|
||||||
|
// When false, the overlay is confirmed (unmovable) and eligible for export.
|
||||||
|
final bool editingEnabled;
|
||||||
const SignatureState({
|
const SignatureState({
|
||||||
required this.rect,
|
required this.rect,
|
||||||
required this.aspectLocked,
|
required this.aspectLocked,
|
||||||
|
@ -62,6 +74,7 @@ class SignatureState {
|
||||||
required this.brightness,
|
required this.brightness,
|
||||||
required this.strokes,
|
required this.strokes,
|
||||||
this.imageBytes,
|
this.imageBytes,
|
||||||
|
this.editingEnabled = false,
|
||||||
});
|
});
|
||||||
factory SignatureState.initial() => const SignatureState(
|
factory SignatureState.initial() => const SignatureState(
|
||||||
rect: null,
|
rect: null,
|
||||||
|
@ -71,6 +84,7 @@ class SignatureState {
|
||||||
brightness: 0.0,
|
brightness: 0.0,
|
||||||
strokes: [],
|
strokes: [],
|
||||||
imageBytes: null,
|
imageBytes: null,
|
||||||
|
editingEnabled: false,
|
||||||
);
|
);
|
||||||
SignatureState copyWith({
|
SignatureState copyWith({
|
||||||
Rect? rect,
|
Rect? rect,
|
||||||
|
@ -80,6 +94,7 @@ class SignatureState {
|
||||||
double? brightness,
|
double? brightness,
|
||||||
List<List<Offset>>? strokes,
|
List<List<Offset>>? strokes,
|
||||||
Uint8List? imageBytes,
|
Uint8List? imageBytes,
|
||||||
|
bool? editingEnabled,
|
||||||
}) => SignatureState(
|
}) => SignatureState(
|
||||||
rect: rect ?? this.rect,
|
rect: rect ?? this.rect,
|
||||||
aspectLocked: aspectLocked ?? this.aspectLocked,
|
aspectLocked: aspectLocked ?? this.aspectLocked,
|
||||||
|
@ -88,5 +103,6 @@ class SignatureState {
|
||||||
brightness: brightness ?? this.brightness,
|
brightness: brightness ?? this.brightness,
|
||||||
strokes: strokes ?? this.strokes,
|
strokes: strokes ?? this.strokes,
|
||||||
imageBytes: imageBytes ?? this.imageBytes,
|
imageBytes: imageBytes ?? this.imageBytes,
|
||||||
|
editingEnabled: editingEnabled ?? this.editingEnabled,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ class PdfController extends StateNotifier<PdfState> {
|
||||||
pickedPdfPath: null,
|
pickedPdfPath: null,
|
||||||
signedPage: null,
|
signedPage: null,
|
||||||
placementsByPage: {},
|
placementsByPage: {},
|
||||||
|
selectedPlacementIndex: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,23 +36,24 @@ class PdfController extends StateNotifier<PdfState> {
|
||||||
pickedPdfBytes: bytes,
|
pickedPdfBytes: bytes,
|
||||||
signedPage: null,
|
signedPage: null,
|
||||||
placementsByPage: {},
|
placementsByPage: {},
|
||||||
|
selectedPlacementIndex: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void jumpTo(int page) {
|
void jumpTo(int page) {
|
||||||
if (!state.loaded) return;
|
if (!state.loaded) return;
|
||||||
final clamped = page.clamp(1, state.pageCount);
|
final clamped = page.clamp(1, state.pageCount);
|
||||||
state = state.copyWith(currentPage: clamped);
|
state = state.copyWith(currentPage: clamped, selectedPlacementIndex: null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set or clear the page that will receive the signature overlay.
|
// Set or clear the page that will receive the signature overlay.
|
||||||
void setSignedPage(int? page) {
|
void setSignedPage(int? page) {
|
||||||
if (!state.loaded) return;
|
if (!state.loaded) return;
|
||||||
if (page == null) {
|
if (page == null) {
|
||||||
state = state.copyWith(signedPage: null);
|
state = state.copyWith(signedPage: null, selectedPlacementIndex: null);
|
||||||
} else {
|
} else {
|
||||||
final clamped = page.clamp(1, state.pageCount);
|
final clamped = page.clamp(1, state.pageCount);
|
||||||
state = state.copyWith(signedPage: clamped);
|
state = state.copyWith(signedPage: clamped, selectedPlacementIndex: null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,7 +70,7 @@ class PdfController extends StateNotifier<PdfState> {
|
||||||
final list = List<Rect>.from(map[p] ?? const []);
|
final list = List<Rect>.from(map[p] ?? const []);
|
||||||
list.add(rect);
|
list.add(rect);
|
||||||
map[p] = list;
|
map[p] = list;
|
||||||
state = state.copyWith(placementsByPage: map);
|
state = state.copyWith(placementsByPage: map, selectedPlacementIndex: null);
|
||||||
}
|
}
|
||||||
|
|
||||||
void removePlacement({required int page, required int index}) {
|
void removePlacement({required int page, required int index}) {
|
||||||
|
@ -83,13 +85,37 @@ class PdfController extends StateNotifier<PdfState> {
|
||||||
} else {
|
} else {
|
||||||
map[p] = list;
|
map[p] = list;
|
||||||
}
|
}
|
||||||
state = state.copyWith(placementsByPage: map);
|
state = state.copyWith(
|
||||||
|
placementsByPage: map,
|
||||||
|
selectedPlacementIndex: null,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Rect> placementsOn(int page) {
|
List<Rect> placementsOn(int page) {
|
||||||
return List<Rect>.from(state.placementsByPage[page] ?? const []);
|
return List<Rect>.from(state.placementsByPage[page] ?? const []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void selectPlacement(int? index) {
|
||||||
|
if (!state.loaded) return;
|
||||||
|
// Only allow valid index on current page; otherwise clear
|
||||||
|
if (index == null) {
|
||||||
|
state = state.copyWith(selectedPlacementIndex: null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final list = state.placementsByPage[state.currentPage] ?? const [];
|
||||||
|
if (index >= 0 && index < list.length) {
|
||||||
|
state = state.copyWith(selectedPlacementIndex: index);
|
||||||
|
} else {
|
||||||
|
state = state.copyWith(selectedPlacementIndex: null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void deleteSelectedPlacement() {
|
||||||
|
final idx = state.selectedPlacementIndex;
|
||||||
|
if (idx == null) return;
|
||||||
|
removePlacement(page: state.currentPage, index: idx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final pdfProvider = StateNotifierProvider<PdfController, PdfState>(
|
final pdfProvider = StateNotifierProvider<PdfController, PdfState>(
|
||||||
|
@ -112,6 +138,7 @@ class SignatureController extends StateNotifier<SignatureState> {
|
||||||
width: w,
|
width: w,
|
||||||
height: h,
|
height: h,
|
||||||
),
|
),
|
||||||
|
editingEnabled: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,6 +150,7 @@ class SignatureController extends StateNotifier<SignatureState> {
|
||||||
width: w,
|
width: w,
|
||||||
height: h,
|
height: h,
|
||||||
),
|
),
|
||||||
|
editingEnabled: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,13 +162,13 @@ class SignatureController extends StateNotifier<SignatureState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void drag(Offset delta) {
|
void drag(Offset delta) {
|
||||||
if (state.rect == null) return;
|
if (state.rect == null || !state.editingEnabled) return;
|
||||||
final moved = state.rect!.shift(delta);
|
final moved = state.rect!.shift(delta);
|
||||||
state = state.copyWith(rect: _clampRectToPage(moved));
|
state = state.copyWith(rect: _clampRectToPage(moved));
|
||||||
}
|
}
|
||||||
|
|
||||||
void resize(Offset delta) {
|
void resize(Offset delta) {
|
||||||
if (state.rect == null) return;
|
if (state.rect == null || !state.editingEnabled) return;
|
||||||
final r = state.rect!;
|
final r = state.rect!;
|
||||||
double newW = r.width + delta.dx;
|
double newW = r.width + delta.dx;
|
||||||
double newH = r.height + delta.dy;
|
double newH = r.height + delta.dy;
|
||||||
|
@ -210,6 +238,7 @@ class SignatureController extends StateNotifier<SignatureState> {
|
||||||
width: 140,
|
width: 140,
|
||||||
height: 70,
|
height: 70,
|
||||||
),
|
),
|
||||||
|
editingEnabled: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -218,6 +247,27 @@ class SignatureController extends StateNotifier<SignatureState> {
|
||||||
if (state.rect == null) {
|
if (state.rect == null) {
|
||||||
placeDefaultRect();
|
placeDefaultRect();
|
||||||
}
|
}
|
||||||
|
// Mark as draft/editable when user just loaded image
|
||||||
|
state = state.copyWith(editingEnabled: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm current signature: freeze editing and place it on the PDF as an immutable overlay.
|
||||||
|
// Returns the Rect placed, or null if no rect to confirm.
|
||||||
|
Rect? confirmCurrentSignature(WidgetRef ref) {
|
||||||
|
final r = state.rect;
|
||||||
|
if (r == null) return null;
|
||||||
|
// Place onto the current page
|
||||||
|
final pdf = ref.read(pdfProvider);
|
||||||
|
if (!pdf.loaded) return null;
|
||||||
|
ref.read(pdfProvider.notifier).addPlacement(page: pdf.currentPage, rect: r);
|
||||||
|
// Freeze editing: keep rect for preview but disable interaction
|
||||||
|
state = state.copyWith(editingEnabled: false);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the active overlay (draft or confirmed preview) but keep image settings intact
|
||||||
|
void clearActiveOverlay() {
|
||||||
|
state = state.copyWith(rect: null, editingEnabled: false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -70,14 +70,29 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _placeCurrentSignatureOnPage() {
|
void _createNewSignature() {
|
||||||
final pdf = ref.read(pdfProvider);
|
// Create a movable signature (draft) that won't be exported until confirmed
|
||||||
final sig = ref.read(signatureProvider);
|
final sig = ref.read(signatureProvider.notifier);
|
||||||
if (!pdf.loaded || sig.rect == null) return;
|
if (ref.read(pdfProvider).loaded) {
|
||||||
|
sig.placeDefaultRect();
|
||||||
ref
|
ref
|
||||||
.read(pdfProvider.notifier)
|
.read(pdfProvider.notifier)
|
||||||
.addPlacement(page: pdf.currentPage, rect: sig.rect!);
|
.setSignedPage(ref.read(pdfProvider).currentPage);
|
||||||
// Keep the active rect so the user can place multiple times if desired.
|
// Hint: how to confirm/delete via context menu
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'Long-press or right-click the signature to Confirm or Delete.',
|
||||||
|
),
|
||||||
|
duration: Duration(seconds: 3),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _confirmSignature() {
|
||||||
|
// Confirm: make current signature immutable and eligible for export by placing it
|
||||||
|
ref.read(signatureProvider.notifier).confirmCurrentSignature(ref);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onDragSignature(Offset delta) {
|
void _onDragSignature(Offset delta) {
|
||||||
|
@ -88,6 +103,41 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
ref.read(signatureProvider.notifier).resize(delta);
|
ref.read(signatureProvider.notifier).resize(delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onSelectPlaced(int? index) {
|
||||||
|
ref.read(pdfProvider.notifier).selectPlacement(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showContextMenuForPlaced({
|
||||||
|
required Offset globalPos,
|
||||||
|
required int index,
|
||||||
|
}) async {
|
||||||
|
// Opening the menu implicitly selects the item
|
||||||
|
_onSelectPlaced(index);
|
||||||
|
final choice = await showMenu<String>(
|
||||||
|
context: context,
|
||||||
|
position: RelativeRect.fromLTRB(
|
||||||
|
globalPos.dx,
|
||||||
|
globalPos.dy,
|
||||||
|
globalPos.dx,
|
||||||
|
globalPos.dy,
|
||||||
|
),
|
||||||
|
items: [
|
||||||
|
const PopupMenuItem<String>(
|
||||||
|
key: Key('ctx_delete_signature'),
|
||||||
|
value: 'delete',
|
||||||
|
child: Text('Delete'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
if (choice == null) return;
|
||||||
|
if (choice == 'delete') {
|
||||||
|
final currentPage = ref.read(pdfProvider).currentPage;
|
||||||
|
ref
|
||||||
|
.read(pdfProvider.notifier)
|
||||||
|
.removePlacement(page: currentPage, index: index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _openDrawCanvas() async {
|
Future<void> _openDrawCanvas() async {
|
||||||
final result = await showModalBottomSheet<Uint8List>(
|
final result = await showModalBottomSheet<Uint8List>(
|
||||||
context: context,
|
context: context,
|
||||||
|
@ -416,21 +466,17 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
onPressed: disabled || !pdf.loaded ? null : _loadSignatureFromFile,
|
onPressed: disabled || !pdf.loaded ? null : _loadSignatureFromFile,
|
||||||
child: Text(l.loadSignatureFromFile),
|
child: Text(l.loadSignatureFromFile),
|
||||||
),
|
),
|
||||||
|
OutlinedButton(
|
||||||
|
key: const Key('btn_create_signature'),
|
||||||
|
onPressed: disabled || !pdf.loaded ? null : _createNewSignature,
|
||||||
|
child: const Text('Create new signature'),
|
||||||
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
key: const Key('btn_draw_signature'),
|
key: const Key('btn_draw_signature'),
|
||||||
onPressed: disabled || !pdf.loaded ? null : _openDrawCanvas,
|
onPressed: disabled || !pdf.loaded ? null : _openDrawCanvas,
|
||||||
child: Text(l.drawSignature),
|
child: Text(l.drawSignature),
|
||||||
),
|
),
|
||||||
OutlinedButton(
|
// Confirm and Delete are available via context menus
|
||||||
key: const Key('btn_place_signature'),
|
|
||||||
onPressed:
|
|
||||||
disabled ||
|
|
||||||
!pdf.loaded ||
|
|
||||||
ref.read(signatureProvider).rect == null
|
|
||||||
? null
|
|
||||||
: _placeCurrentSignatureOnPage,
|
|
||||||
child: const Text('Place on page'),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -539,6 +585,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
SignatureState sig,
|
SignatureState sig,
|
||||||
Rect r, {
|
Rect r, {
|
||||||
bool interactive = true,
|
bool interactive = true,
|
||||||
|
int? placedIndex,
|
||||||
}) {
|
}) {
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
|
@ -558,6 +605,15 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
height: height,
|
height: height,
|
||||||
child: Builder(
|
child: Builder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
|
final selectedIdx =
|
||||||
|
ref.read(pdfProvider).selectedPlacementIndex;
|
||||||
|
final bool isPlaced = placedIndex != null;
|
||||||
|
final bool isSelected =
|
||||||
|
isPlaced && selectedIdx == placedIndex;
|
||||||
|
final Color borderColor =
|
||||||
|
isPlaced ? Colors.red : Colors.indigo;
|
||||||
|
final double borderWidth =
|
||||||
|
isPlaced ? (isSelected ? 3.0 : 2.0) : 2.0;
|
||||||
Widget content = DecoratedBox(
|
Widget content = DecoratedBox(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Color.fromRGBO(
|
color: Color.fromRGBO(
|
||||||
|
@ -566,7 +622,10 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
0,
|
0,
|
||||||
0.05 + math.min(0.25, (sig.contrast - 1.0).abs()),
|
0.05 + math.min(0.25, (sig.contrast - 1.0).abs()),
|
||||||
),
|
),
|
||||||
border: Border.all(color: Colors.indigo, width: 2),
|
border: Border.all(
|
||||||
|
color: borderColor,
|
||||||
|
width: borderWidth,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
|
@ -603,10 +662,11 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
child: const Icon(Icons.open_in_full, size: 20),
|
child: const Icon(Icons.open_in_full, size: 20),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// No inline buttons for placed overlays; use context menu instead
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (interactive) {
|
if (interactive && sig.editingEnabled) {
|
||||||
content = GestureDetector(
|
content = GestureDetector(
|
||||||
key: const Key('signature_overlay'),
|
key: const Key('signature_overlay'),
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
|
@ -615,6 +675,95 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
(d) => _onDragSignature(
|
(d) => _onDragSignature(
|
||||||
Offset(d.delta.dx / scaleX, d.delta.dy / scaleY),
|
Offset(d.delta.dx / scaleX, d.delta.dy / scaleY),
|
||||||
),
|
),
|
||||||
|
onSecondaryTapDown: (d) {
|
||||||
|
// Context menu for active signature: confirm or delete draft (clear)
|
||||||
|
final pos = d.globalPosition;
|
||||||
|
showMenu<String>(
|
||||||
|
context: context,
|
||||||
|
position: RelativeRect.fromLTRB(
|
||||||
|
pos.dx,
|
||||||
|
pos.dy,
|
||||||
|
pos.dx,
|
||||||
|
pos.dy,
|
||||||
|
),
|
||||||
|
items: [
|
||||||
|
const PopupMenuItem<String>(
|
||||||
|
key: Key('ctx_active_confirm'),
|
||||||
|
value: 'confirm',
|
||||||
|
child: Text('Confirm'),
|
||||||
|
),
|
||||||
|
const PopupMenuItem<String>(
|
||||||
|
key: Key('ctx_active_delete'),
|
||||||
|
value: 'delete',
|
||||||
|
child: Text('Delete'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).then((choice) {
|
||||||
|
if (choice == 'confirm') {
|
||||||
|
_confirmSignature();
|
||||||
|
} else if (choice == 'delete') {
|
||||||
|
ref
|
||||||
|
.read(signatureProvider.notifier)
|
||||||
|
.clearActiveOverlay();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onLongPressStart: (d) {
|
||||||
|
final pos = d.globalPosition;
|
||||||
|
showMenu<String>(
|
||||||
|
context: context,
|
||||||
|
position: RelativeRect.fromLTRB(
|
||||||
|
pos.dx,
|
||||||
|
pos.dy,
|
||||||
|
pos.dx,
|
||||||
|
pos.dy,
|
||||||
|
),
|
||||||
|
items: [
|
||||||
|
const PopupMenuItem<String>(
|
||||||
|
key: Key('ctx_active_confirm_lp'),
|
||||||
|
value: 'confirm',
|
||||||
|
child: Text('Confirm'),
|
||||||
|
),
|
||||||
|
const PopupMenuItem<String>(
|
||||||
|
key: Key('ctx_active_delete_lp'),
|
||||||
|
value: 'delete',
|
||||||
|
child: Text('Delete'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).then((choice) {
|
||||||
|
if (choice == 'confirm') {
|
||||||
|
_confirmSignature();
|
||||||
|
} else if (choice == 'delete') {
|
||||||
|
ref
|
||||||
|
.read(signatureProvider.notifier)
|
||||||
|
.clearActiveOverlay();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: content,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// For placed items: tap to select; long-press/right-click for context menu
|
||||||
|
content = GestureDetector(
|
||||||
|
key: Key('placed_signature_${placedIndex ?? 'x'}'),
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onTap: () => _onSelectPlaced(placedIndex),
|
||||||
|
onSecondaryTapDown: (d) {
|
||||||
|
if (placedIndex != null) {
|
||||||
|
_showContextMenuForPlaced(
|
||||||
|
globalPos: d.globalPosition,
|
||||||
|
index: placedIndex,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongPressStart: (d) {
|
||||||
|
if (placedIndex != null) {
|
||||||
|
_showContextMenuForPlaced(
|
||||||
|
globalPos: d.globalPosition,
|
||||||
|
index: placedIndex,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
child: content,
|
child: content,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -633,11 +782,15 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
final current = pdf.currentPage;
|
final current = pdf.currentPage;
|
||||||
final placed = pdf.placementsByPage[current] ?? const <Rect>[];
|
final placed = pdf.placementsByPage[current] ?? const <Rect>[];
|
||||||
final widgets = <Widget>[];
|
final widgets = <Widget>[];
|
||||||
for (final r in placed) {
|
for (int i = 0; i < placed.length; i++) {
|
||||||
widgets.add(_buildSignatureOverlay(sig, r, interactive: false));
|
final r = placed[i];
|
||||||
|
widgets.add(
|
||||||
|
_buildSignatureOverlay(sig, r, interactive: false, placedIndex: i),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// Show the active editing rect only on the selected (signed) page
|
// Show the active editing rect only on the selected (signed) page
|
||||||
if (sig.rect != null &&
|
if (sig.rect != null &&
|
||||||
|
sig.editingEnabled &&
|
||||||
(pdf.signedPage == null || pdf.signedPage == current)) {
|
(pdf.signedPage == null || pdf.signedPage == current)) {
|
||||||
widgets.add(_buildSignatureOverlay(sig, sig.rect!, interactive: true));
|
widgets.add(_buildSignatureOverlay(sig, sig.rect!, interactive: true));
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue