feat: partially implement multi-signature feature
This commit is contained in:
parent
a53e881d7b
commit
5b71b294ac
|
@ -38,3 +38,7 @@
|
||||||
* role: user
|
* role: user
|
||||||
* functionality: app provide localization support
|
* functionality: app provide localization support
|
||||||
* benefit: improve accessibility and usability for non-English speakers
|
* benefit: improve accessibility and usability for non-English speakers
|
||||||
|
* name: [support multiple signatures](../test/features/support_multiple_signatures.feature)
|
||||||
|
* role: user
|
||||||
|
* functionality: the ability to sign multiple locations within a PDF document
|
||||||
|
* benefit: documents requiring multiple signatures can be signed simultaneously
|
||||||
|
|
|
@ -8,6 +8,8 @@ class PdfState {
|
||||||
final String? pickedPdfPath;
|
final String? pickedPdfPath;
|
||||||
final Uint8List? pickedPdfBytes;
|
final Uint8List? pickedPdfBytes;
|
||||||
final int? signedPage;
|
final int? signedPage;
|
||||||
|
// Multiple signature placements per page, stored as UI-space rects (e.g., 400x560)
|
||||||
|
final Map<int, List<Rect>> placementsByPage;
|
||||||
const PdfState({
|
const PdfState({
|
||||||
required this.loaded,
|
required this.loaded,
|
||||||
required this.pageCount,
|
required this.pageCount,
|
||||||
|
@ -15,6 +17,7 @@ class PdfState {
|
||||||
this.pickedPdfPath,
|
this.pickedPdfPath,
|
||||||
this.pickedPdfBytes,
|
this.pickedPdfBytes,
|
||||||
this.signedPage,
|
this.signedPage,
|
||||||
|
this.placementsByPage = const {},
|
||||||
});
|
});
|
||||||
factory PdfState.initial() => const PdfState(
|
factory PdfState.initial() => const PdfState(
|
||||||
loaded: false,
|
loaded: false,
|
||||||
|
@ -22,6 +25,7 @@ class PdfState {
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pickedPdfBytes: null,
|
pickedPdfBytes: null,
|
||||||
signedPage: null,
|
signedPage: null,
|
||||||
|
placementsByPage: {},
|
||||||
);
|
);
|
||||||
PdfState copyWith({
|
PdfState copyWith({
|
||||||
bool? loaded,
|
bool? loaded,
|
||||||
|
@ -30,6 +34,7 @@ class PdfState {
|
||||||
String? pickedPdfPath,
|
String? pickedPdfPath,
|
||||||
Uint8List? pickedPdfBytes,
|
Uint8List? pickedPdfBytes,
|
||||||
int? signedPage,
|
int? signedPage,
|
||||||
|
Map<int, List<Rect>>? placementsByPage,
|
||||||
}) => PdfState(
|
}) => PdfState(
|
||||||
loaded: loaded ?? this.loaded,
|
loaded: loaded ?? this.loaded,
|
||||||
pageCount: pageCount ?? this.pageCount,
|
pageCount: pageCount ?? this.pageCount,
|
||||||
|
@ -37,6 +42,7 @@ class PdfState {
|
||||||
pickedPdfPath: pickedPdfPath ?? this.pickedPdfPath,
|
pickedPdfPath: pickedPdfPath ?? this.pickedPdfPath,
|
||||||
pickedPdfBytes: pickedPdfBytes ?? this.pickedPdfBytes,
|
pickedPdfBytes: pickedPdfBytes ?? this.pickedPdfBytes,
|
||||||
signedPage: signedPage ?? this.signedPage,
|
signedPage: signedPage ?? this.signedPage,
|
||||||
|
placementsByPage: placementsByPage ?? this.placementsByPage,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,7 @@ class ExportService {
|
||||||
required Rect? signatureRectUi,
|
required Rect? signatureRectUi,
|
||||||
required Size uiPageSize,
|
required Size uiPageSize,
|
||||||
required Uint8List? signatureImageBytes,
|
required Uint8List? signatureImageBytes,
|
||||||
|
Map<int, List<Rect>>? placementsByPage,
|
||||||
double targetDpi = 144.0,
|
double targetDpi = 144.0,
|
||||||
}) async {
|
}) async {
|
||||||
// print(
|
// print(
|
||||||
|
@ -51,6 +52,7 @@ class ExportService {
|
||||||
signatureRectUi: signatureRectUi,
|
signatureRectUi: signatureRectUi,
|
||||||
uiPageSize: uiPageSize,
|
uiPageSize: uiPageSize,
|
||||||
signatureImageBytes: signatureImageBytes,
|
signatureImageBytes: signatureImageBytes,
|
||||||
|
placementsByPage: placementsByPage,
|
||||||
targetDpi: targetDpi,
|
targetDpi: targetDpi,
|
||||||
);
|
);
|
||||||
if (bytes == null) return false;
|
if (bytes == null) return false;
|
||||||
|
@ -70,6 +72,7 @@ class ExportService {
|
||||||
required Rect? signatureRectUi,
|
required Rect? signatureRectUi,
|
||||||
required Size uiPageSize,
|
required Size uiPageSize,
|
||||||
required Uint8List? signatureImageBytes,
|
required Uint8List? signatureImageBytes,
|
||||||
|
Map<int, List<Rect>>? placementsByPage,
|
||||||
double targetDpi = 144.0,
|
double targetDpi = 144.0,
|
||||||
}) async {
|
}) async {
|
||||||
final out = pw.Document(version: pdf.PdfVersion.pdf_1_4, compress: false);
|
final out = pw.Document(version: pdf.PdfVersion.pdf_1_4, compress: false);
|
||||||
|
@ -91,13 +94,25 @@ class ExportService {
|
||||||
final bgImg = pw.MemoryImage(bgPng);
|
final bgImg = pw.MemoryImage(bgPng);
|
||||||
|
|
||||||
pw.MemoryImage? sigImgObj;
|
pw.MemoryImage? sigImgObj;
|
||||||
final shouldStamp =
|
final hasMulti =
|
||||||
|
(placementsByPage != null && placementsByPage.isNotEmpty);
|
||||||
|
final pagePlacements =
|
||||||
|
hasMulti
|
||||||
|
? (placementsByPage[pageIndex] ?? const <Rect>[])
|
||||||
|
: const <Rect>[];
|
||||||
|
final shouldStampSingle =
|
||||||
|
!hasMulti &&
|
||||||
signedPage != null &&
|
signedPage != null &&
|
||||||
pageIndex == signedPage &&
|
pageIndex == signedPage &&
|
||||||
signatureRectUi != null &&
|
signatureRectUi != null &&
|
||||||
signatureImageBytes != null &&
|
signatureImageBytes != null &&
|
||||||
signatureImageBytes.isNotEmpty;
|
signatureImageBytes.isNotEmpty;
|
||||||
if (shouldStamp) {
|
final shouldStampMulti =
|
||||||
|
hasMulti &&
|
||||||
|
pagePlacements.isNotEmpty &&
|
||||||
|
signatureImageBytes != null &&
|
||||||
|
signatureImageBytes.isNotEmpty;
|
||||||
|
if (shouldStampSingle || shouldStampMulti) {
|
||||||
try {
|
try {
|
||||||
sigImgObj = pw.MemoryImage(signatureImageBytes);
|
sigImgObj = pw.MemoryImage(signatureImageBytes);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
|
@ -125,7 +140,8 @@ class ExportService {
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
if (sigImgObj != null) {
|
if (sigImgObj != null) {
|
||||||
final r = signatureRectUi!;
|
if (hasMulti && pagePlacements.isNotEmpty) {
|
||||||
|
for (final r in pagePlacements) {
|
||||||
final left = r.left / uiPageSize.width * widthPts;
|
final left = r.left / uiPageSize.width * widthPts;
|
||||||
final top = r.top / uiPageSize.height * heightPts;
|
final top = r.top / uiPageSize.height * heightPts;
|
||||||
final w = r.width / uiPageSize.width * widthPts;
|
final w = r.width / uiPageSize.width * widthPts;
|
||||||
|
@ -138,6 +154,21 @@ class ExportService {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} else if (shouldStampSingle) {
|
||||||
|
final r = signatureRectUi;
|
||||||
|
final left = r.left / uiPageSize.width * widthPts;
|
||||||
|
final top = r.top / uiPageSize.height * heightPts;
|
||||||
|
final w = r.width / uiPageSize.width * widthPts;
|
||||||
|
final h = r.height / uiPageSize.height * heightPts;
|
||||||
|
children.add(
|
||||||
|
pw.Positioned(
|
||||||
|
left: left,
|
||||||
|
top: top,
|
||||||
|
child: pw.Image(sigImgObj, width: w, height: h),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
return pw.Stack(children: children);
|
return pw.Stack(children: children);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -152,13 +183,23 @@ class ExportService {
|
||||||
final widthPts = pdf.PdfPageFormat.a4.width;
|
final widthPts = pdf.PdfPageFormat.a4.width;
|
||||||
final heightPts = pdf.PdfPageFormat.a4.height;
|
final heightPts = pdf.PdfPageFormat.a4.height;
|
||||||
pw.MemoryImage? sigImgObj;
|
pw.MemoryImage? sigImgObj;
|
||||||
final shouldStamp =
|
final hasMulti =
|
||||||
|
(placementsByPage != null && placementsByPage.isNotEmpty);
|
||||||
|
final pagePlacements =
|
||||||
|
hasMulti ? (placementsByPage[1] ?? const <Rect>[]) : const <Rect>[];
|
||||||
|
final shouldStampSingle =
|
||||||
|
!hasMulti &&
|
||||||
signedPage != null &&
|
signedPage != null &&
|
||||||
signedPage == 1 &&
|
signedPage == 1 &&
|
||||||
signatureRectUi != null &&
|
signatureRectUi != null &&
|
||||||
signatureImageBytes != null &&
|
signatureImageBytes != null &&
|
||||||
signatureImageBytes.isNotEmpty;
|
signatureImageBytes.isNotEmpty;
|
||||||
if (shouldStamp) {
|
final shouldStampMulti =
|
||||||
|
hasMulti &&
|
||||||
|
pagePlacements.isNotEmpty &&
|
||||||
|
signatureImageBytes != null &&
|
||||||
|
signatureImageBytes.isNotEmpty;
|
||||||
|
if (shouldStampSingle || shouldStampMulti) {
|
||||||
try {
|
try {
|
||||||
// If it's already PNG, keep as-is to preserve alpha; otherwise decode/encode PNG
|
// If it's already PNG, keep as-is to preserve alpha; otherwise decode/encode PNG
|
||||||
final asStr = String.fromCharCodes(signatureImageBytes.take(8));
|
final asStr = String.fromCharCodes(signatureImageBytes.take(8));
|
||||||
|
@ -192,7 +233,8 @@ class ExportService {
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
if (sigImgObj != null) {
|
if (sigImgObj != null) {
|
||||||
final r = signatureRectUi!;
|
if (hasMulti && pagePlacements.isNotEmpty) {
|
||||||
|
for (final r in pagePlacements) {
|
||||||
final left = r.left / uiPageSize.width * widthPts;
|
final left = r.left / uiPageSize.width * widthPts;
|
||||||
final top = r.top / uiPageSize.height * heightPts;
|
final top = r.top / uiPageSize.height * heightPts;
|
||||||
final w = r.width / uiPageSize.width * widthPts;
|
final w = r.width / uiPageSize.width * widthPts;
|
||||||
|
@ -205,6 +247,21 @@ class ExportService {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} else if (shouldStampSingle) {
|
||||||
|
final r = signatureRectUi;
|
||||||
|
final left = r.left / uiPageSize.width * widthPts;
|
||||||
|
final top = r.top / uiPageSize.height * heightPts;
|
||||||
|
final w = r.width / uiPageSize.width * widthPts;
|
||||||
|
final h = r.height / uiPageSize.height * heightPts;
|
||||||
|
children.add(
|
||||||
|
pw.Positioned(
|
||||||
|
left: left,
|
||||||
|
top: top,
|
||||||
|
child: pw.Image(sigImgObj, width: w, height: h),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
return pw.Stack(children: children);
|
return pw.Stack(children: children);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
@ -18,6 +18,7 @@ class PdfController extends StateNotifier<PdfState> {
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pickedPdfPath: null,
|
pickedPdfPath: null,
|
||||||
signedPage: null,
|
signedPage: null,
|
||||||
|
placementsByPage: {},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,6 +34,7 @@ class PdfController extends StateNotifier<PdfState> {
|
||||||
pickedPdfPath: path,
|
pickedPdfPath: path,
|
||||||
pickedPdfBytes: bytes,
|
pickedPdfBytes: bytes,
|
||||||
signedPage: null,
|
signedPage: null,
|
||||||
|
placementsByPage: {},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,6 +59,37 @@ class PdfController extends StateNotifier<PdfState> {
|
||||||
if (!state.loaded) return;
|
if (!state.loaded) return;
|
||||||
state = state.copyWith(pageCount: count.clamp(1, 9999));
|
state = state.copyWith(pageCount: count.clamp(1, 9999));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Multiple-signature helpers
|
||||||
|
void addPlacement({required int page, required Rect rect}) {
|
||||||
|
if (!state.loaded) return;
|
||||||
|
final p = page.clamp(1, state.pageCount);
|
||||||
|
final map = Map<int, List<Rect>>.from(state.placementsByPage);
|
||||||
|
final list = List<Rect>.from(map[p] ?? const []);
|
||||||
|
list.add(rect);
|
||||||
|
map[p] = list;
|
||||||
|
state = state.copyWith(placementsByPage: map);
|
||||||
|
}
|
||||||
|
|
||||||
|
void removePlacement({required int page, required int index}) {
|
||||||
|
if (!state.loaded) return;
|
||||||
|
final p = page.clamp(1, state.pageCount);
|
||||||
|
final map = Map<int, List<Rect>>.from(state.placementsByPage);
|
||||||
|
final list = List<Rect>.from(map[p] ?? const []);
|
||||||
|
if (index >= 0 && index < list.length) {
|
||||||
|
list.removeAt(index);
|
||||||
|
if (list.isEmpty) {
|
||||||
|
map.remove(p);
|
||||||
|
} else {
|
||||||
|
map[p] = list;
|
||||||
|
}
|
||||||
|
state = state.copyWith(placementsByPage: map);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Rect> placementsOn(int page) {
|
||||||
|
return List<Rect>.from(state.placementsByPage[page] ?? const []);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final pdfProvider = StateNotifierProvider<PdfController, PdfState>(
|
final pdfProvider = StateNotifierProvider<PdfController, PdfState>(
|
||||||
|
|
|
@ -70,6 +70,16 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _placeCurrentSignatureOnPage() {
|
||||||
|
final pdf = ref.read(pdfProvider);
|
||||||
|
final sig = ref.read(signatureProvider);
|
||||||
|
if (!pdf.loaded || sig.rect == null) return;
|
||||||
|
ref
|
||||||
|
.read(pdfProvider.notifier)
|
||||||
|
.addPlacement(page: pdf.currentPage, rect: sig.rect!);
|
||||||
|
// Keep the active rect so the user can place multiple times if desired.
|
||||||
|
}
|
||||||
|
|
||||||
void _onDragSignature(Offset delta) {
|
void _onDragSignature(Offset delta) {
|
||||||
ref.read(signatureProvider.notifier).drag(delta);
|
ref.read(signatureProvider.notifier).drag(delta);
|
||||||
}
|
}
|
||||||
|
@ -130,6 +140,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
signatureRectUi: sig.rect,
|
signatureRectUi: sig.rect,
|
||||||
uiPageSize: SignatureController.pageSize,
|
uiPageSize: SignatureController.pageSize,
|
||||||
signatureImageBytes: processed ?? sig.imageBytes,
|
signatureImageBytes: processed ?? sig.imageBytes,
|
||||||
|
placementsByPage: pdf.placementsByPage,
|
||||||
targetDpi: targetDpi,
|
targetDpi: targetDpi,
|
||||||
);
|
);
|
||||||
if (bytes != null) {
|
if (bytes != null) {
|
||||||
|
@ -161,6 +172,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
signatureRectUi: sig.rect,
|
signatureRectUi: sig.rect,
|
||||||
uiPageSize: SignatureController.pageSize,
|
uiPageSize: SignatureController.pageSize,
|
||||||
signatureImageBytes: processed ?? sig.imageBytes,
|
signatureImageBytes: processed ?? sig.imageBytes,
|
||||||
|
placementsByPage: pdf.placementsByPage,
|
||||||
targetDpi: targetDpi,
|
targetDpi: targetDpi,
|
||||||
);
|
);
|
||||||
if (useMock) {
|
if (useMock) {
|
||||||
|
@ -187,6 +199,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
signatureRectUi: sig.rect,
|
signatureRectUi: sig.rect,
|
||||||
uiPageSize: SignatureController.pageSize,
|
uiPageSize: SignatureController.pageSize,
|
||||||
signatureImageBytes: processed ?? sig.imageBytes,
|
signatureImageBytes: processed ?? sig.imageBytes,
|
||||||
|
placementsByPage: pdf.placementsByPage,
|
||||||
targetDpi: targetDpi,
|
targetDpi: targetDpi,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -408,6 +421,16 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
onPressed: disabled || !pdf.loaded ? null : _openDrawCanvas,
|
onPressed: disabled || !pdf.loaded ? null : _openDrawCanvas,
|
||||||
child: Text(l.drawSignature),
|
child: Text(l.drawSignature),
|
||||||
),
|
),
|
||||||
|
OutlinedButton(
|
||||||
|
key: const Key('btn_place_signature'),
|
||||||
|
onPressed:
|
||||||
|
disabled ||
|
||||||
|
!pdf.loaded ||
|
||||||
|
ref.read(signatureProvider).rect == null
|
||||||
|
? null
|
||||||
|
: _placeCurrentSignatureOnPage,
|
||||||
|
child: const Text('Place on page'),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -428,7 +451,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
key: const Key('page_stack'),
|
key: const Key('page_stack'),
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
key: const Key('pdf_page'),
|
key: ValueKey('pdf_page_view_${pdf.currentPage}'),
|
||||||
color: Colors.grey.shade200,
|
color: Colors.grey.shade200,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
|
@ -446,8 +469,8 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
builder: (context, ref, _) {
|
builder: (context, ref, _) {
|
||||||
final sig = ref.watch(signatureProvider);
|
final sig = ref.watch(signatureProvider);
|
||||||
final visible = ref.watch(signatureVisibilityProvider);
|
final visible = ref.watch(signatureVisibilityProvider);
|
||||||
return sig.rect != null && visible
|
return visible
|
||||||
? _buildSignatureOverlay(sig)
|
? _buildPageOverlays(sig)
|
||||||
: const SizedBox.shrink();
|
: const SizedBox.shrink();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -486,6 +509,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
key: const Key('page_stack'),
|
key: const Key('page_stack'),
|
||||||
children: [
|
children: [
|
||||||
PdfPageView(
|
PdfPageView(
|
||||||
|
key: ValueKey('pdf_page_view_$pageNum'),
|
||||||
document: document,
|
document: document,
|
||||||
pageNumber: pageNum,
|
pageNumber: pageNum,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
|
@ -494,8 +518,8 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
builder: (context, ref, _) {
|
builder: (context, ref, _) {
|
||||||
final sig = ref.watch(signatureProvider);
|
final sig = ref.watch(signatureProvider);
|
||||||
final visible = ref.watch(signatureVisibilityProvider);
|
final visible = ref.watch(signatureVisibilityProvider);
|
||||||
return sig.rect != null && visible
|
return visible
|
||||||
? _buildSignatureOverlay(sig)
|
? _buildPageOverlays(sig)
|
||||||
: const SizedBox.shrink();
|
: const SizedBox.shrink();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -511,8 +535,11 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSignatureOverlay(SignatureState sig) {
|
Widget _buildSignatureOverlay(
|
||||||
final r = sig.rect!;
|
SignatureState sig,
|
||||||
|
Rect r, {
|
||||||
|
bool interactive = true,
|
||||||
|
}) {
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final scaleX = constraints.maxWidth / _pageSize.width;
|
final scaleX = constraints.maxWidth / _pageSize.width;
|
||||||
|
@ -529,15 +556,9 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
top: top,
|
top: top,
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
child: GestureDetector(
|
child: Builder(
|
||||||
key: const Key('signature_overlay'),
|
builder: (context) {
|
||||||
behavior: HitTestBehavior.opaque,
|
Widget content = DecoratedBox(
|
||||||
onPanStart: (_) {},
|
|
||||||
onPanUpdate:
|
|
||||||
(d) => _onDragSignature(
|
|
||||||
Offset(d.delta.dx / scaleX, d.delta.dy / scaleY),
|
|
||||||
),
|
|
||||||
child: DecoratedBox(
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Color.fromRGBO(
|
color: Color.fromRGBO(
|
||||||
0,
|
0,
|
||||||
|
@ -565,6 +586,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
return Image.memory(bytes, fit: BoxFit.contain);
|
return Image.memory(bytes, fit: BoxFit.contain);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
if (interactive)
|
||||||
Positioned(
|
Positioned(
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
|
@ -583,7 +605,21 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
if (interactive) {
|
||||||
|
content = GestureDetector(
|
||||||
|
key: const Key('signature_overlay'),
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onPanStart: (_) {},
|
||||||
|
onPanUpdate:
|
||||||
|
(d) => _onDragSignature(
|
||||||
|
Offset(d.delta.dx / scaleX, d.delta.dy / scaleY),
|
||||||
),
|
),
|
||||||
|
child: content,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return content;
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -592,6 +628,22 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildPageOverlays(SignatureState sig) {
|
||||||
|
final pdf = ref.watch(pdfProvider);
|
||||||
|
final current = pdf.currentPage;
|
||||||
|
final placed = pdf.placementsByPage[current] ?? const <Rect>[];
|
||||||
|
final widgets = <Widget>[];
|
||||||
|
for (final r in placed) {
|
||||||
|
widgets.add(_buildSignatureOverlay(sig, r, interactive: false));
|
||||||
|
}
|
||||||
|
// Show the active editing rect only on the selected (signed) page
|
||||||
|
if (sig.rect != null &&
|
||||||
|
(pdf.signedPage == null || pdf.signedPage == current)) {
|
||||||
|
widgets.add(_buildSignatureOverlay(sig, sig.rect!, interactive: true));
|
||||||
|
}
|
||||||
|
return Stack(children: widgets);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildAdjustmentsPanel(SignatureState sig) {
|
Widget _buildAdjustmentsPanel(SignatureState sig) {
|
||||||
return Column(
|
return Column(
|
||||||
key: const Key('adjustments_panel'),
|
key: const Key('adjustments_panel'),
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||||
|
import '_world.dart';
|
||||||
|
|
||||||
|
/// Usage: a PDF is open and contains multiple placed signatures across pages
|
||||||
|
Future<void> aPdfIsOpenAndContainsMultiplePlacedSignaturesAcrossPages(
|
||||||
|
WidgetTester tester,
|
||||||
|
) async {
|
||||||
|
final container = TestWorld.container ?? ProviderContainer();
|
||||||
|
TestWorld.container = container;
|
||||||
|
container
|
||||||
|
.read(pdfProvider.notifier)
|
||||||
|
.openPicked(path: 'mock.pdf', pageCount: 6);
|
||||||
|
// Ensure signature image exists
|
||||||
|
container
|
||||||
|
.read(signatureProvider.notifier)
|
||||||
|
.setImageBytes(Uint8List.fromList([1, 2, 3]));
|
||||||
|
// Place on two pages
|
||||||
|
container
|
||||||
|
.read(pdfProvider.notifier)
|
||||||
|
.addPlacement(page: 1, rect: const Rect.fromLTWH(10, 10, 80, 40));
|
||||||
|
container
|
||||||
|
.read(pdfProvider.notifier)
|
||||||
|
.addPlacement(page: 4, rect: const Rect.fromLTWH(120, 200, 100, 50));
|
||||||
|
// Keep backward compatibility with existing export step expectations
|
||||||
|
container.read(pdfProvider.notifier).setSignedPage(1);
|
||||||
|
container.read(signatureProvider.notifier).placeDefaultRect();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Usage: all placed signatures appear on their corresponding pages in the output
|
||||||
|
Future<void> allPlacedSignaturesAppearOnTheirCorrespondingPagesInTheOutput(
|
||||||
|
WidgetTester tester,
|
||||||
|
) async {
|
||||||
|
// In this logic-level test suite, we simply assert that placements exist
|
||||||
|
// on multiple pages and that a simulated export has bytes.
|
||||||
|
final container = TestWorld.container ?? ProviderContainer();
|
||||||
|
expect(container.read(pdfProvider.notifier).placementsOn(1), isNotEmpty);
|
||||||
|
expect(container.read(pdfProvider.notifier).placementsOn(4), isNotEmpty);
|
||||||
|
expect(TestWorld.lastExportBytes, isNotNull);
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||||
|
import '_world.dart';
|
||||||
|
|
||||||
|
/// Usage: a signature image is loaded or drawn
|
||||||
|
Future<void> aSignatureImageIsLoadedOrDrawn(WidgetTester tester) async {
|
||||||
|
final container = TestWorld.container ?? ProviderContainer();
|
||||||
|
TestWorld.container = container;
|
||||||
|
container
|
||||||
|
.read(signatureProvider.notifier)
|
||||||
|
.setImageBytes(Uint8List.fromList([1, 2, 3]));
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||||
|
import '_world.dart';
|
||||||
|
|
||||||
|
/// Usage: a signature is placed on page 2
|
||||||
|
Future<void> aSignatureIsPlacedOnPage2(WidgetTester tester) async {
|
||||||
|
final container = TestWorld.container ?? ProviderContainer();
|
||||||
|
TestWorld.container = container;
|
||||||
|
container
|
||||||
|
.read(pdfProvider.notifier)
|
||||||
|
.openPicked(path: 'mock.pdf', pageCount: 8);
|
||||||
|
container
|
||||||
|
.read(pdfProvider.notifier)
|
||||||
|
.addPlacement(page: 2, rect: const Rect.fromLTWH(50, 100, 80, 40));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Usage: the user navigates to page 5 and places another signature
|
||||||
|
Future<void> theUserNavigatesToPage5AndPlacesAnotherSignature(
|
||||||
|
WidgetTester tester,
|
||||||
|
) async {
|
||||||
|
final container = TestWorld.container ?? ProviderContainer();
|
||||||
|
container.read(pdfProvider.notifier).jumpTo(5);
|
||||||
|
container
|
||||||
|
.read(pdfProvider.notifier)
|
||||||
|
.addPlacement(page: 5, rect: const Rect.fromLTWH(60, 120, 80, 40));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Usage: the signature on page 2 remains
|
||||||
|
Future<void> theSignatureOnPage2Remains(WidgetTester tester) async {
|
||||||
|
final container = TestWorld.container ?? ProviderContainer();
|
||||||
|
expect(container.read(pdfProvider.notifier).placementsOn(2), isNotEmpty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Usage: the signature on page 5 is shown on page 5
|
||||||
|
Future<void> theSignatureOnPage5IsShownOnPage5(WidgetTester tester) async {
|
||||||
|
final container = TestWorld.container ?? ProviderContainer();
|
||||||
|
expect(container.read(pdfProvider.notifier).placementsOn(5), isNotEmpty);
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||||
|
import '_world.dart';
|
||||||
|
|
||||||
|
/// Usage: both signatures are shown on their respective pages
|
||||||
|
Future<void> bothSignaturesAreShownOnTheirRespectivePages(
|
||||||
|
WidgetTester tester,
|
||||||
|
) async {
|
||||||
|
final container = TestWorld.container ?? ProviderContainer();
|
||||||
|
final p1 = container.read(pdfProvider.notifier).placementsOn(1);
|
||||||
|
final p3 = container.read(pdfProvider.notifier).placementsOn(3);
|
||||||
|
expect(p1, isNotEmpty);
|
||||||
|
expect(p3, isNotEmpty);
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||||
|
import '_world.dart';
|
||||||
|
|
||||||
|
/// Usage: the user navigates to page 3 and places another signature
|
||||||
|
Future<void> theUserNavigatesToPage3AndPlacesAnotherSignature(
|
||||||
|
WidgetTester tester,
|
||||||
|
) async {
|
||||||
|
final container = TestWorld.container ?? ProviderContainer();
|
||||||
|
TestWorld.container = container;
|
||||||
|
container.read(pdfProvider.notifier).jumpTo(3);
|
||||||
|
container
|
||||||
|
.read(signatureProvider.notifier)
|
||||||
|
.setImageBytes(Uint8List.fromList([1, 2, 3]));
|
||||||
|
container.read(signatureProvider.notifier).placeDefaultRect();
|
||||||
|
final rect = container.read(signatureProvider).rect!;
|
||||||
|
container.read(pdfProvider.notifier).addPlacement(page: 3, rect: rect);
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||||
|
import '_world.dart';
|
||||||
|
|
||||||
|
/// Usage: the user places a signature on page 1
|
||||||
|
Future<void> theUserPlacesASignatureOnPage1(WidgetTester tester) async {
|
||||||
|
final container = TestWorld.container ?? ProviderContainer();
|
||||||
|
TestWorld.container = container;
|
||||||
|
// Ensure image exists so placement is meaningful
|
||||||
|
container
|
||||||
|
.read(signatureProvider.notifier)
|
||||||
|
.setImageBytes(Uint8List.fromList([1, 2, 3]));
|
||||||
|
// Place a default rect on page 1
|
||||||
|
container.read(signatureProvider.notifier).placeDefaultRect();
|
||||||
|
final rect = container.read(signatureProvider).rect!;
|
||||||
|
container.read(pdfProvider.notifier).addPlacement(page: 1, rect: rect);
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||||
|
import '_world.dart';
|
||||||
|
|
||||||
|
/// Usage: the user places it in multiple locations in the document
|
||||||
|
Future<void> theUserPlacesItInMultipleLocationsInTheDocument(
|
||||||
|
WidgetTester tester,
|
||||||
|
) async {
|
||||||
|
final container = TestWorld.container ?? ProviderContainer();
|
||||||
|
TestWorld.container = container;
|
||||||
|
final notifier = container.read(pdfProvider.notifier);
|
||||||
|
// Always open a fresh doc to avoid state bleed between scenarios
|
||||||
|
notifier.openPicked(path: 'mock.pdf', pageCount: 6);
|
||||||
|
// Place two on page 2 and one on page 4
|
||||||
|
notifier.addPlacement(page: 2, rect: const Rect.fromLTWH(10, 10, 80, 40));
|
||||||
|
notifier.addPlacement(page: 2, rect: const Rect.fromLTWH(120, 50, 80, 40));
|
||||||
|
notifier.addPlacement(page: 4, rect: const Rect.fromLTWH(20, 200, 100, 50));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Usage: identical signature instances appear in each location
|
||||||
|
Future<void> identicalSignatureInstancesAppearInEachLocation(
|
||||||
|
WidgetTester tester,
|
||||||
|
) async {
|
||||||
|
final container = TestWorld.container ?? ProviderContainer();
|
||||||
|
TestWorld.container = container;
|
||||||
|
final state = container.read(pdfProvider);
|
||||||
|
final p2 = state.placementsByPage[2] ?? const [];
|
||||||
|
final p4 = state.placementsByPage[4] ?? const [];
|
||||||
|
expect(p2.length, greaterThanOrEqualTo(2));
|
||||||
|
expect(p4.length, greaterThanOrEqualTo(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Usage: adjusting one instance does not affect the others
|
||||||
|
Future<void> adjustingOneInstanceDoesNotAffectTheOthers(
|
||||||
|
WidgetTester tester,
|
||||||
|
) async {
|
||||||
|
final container = TestWorld.container ?? ProviderContainer();
|
||||||
|
final before = container.read(pdfProvider.notifier).placementsOn(2);
|
||||||
|
expect(before.length, greaterThanOrEqualTo(2));
|
||||||
|
final modified = before[0].inflate(5);
|
||||||
|
container.read(pdfProvider.notifier).removePlacement(page: 2, index: 0);
|
||||||
|
container.read(pdfProvider.notifier).addPlacement(page: 2, rect: modified);
|
||||||
|
final after = container.read(pdfProvider.notifier).placementsOn(2);
|
||||||
|
expect(after.any((r) => r == before[1]), isTrue);
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||||
|
import '_world.dart';
|
||||||
|
|
||||||
|
/// Usage: the user places two signatures on the same page
|
||||||
|
Future<void> theUserPlacesTwoSignaturesOnTheSamePage(
|
||||||
|
WidgetTester tester,
|
||||||
|
) async {
|
||||||
|
final container = TestWorld.container ?? ProviderContainer();
|
||||||
|
TestWorld.container = container;
|
||||||
|
container
|
||||||
|
.read(signatureProvider.notifier)
|
||||||
|
.setImageBytes(Uint8List.fromList([1, 2, 3]));
|
||||||
|
// First
|
||||||
|
container.read(signatureProvider.notifier).placeDefaultRect();
|
||||||
|
final r1 = container.read(signatureProvider).rect!;
|
||||||
|
container.read(pdfProvider.notifier).addPlacement(page: 1, rect: r1);
|
||||||
|
// Second (offset a bit)
|
||||||
|
final r2 = r1.shift(const Offset(30, 30));
|
||||||
|
container.read(pdfProvider.notifier).addPlacement(page: 1, rect: r2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Usage: each signature can be dragged and resized independently
|
||||||
|
Future<void> eachSignatureCanBeDraggedAndResizedIndependently(
|
||||||
|
WidgetTester tester,
|
||||||
|
) async {
|
||||||
|
final container = TestWorld.container ?? ProviderContainer();
|
||||||
|
final list = container.read(pdfProvider.notifier).placementsOn(1);
|
||||||
|
expect(list.length, greaterThanOrEqualTo(2));
|
||||||
|
// Independence is modeled by distinct rects; ensure not equal and both within page
|
||||||
|
expect(list[0], isNot(equals(list[1])));
|
||||||
|
for (final r in list.take(2)) {
|
||||||
|
expect(r.left, greaterThanOrEqualTo(0));
|
||||||
|
expect(r.top, greaterThanOrEqualTo(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Usage: dragging or resizing one does not change the other
|
||||||
|
Future<void> draggingOrResizingOneDoesNotChangeTheOther(
|
||||||
|
WidgetTester tester,
|
||||||
|
) async {
|
||||||
|
final container = TestWorld.container ?? ProviderContainer();
|
||||||
|
final list = container.read(pdfProvider.notifier).placementsOn(1);
|
||||||
|
expect(list.length, greaterThanOrEqualTo(2));
|
||||||
|
final before = List<Rect>.from(list.take(2));
|
||||||
|
// Simulate changing the first only
|
||||||
|
final changed = before[0].shift(const Offset(5, 5));
|
||||||
|
container.read(pdfProvider.notifier).removePlacement(page: 1, index: 0);
|
||||||
|
container.read(pdfProvider.notifier).addPlacement(page: 1, rect: changed);
|
||||||
|
final after = container.read(pdfProvider.notifier).placementsOn(1);
|
||||||
|
expect(after[0], isNot(equals(before[0])));
|
||||||
|
// The other remains the same (order may differ after remove/add, check set containment)
|
||||||
|
expect(after.any((r) => r == before[1]), isTrue);
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||||
|
import '_world.dart';
|
||||||
|
|
||||||
|
/// Usage: three signatures are placed on the current page
|
||||||
|
Future<void> threeSignaturesArePlacedOnTheCurrentPage(
|
||||||
|
WidgetTester tester,
|
||||||
|
) async {
|
||||||
|
final container = TestWorld.container ?? ProviderContainer();
|
||||||
|
TestWorld.container = container;
|
||||||
|
container
|
||||||
|
.read(pdfProvider.notifier)
|
||||||
|
.openPicked(path: 'mock.pdf', pageCount: 5);
|
||||||
|
final n = container.read(pdfProvider.notifier);
|
||||||
|
n.addPlacement(page: 1, rect: const Rect.fromLTWH(10, 10, 80, 40));
|
||||||
|
n.addPlacement(page: 1, rect: const Rect.fromLTWH(100, 50, 80, 40));
|
||||||
|
n.addPlacement(page: 1, rect: const Rect.fromLTWH(200, 90, 80, 40));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Usage: the user deletes one selected signature
|
||||||
|
Future<void> theUserDeletesOneSelectedSignature(WidgetTester tester) async {
|
||||||
|
final container = TestWorld.container ?? ProviderContainer();
|
||||||
|
// Remove the middle one (index 1)
|
||||||
|
container.read(pdfProvider.notifier).removePlacement(page: 1, index: 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Usage: only the selected signature is removed
|
||||||
|
Future<void> onlyTheSelectedSignatureIsRemoved(WidgetTester tester) async {
|
||||||
|
final container = TestWorld.container ?? ProviderContainer();
|
||||||
|
final list = container.read(pdfProvider.notifier).placementsOn(1);
|
||||||
|
expect(list.length, 2);
|
||||||
|
expect(list[0].left, equals(10));
|
||||||
|
expect(list[1].left, equals(200));
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
Feature: support multiple signatures
|
||||||
|
|
||||||
|
Scenario: Place signatures on different pages
|
||||||
|
Given a multi-page PDF is open
|
||||||
|
When the user places a signature on page 1
|
||||||
|
And the user navigates to page 3 and places another signature
|
||||||
|
Then both signatures are shown on their respective pages
|
||||||
|
|
||||||
|
Scenario: Place multiple signatures on the same page independently
|
||||||
|
Given a PDF page is selected for signing
|
||||||
|
When the user places two signatures on the same page
|
||||||
|
Then each signature can be dragged and resized independently
|
||||||
|
And dragging or resizing one does not change the other
|
||||||
|
|
||||||
|
Scenario: Reuse the same signature asset in multiple locations
|
||||||
|
Given a signature image is loaded or drawn
|
||||||
|
When the user places it in multiple locations in the document
|
||||||
|
Then identical signature instances appear in each location
|
||||||
|
And adjusting one instance does not affect the others
|
||||||
|
|
||||||
|
Scenario: Remove one of many signatures
|
||||||
|
Given three signatures are placed on the current page
|
||||||
|
When the user deletes one selected signature
|
||||||
|
Then only the selected signature is removed
|
||||||
|
And the other signatures remain unchanged
|
||||||
|
|
||||||
|
Scenario: Keep earlier signatures while navigating between pages
|
||||||
|
Given a signature is placed on page 2
|
||||||
|
When the user navigates to page 5 and places another signature
|
||||||
|
Then the signature on page 2 remains
|
||||||
|
And the signature on page 5 is shown on page 5
|
||||||
|
|
||||||
|
Scenario: Save a document with multiple signatures across pages
|
||||||
|
Given a PDF is open and contains multiple placed signatures across pages
|
||||||
|
When the user saves/exports the document
|
||||||
|
Then all placed signatures appear on their corresponding pages in the output
|
||||||
|
And other page content remains unaltered
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart';
|
||||||
|
import 'package:pdf_signature/ui/features/pdf/view_model/view_model.dart';
|
||||||
|
import 'package:pdf_signature/data/model/model.dart';
|
||||||
|
import 'package:pdf_signature/data/services/providers.dart';
|
||||||
|
import 'package:pdf_signature/l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
class _TestPdfController extends PdfController {
|
||||||
|
_TestPdfController() : super() {
|
||||||
|
// Start with a loaded multi-page doc, page 1 of 5
|
||||||
|
state = PdfState.initial().copyWith(
|
||||||
|
loaded: true,
|
||||||
|
pageCount: 5,
|
||||||
|
currentPage: 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('PDF navigation: prev/next and goto update page label', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
ProviderScope(
|
||||||
|
overrides: [
|
||||||
|
useMockViewerProvider.overrideWithValue(true),
|
||||||
|
pdfProvider.overrideWith((ref) => _TestPdfController()),
|
||||||
|
],
|
||||||
|
child: MaterialApp(
|
||||||
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
|
locale: const Locale('en'),
|
||||||
|
home: const PdfSignatureHomePage(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initial label and page view key
|
||||||
|
expect(find.byKey(const Key('lbl_page_info')), findsOneWidget);
|
||||||
|
Text label() => tester.widget<Text>(find.byKey(const Key('lbl_page_info')));
|
||||||
|
expect(label().data, equals('Page 1/5'));
|
||||||
|
expect(find.byKey(const ValueKey('pdf_page_view_1')), findsOneWidget);
|
||||||
|
|
||||||
|
// Next
|
||||||
|
await tester.tap(find.byKey(const Key('btn_next')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(label().data, equals('Page 2/5'));
|
||||||
|
expect(find.byKey(const ValueKey('pdf_page_view_2')), findsOneWidget);
|
||||||
|
|
||||||
|
// Prev
|
||||||
|
await tester.tap(find.byKey(const Key('btn_prev')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(label().data, equals('Page 1/5'));
|
||||||
|
expect(find.byKey(const ValueKey('pdf_page_view_1')), findsOneWidget);
|
||||||
|
|
||||||
|
// Goto specific page
|
||||||
|
await tester.tap(find.byKey(const Key('txt_goto')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.enterText(find.byKey(const Key('txt_goto')), '4');
|
||||||
|
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(label().data, equals('Page 4/5'));
|
||||||
|
expect(find.byKey(const ValueKey('pdf_page_view_4')), findsOneWidget);
|
||||||
|
|
||||||
|
// Goto beyond upper bound -> clamp to 5
|
||||||
|
await tester.tap(find.byKey(const Key('txt_goto')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.enterText(find.byKey(const Key('txt_goto')), '999');
|
||||||
|
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(label().data, equals('Page 5/5'));
|
||||||
|
expect(find.byKey(const ValueKey('pdf_page_view_5')), findsOneWidget);
|
||||||
|
|
||||||
|
// Goto below 1 -> clamp to 1
|
||||||
|
await tester.tap(find.byKey(const Key('txt_goto')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.enterText(find.byKey(const Key('txt_goto')), '0');
|
||||||
|
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(label().data, equals('Page 1/5'));
|
||||||
|
expect(find.byKey(const ValueKey('pdf_page_view_1')), findsOneWidget);
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,2 +0,0 @@
|
||||||
// Split into multiple *_test.dart files. Intentionally left empty.
|
|
||||||
void main() {}
|
|
Loading…
Reference in New Issue