Compare commits
4 Commits
9e0ae1dcfe
...
e806c3c830
| Author | SHA1 | Date |
|---|---|---|
|
|
e806c3c830 | |
|
|
9250d2ecc5 | |
|
|
f7d37517a4 | |
|
|
741decdae3 |
|
|
@ -2,7 +2,7 @@
|
||||||
<application
|
<application
|
||||||
android:label="pdf_signature"
|
android:label="pdf_signature"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/launcher_icon">
|
android:icon="@mipmap/ic_launcher">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 3.5 KiB |
|
|
@ -1,52 +1,44 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<svg
|
<!-- Cropped viewBox from 0 0 128 128 to 8 8 112 112 so icon artwork occupies full box -->
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="128px" height="128px" viewBox="8 8 112 112" version="1.1">
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
<defs>
|
||||||
viewBox="0 0 64 64"
|
<filter id="alpha" filterUnits="objectBoundingBox" x="0%" y="0%" width="100%" height="100%">
|
||||||
width="64"
|
<feColorMatrix type="matrix" in="SourceGraphic" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
|
||||||
height="64"
|
</filter>
|
||||||
role="img"
|
<mask id="mask0">
|
||||||
aria-labelledby="title desc"
|
<g filter="url(#alpha)">
|
||||||
>
|
<rect x="0" y="0" width="128" height="128" style="fill:rgb(0%,0%,0%);fill-opacity:0.0509804;stroke:none;"/>
|
||||||
<title id="title">PDF Signature</title>
|
</g>
|
||||||
<desc id="desc">An app icon showing a PDF page with a folded corner and a handwritten signature.</desc>
|
</mask>
|
||||||
|
<clipPath id="clip1">
|
||||||
<!-- Background tile -->
|
<rect x="0" y="0" width="128" height="128"/>
|
||||||
<rect x="4" y="4" width="56" height="56" rx="12" fill="#2563EB" />
|
</clipPath>
|
||||||
|
<g id="surface5" clip-path="url(#clip1)">
|
||||||
<!-- Paper with folded corner -->
|
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 40 32 L 76 32 L 96 52 L 96 54 L 88 54 C 79.199219 54 72 46.800781 72 38 L 72 32 L 40 32 C 37.800781 32 36 33.800781 36 36 C 36 33.800781 37.800781 32 40 32 Z M 40 32 "/>
|
||||||
<g>
|
</g>
|
||||||
<path
|
<mask id="mask1">
|
||||||
d="M20 16h18l10 10v22c0 2.2-1.8 4-4 4H20c-2.2 0-4-1.8-4-4V20c0-2.2 1.8-4 4-4z"
|
<g filter="url(#alpha)">
|
||||||
fill="#FFFFFF"
|
<rect x="0" y="0" width="128" height="128" style="fill:rgb(0%,0%,0%);fill-opacity:0.898039;stroke:none;"/>
|
||||||
/>
|
</g>
|
||||||
<path
|
</mask>
|
||||||
d="M38 16v8c0 3.3 2.7 6 6 6h8l-14-14z"
|
<clipPath id="clip2">
|
||||||
fill="#F3F4F6"
|
<rect x="0" y="0" width="128" height="128"/>
|
||||||
/>
|
</clipPath>
|
||||||
|
<g id="surface8" clip-path="url(#clip2)">
|
||||||
|
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(89.803922%,22.352941%,20.784314%);fill-opacity:1;" d="M 50 56 C 50 54.894531 49.105469 54 48 54 C 46.894531 54 46 54.894531 46 56 C 46 57.105469 46.894531 58 48 58 C 49.105469 58 50 57.105469 50 56 Z M 50 56 "/>
|
||||||
|
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(89.803922%,22.352941%,20.784314%);fill-opacity:1;" d="M 58 56 C 58 54.894531 57.105469 54 56 54 C 54.894531 54 54 54.894531 54 56 C 54 57.105469 54.894531 58 56 58 C 57.105469 58 58 57.105469 58 56 Z M 58 56 "/>
|
||||||
|
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(89.803922%,22.352941%,20.784314%);fill-opacity:1;" d="M 66 56 C 66 54.894531 65.105469 54 64 54 C 62.894531 54 62 54.894531 62 56 C 62 57.105469 62.894531 58 64 58 C 65.105469 58 66 57.105469 66 56 Z M 66 56 "/>
|
||||||
|
</g>
|
||||||
|
</defs>
|
||||||
|
<g id="surface1">
|
||||||
|
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(14.509805%,38.82353%,92.156863%);fill-opacity:1;" d="M 32 8 L 96 8 C 109.253906 8 120 18.746094 120 32 L 120 96 C 120 109.253906 109.253906 120 96 120 L 32 120 C 18.746094 120 8 109.253906 8 96 L 8 32 C 8 18.746094 18.746094 8 32 8 Z M 32 8 "/>
|
||||||
|
<!-- Slight enlargement (1.05x) of document artwork -->
|
||||||
|
<g id="doc" transform="matrix(1.05 0 0 1.05 -4.8 -3.4)">
|
||||||
|
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 40 32 L 76 32 L 96 52 L 96 96 C 96 100.398438 92.398438 104 88 104 L 40 104 C 35.601562 104 32 100.398438 32 96 L 32 40 C 32 35.601562 35.601562 32 40 32 Z M 40 32 "/>
|
||||||
|
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(95.294118%,95.686275%,96.470588%);fill-opacity:1;" d="M 76 32 L 76 48 C 76 54.601562 81.398438 60 88 60 L 104 60 Z M 76 32 "/>
|
||||||
|
<path style="fill:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round;stroke:rgb(12.156863%,16.078432%,21.568628%);stroke-opacity:1;stroke-miterlimit:4;" d="M 18 42 C 21 40 24 39.800781 26.5 42 C 29 44.199219 31.300781 43.800781 34.699219 40.800781 C 38.099609 37.800781 41.599609 35.5 44.099609 38 C 45.300781 39.199219 44.599609 41.199219 42.900391 41.599609 C 39.400391 42.5 46.199219 34.800781 49.300781 37 C 51.300781 38.400391 47.800781 43.699219 44.5 44.800781 C 39.900391 46.400391 33.599609 44.199219 30.699219 44.199219 C 26.300781 44.199219 23.199219 46.599609 18.699219 47 " transform="matrix(2,0,0,2,0,0)"/>
|
||||||
|
<use xlink:href="#surface5" mask="url(#mask0)"/>
|
||||||
|
<use xlink:href="#surface8" mask="url(#mask1)"/>
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
<!-- Signature stroke -->
|
|
||||||
<path
|
|
||||||
d="M18 42c3-2 6-2.2 8.5 0 2.5 2.2 4.8 1.8 8.2-1.2 3.4-3 6.9-5.3 9.4-2.8 1.2 1.2 0.5 3.2-1.2 3.6-3.5 0.9 3.3-6.8 6.4-4.6 2 1.4-1.5 6.7-4.8 7.8-4.6 1.6-10.9-0.6-13.8-0.6-4.4 0-7.5 2.4-12 2.8"
|
|
||||||
fill="none"
|
|
||||||
stroke="#1F2937"
|
|
||||||
stroke-width="2.5"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Subtle page shadow for depth (kept minimal for clarity) -->
|
|
||||||
<path
|
|
||||||
d="M20 16h18l10 10v1H44c-4.4 0-8-3.6-8-8v-3H20c-1.1 0-2 .9-2 2v0c0-1.1.9-2 2-2z"
|
|
||||||
fill="#000"
|
|
||||||
opacity=".05"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Optional PDF label dots (very subtle) -->
|
|
||||||
<g fill="#E53935" opacity=".9">
|
|
||||||
<circle cx="24" cy="28" r="1" />
|
|
||||||
<circle cx="28" cy="28" r="1" />
|
|
||||||
<circle cx="32" cy="28" r="1" />
|
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 3.7 KiB |
|
|
@ -37,9 +37,12 @@ void main() {
|
||||||
(ref) => PreferencesStateNotifier(prefs),
|
(ref) => PreferencesStateNotifier(prefs),
|
||||||
),
|
),
|
||||||
documentRepositoryProvider.overrideWith(
|
documentRepositoryProvider.overrideWith(
|
||||||
(ref) =>
|
(ref) => DocumentStateNotifier(service: ExportService())
|
||||||
DocumentStateNotifier(service: ExportService())
|
..openDocument(
|
||||||
..openPicked(pageCount: 3, bytes: pdfBytes),
|
bytes: pdfBytes,
|
||||||
|
pageCount: 3,
|
||||||
|
knownPageCount: true,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
pdfViewModelProvider.overrideWith(
|
pdfViewModelProvider.overrideWith(
|
||||||
(ref) => PdfViewModel(ref, useMockViewer: false),
|
(ref) => PdfViewModel(ref, useMockViewer: false),
|
||||||
|
|
@ -93,9 +96,12 @@ void main() {
|
||||||
(ref) => PreferencesStateNotifier(prefs),
|
(ref) => PreferencesStateNotifier(prefs),
|
||||||
),
|
),
|
||||||
documentRepositoryProvider.overrideWith(
|
documentRepositoryProvider.overrideWith(
|
||||||
(ref) =>
|
(ref) => DocumentStateNotifier(service: ExportService())
|
||||||
DocumentStateNotifier(service: ExportService())
|
..openDocument(
|
||||||
..openPicked(pageCount: 3, bytes: pdfBytes),
|
bytes: pdfBytes,
|
||||||
|
pageCount: 3,
|
||||||
|
knownPageCount: true,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
pdfViewModelProvider.overrideWith(
|
pdfViewModelProvider.overrideWith(
|
||||||
(ref) => PdfViewModel(ref, useMockViewer: false),
|
(ref) => PdfViewModel(ref, useMockViewer: false),
|
||||||
|
|
@ -164,7 +170,11 @@ void main() {
|
||||||
documentRepositoryProvider.overrideWith(
|
documentRepositoryProvider.overrideWith(
|
||||||
(ref) => DocumentStateNotifier(
|
(ref) => DocumentStateNotifier(
|
||||||
service: ExportService(enableRaster: false),
|
service: ExportService(enableRaster: false),
|
||||||
)..openPicked(pageCount: 3, bytes: pdfBytes),
|
)..openDocument(
|
||||||
|
bytes: pdfBytes,
|
||||||
|
pageCount: 3,
|
||||||
|
knownPageCount: true,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
pdfViewModelProvider.overrideWith(
|
pdfViewModelProvider.overrideWith(
|
||||||
(ref) => PdfViewModel(ref, useMockViewer: false),
|
(ref) => PdfViewModel(ref, useMockViewer: false),
|
||||||
|
|
@ -210,7 +220,11 @@ void main() {
|
||||||
documentRepositoryProvider.overrideWith(
|
documentRepositoryProvider.overrideWith(
|
||||||
(ref) => DocumentStateNotifier(
|
(ref) => DocumentStateNotifier(
|
||||||
service: ExportService(enableRaster: false),
|
service: ExportService(enableRaster: false),
|
||||||
)..openPicked(pageCount: 3, bytes: pdfBytes),
|
)..openDocument(
|
||||||
|
bytes: pdfBytes,
|
||||||
|
pageCount: 3,
|
||||||
|
knownPageCount: true,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
pdfViewModelProvider.overrideWith(
|
pdfViewModelProvider.overrideWith(
|
||||||
(ref) => PdfViewModel(ref, useMockViewer: false),
|
(ref) => PdfViewModel(ref, useMockViewer: false),
|
||||||
|
|
@ -259,7 +273,11 @@ void main() {
|
||||||
documentRepositoryProvider.overrideWith(
|
documentRepositoryProvider.overrideWith(
|
||||||
(ref) => DocumentStateNotifier(
|
(ref) => DocumentStateNotifier(
|
||||||
service: ExportService(enableRaster: false),
|
service: ExportService(enableRaster: false),
|
||||||
)..openPicked(pageCount: 3, bytes: pdfBytes),
|
)..openDocument(
|
||||||
|
bytes: pdfBytes,
|
||||||
|
pageCount: 3,
|
||||||
|
knownPageCount: true,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
pdfViewModelProvider.overrideWith(
|
pdfViewModelProvider.overrideWith(
|
||||||
(ref) => PdfViewModel(ref, useMockViewer: false),
|
(ref) => PdfViewModel(ref, useMockViewer: false),
|
||||||
|
|
@ -309,9 +327,12 @@ void main() {
|
||||||
(ref) => PreferencesStateNotifier(prefs),
|
(ref) => PreferencesStateNotifier(prefs),
|
||||||
),
|
),
|
||||||
documentRepositoryProvider.overrideWith(
|
documentRepositoryProvider.overrideWith(
|
||||||
(ref) =>
|
(ref) => DocumentStateNotifier(service: ExportService())
|
||||||
DocumentStateNotifier(service: ExportService())
|
..openDocument(
|
||||||
..openPicked(pageCount: 3, bytes: pdfBytes),
|
bytes: pdfBytes,
|
||||||
|
pageCount: 3,
|
||||||
|
knownPageCount: true,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
pdfViewModelProvider.overrideWith(
|
pdfViewModelProvider.overrideWith(
|
||||||
(ref) => PdfViewModel(ref, useMockViewer: false),
|
(ref) => PdfViewModel(ref, useMockViewer: false),
|
||||||
|
|
@ -361,9 +382,12 @@ void main() {
|
||||||
(ref) => PreferencesStateNotifier(prefs),
|
(ref) => PreferencesStateNotifier(prefs),
|
||||||
),
|
),
|
||||||
documentRepositoryProvider.overrideWith(
|
documentRepositoryProvider.overrideWith(
|
||||||
(ref) =>
|
(ref) => DocumentStateNotifier(service: ExportService())
|
||||||
DocumentStateNotifier(service: ExportService())
|
..openDocument(
|
||||||
..openPicked(pageCount: 3, bytes: pdfBytes),
|
bytes: pdfBytes,
|
||||||
|
pageCount: 3,
|
||||||
|
knownPageCount: true,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
pdfViewModelProvider.overrideWith(
|
pdfViewModelProvider.overrideWith(
|
||||||
(ref) => PdfViewModel(ref, useMockViewer: false),
|
(ref) => PdfViewModel(ref, useMockViewer: false),
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,11 @@ void main() {
|
||||||
),
|
),
|
||||||
documentRepositoryProvider.overrideWith(
|
documentRepositoryProvider.overrideWith(
|
||||||
(ref) =>
|
(ref) =>
|
||||||
DocumentStateNotifier()
|
DocumentStateNotifier()..openDocument(
|
||||||
..openPicked(pageCount: 3, bytes: pdfBytes),
|
bytes: pdfBytes,
|
||||||
|
pageCount: 3,
|
||||||
|
knownPageCount: true,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
pdfViewModelProvider.overrideWith(
|
pdfViewModelProvider.overrideWith(
|
||||||
(ref) => PdfViewModel(ref, useMockViewer: false),
|
(ref) => PdfViewModel(ref, useMockViewer: false),
|
||||||
|
|
@ -62,14 +65,31 @@ void main() {
|
||||||
final vm = container.read(pdfViewModelProvider);
|
final vm = container.read(pdfViewModelProvider);
|
||||||
expect(vm.currentPage, 1);
|
expect(vm.currentPage, 1);
|
||||||
|
|
||||||
container.read(pdfViewModelProvider.notifier).jumpToPage(2);
|
final controller = container.read(pdfViewModelProvider).controller;
|
||||||
await tester.pumpAndSettle();
|
// Wait until the underlying viewer controller reports ready.
|
||||||
await tester.pump(const Duration(milliseconds: 120));
|
final readyStart = DateTime.now();
|
||||||
expect(container.read(pdfViewModelProvider).currentPage, 2);
|
while (!controller.isReady) {
|
||||||
|
await tester.pump(const Duration(milliseconds: 40));
|
||||||
|
if (DateTime.now().difference(readyStart) > const Duration(seconds: 5)) {
|
||||||
|
fail('PdfViewerController never became ready');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Future<void> goAndAwait(int target) async {
|
||||||
|
controller.goToPage(pageNumber: target);
|
||||||
|
final start = DateTime.now();
|
||||||
|
while (container.read(pdfViewModelProvider).currentPage != target) {
|
||||||
|
await tester.pump(const Duration(milliseconds: 40));
|
||||||
|
if (DateTime.now().difference(start) > const Duration(seconds: 3)) {
|
||||||
|
fail(
|
||||||
|
'Timeout waiting to reach page $target (current=${container.read(pdfViewModelProvider).currentPage})',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
container.read(pdfViewModelProvider.notifier).jumpToPage(3);
|
await goAndAwait(2);
|
||||||
await tester.pumpAndSettle();
|
expect(container.read(pdfViewModelProvider).currentPage, 2);
|
||||||
await tester.pump(const Duration(milliseconds: 120));
|
await goAndAwait(3);
|
||||||
expect(container.read(pdfViewModelProvider).currentPage, 3);
|
expect(container.read(pdfViewModelProvider).currentPage, 3);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -87,8 +107,11 @@ void main() {
|
||||||
),
|
),
|
||||||
documentRepositoryProvider.overrideWith(
|
documentRepositoryProvider.overrideWith(
|
||||||
(ref) =>
|
(ref) =>
|
||||||
DocumentStateNotifier()
|
DocumentStateNotifier()..openDocument(
|
||||||
..openPicked(pageCount: 3, bytes: pdfBytes),
|
bytes: pdfBytes,
|
||||||
|
pageCount: 3,
|
||||||
|
knownPageCount: true,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
pdfViewModelProvider.overrideWith(
|
pdfViewModelProvider.overrideWith(
|
||||||
(ref) => PdfViewModel(ref, useMockViewer: false),
|
(ref) => PdfViewModel(ref, useMockViewer: false),
|
||||||
|
|
@ -141,8 +164,11 @@ void main() {
|
||||||
),
|
),
|
||||||
documentRepositoryProvider.overrideWith(
|
documentRepositoryProvider.overrideWith(
|
||||||
(ref) =>
|
(ref) =>
|
||||||
DocumentStateNotifier()
|
DocumentStateNotifier()..openDocument(
|
||||||
..openPicked(pageCount: 3, bytes: pdfBytes),
|
bytes: pdfBytes,
|
||||||
|
pageCount: 3,
|
||||||
|
knownPageCount: true,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
pdfViewModelProvider.overrideWith(
|
pdfViewModelProvider.overrideWith(
|
||||||
(ref) => PdfViewModel(ref, useMockViewer: false),
|
(ref) => PdfViewModel(ref, useMockViewer: false),
|
||||||
|
|
@ -228,8 +254,11 @@ void main() {
|
||||||
),
|
),
|
||||||
documentRepositoryProvider.overrideWith(
|
documentRepositoryProvider.overrideWith(
|
||||||
(ref) =>
|
(ref) =>
|
||||||
DocumentStateNotifier()
|
DocumentStateNotifier()..openDocument(
|
||||||
..openPicked(pageCount: 3, bytes: pdfBytes),
|
bytes: pdfBytes,
|
||||||
|
pageCount: 3,
|
||||||
|
knownPageCount: true,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
pdfViewModelProvider.overrideWith(
|
pdfViewModelProvider.overrideWith(
|
||||||
(ref) => PdfViewModel(ref, useMockViewer: false),
|
(ref) => PdfViewModel(ref, useMockViewer: false),
|
||||||
|
|
@ -294,8 +323,11 @@ void main() {
|
||||||
),
|
),
|
||||||
documentRepositoryProvider.overrideWith(
|
documentRepositoryProvider.overrideWith(
|
||||||
(ref) =>
|
(ref) =>
|
||||||
DocumentStateNotifier()
|
DocumentStateNotifier()..openDocument(
|
||||||
..openPicked(pageCount: 3, bytes: pdfBytes),
|
bytes: pdfBytes,
|
||||||
|
pageCount: 3,
|
||||||
|
knownPageCount: true,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
pdfViewModelProvider.overrideWith(
|
pdfViewModelProvider.overrideWith(
|
||||||
(ref) => PdfViewModel(ref, useMockViewer: false),
|
(ref) => PdfViewModel(ref, useMockViewer: false),
|
||||||
|
|
@ -339,4 +371,116 @@ void main() {
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(container.read(pdfViewModelProvider).currentPage, 3);
|
expect(container.read(pdfViewModelProvider).currentPage, 3);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('PDF View: reopen another PDF via toolbar picker updates viewer', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final initialBytes =
|
||||||
|
await File('integration_test/data/sample-local-pdf.pdf').readAsBytes();
|
||||||
|
// 3 pages
|
||||||
|
final newBytes =
|
||||||
|
await File(
|
||||||
|
'integration_test/data/PPFZ-Local-Purchase-Form.pdf',
|
||||||
|
).readAsBytes();
|
||||||
|
// 10 pages
|
||||||
|
SharedPreferences.setMockInitialValues({});
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
|
// We'll override onPickPdf to simulate opening a new file with a different page count
|
||||||
|
// TODO: Replace PPFZ-Local-Purchase-Form.pdf with a 10-page PDF to test page count change
|
||||||
|
late ProviderContainer container; // capture to use inside callback
|
||||||
|
Future<void> simulatePick() async {
|
||||||
|
container
|
||||||
|
.read(documentRepositoryProvider.notifier)
|
||||||
|
.openDocument(bytes: newBytes);
|
||||||
|
// Reset the current page explicitly to 1 as openPicked establishes new doc
|
||||||
|
container.read(pdfViewModelProvider.notifier).jumpToPage(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
int? lastDocPageCount; // capture page count from callback
|
||||||
|
await tester.pumpWidget(
|
||||||
|
ProviderScope(
|
||||||
|
overrides: [
|
||||||
|
preferencesRepositoryProvider.overrideWith(
|
||||||
|
(ref) => PreferencesStateNotifier(prefs),
|
||||||
|
),
|
||||||
|
documentRepositoryProvider.overrideWith(
|
||||||
|
(ref) =>
|
||||||
|
DocumentStateNotifier()..openDocument(
|
||||||
|
bytes: initialBytes,
|
||||||
|
pageCount: 3,
|
||||||
|
knownPageCount: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pdfViewModelProvider.overrideWith(
|
||||||
|
(ref) => PdfViewModel(ref, useMockViewer: false),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
container = ProviderScope.containerOf(context);
|
||||||
|
return MaterialApp(
|
||||||
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
|
locale: const Locale('en'),
|
||||||
|
home: PdfSignatureHomePage(
|
||||||
|
onPickPdf: simulatePick,
|
||||||
|
onClosePdf: () {},
|
||||||
|
currentFile: XFile('initial.pdf'),
|
||||||
|
// The only reliable way to detect the new document load correctly
|
||||||
|
onDocumentChanged: (doc) {
|
||||||
|
if (doc != null) {
|
||||||
|
lastDocPageCount = doc.pages.length;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
// Verify initial state Page 1/3
|
||||||
|
expect(find.byKey(const Key('lbl_page_info')), findsOneWidget);
|
||||||
|
final initialLabel =
|
||||||
|
tester.widget<Text>(find.byKey(const Key('lbl_page_info'))).data;
|
||||||
|
expect(initialLabel, contains('/3'));
|
||||||
|
|
||||||
|
// Tap open picker button to simulate opening new PDF
|
||||||
|
await tester.tap(find.byKey(const Key('btn_open_pdf_picker')));
|
||||||
|
// Allow frame to process state changes from simulatePick
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Wait for async page count detection to complete in repository
|
||||||
|
await tester.runAsync(() async {
|
||||||
|
final start = DateTime.now();
|
||||||
|
while (container.read(documentRepositoryProvider).pageCount != 10) {
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
if (DateTime.now().difference(start) > const Duration(seconds: 8)) {
|
||||||
|
final pageCount =
|
||||||
|
container.read(documentRepositoryProvider).pageCount;
|
||||||
|
fail(
|
||||||
|
'Timeout waiting for repository page count to update to 10 (current=$pageCount)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for restoration mechanism to complete
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 500));
|
||||||
|
await tester.pump();
|
||||||
|
});
|
||||||
|
|
||||||
|
final updatedLabel =
|
||||||
|
tester.widget<Text>(find.byKey(const Key('lbl_page_info'))).data;
|
||||||
|
expect(updatedLabel, contains('/10'));
|
||||||
|
// Verify that repository correctly analyzed PDF bytes and updated page count
|
||||||
|
expect(container.read(documentRepositoryProvider).pageCount, 10);
|
||||||
|
expect(lastDocPageCount, 10);
|
||||||
|
expect(container.read(pdfViewModelProvider).currentPage, 1);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 1012 B After Width: | Height: | Size: 676 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 9.2 KiB |
|
|
@ -4,6 +4,7 @@ import 'package:image/image.dart' as img;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:pdf_signature/data/services/export_service.dart';
|
import 'package:pdf_signature/data/services/export_service.dart';
|
||||||
|
import 'package:pdfrx/pdfrx.dart';
|
||||||
|
|
||||||
import '../../domain/models/model.dart';
|
import '../../domain/models/model.dart';
|
||||||
|
|
||||||
|
|
@ -24,7 +25,69 @@ class DocumentStateNotifier extends StateNotifier<Document> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void openPicked({required int pageCount, Uint8List? bytes}) {
|
/// Unified open API replacing multiple legacy variants.
|
||||||
|
///
|
||||||
|
/// Usage patterns:
|
||||||
|
/// openDocument(bytes: data) -> derive page count asynchronously.
|
||||||
|
/// openDocument(bytes: data, pageCount: 203, knownPageCount: true) -> fast path.
|
||||||
|
/// openDocument(pageCount: 5) -> open empty placeholder document (tests).
|
||||||
|
void openDocument({
|
||||||
|
Uint8List? bytes,
|
||||||
|
int? pageCount,
|
||||||
|
bool knownPageCount = false,
|
||||||
|
}) {
|
||||||
|
debugPrint(
|
||||||
|
'[DocumentRepository] openDocument called (bytes=${bytes?.length} pageCount=$pageCount known=$knownPageCount)',
|
||||||
|
);
|
||||||
|
if (bytes == null) {
|
||||||
|
// No bytes: treat as synthetic document (tests) using provided pageCount or default 1
|
||||||
|
final pc = pageCount ?? 1;
|
||||||
|
state = state.copyWith(
|
||||||
|
loaded: true,
|
||||||
|
pageCount: pc,
|
||||||
|
pickedPdfBytes: null,
|
||||||
|
placementsByPage: <int, List<SignaturePlacement>>{},
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Bytes provided
|
||||||
|
if ((knownPageCount || pageCount != null) && pageCount != null) {
|
||||||
|
// Fast path: caller already determined count
|
||||||
|
state = state.copyWith(
|
||||||
|
loaded: true,
|
||||||
|
pageCount: pageCount.clamp(1, 9999),
|
||||||
|
pickedPdfBytes: bytes,
|
||||||
|
placementsByPage: <int, List<SignaturePlacement>>{},
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Derive asynchronously
|
||||||
|
_openPickedAsync(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Deprecated wrappers for backward compatibility (can be removed later) ---
|
||||||
|
@Deprecated('Use openDocument(bytes: ...) instead')
|
||||||
|
void openPicked({Uint8List? bytes}) => openDocument(bytes: bytes);
|
||||||
|
|
||||||
|
@Deprecated(
|
||||||
|
'Use openDocument(bytes: ..., pageCount: x, knownPageCount: true) instead',
|
||||||
|
)
|
||||||
|
void openPickedKnown({required int pageCount, required Uint8List bytes}) =>
|
||||||
|
openDocument(bytes: bytes, pageCount: pageCount, knownPageCount: true);
|
||||||
|
|
||||||
|
Future<void> _openPickedAsync(Uint8List bytes) async {
|
||||||
|
int pageCount = 1; // default fallback
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Determine actual page count from PDF bytes
|
||||||
|
final doc = await PdfDocument.openData(bytes);
|
||||||
|
pageCount = doc.pages.length;
|
||||||
|
debugPrint('[DocumentRepository] PDF has $pageCount pages');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[DocumentRepository] Failed to read PDF page count: $e');
|
||||||
|
// Keep default pageCount = 1 on error
|
||||||
|
}
|
||||||
|
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
loaded: true,
|
loaded: true,
|
||||||
pageCount: pageCount,
|
pageCount: pageCount,
|
||||||
|
|
@ -33,12 +96,23 @@ class DocumentStateNotifier extends StateNotifier<Document> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For tests that need to specify page count explicitly
|
||||||
|
@visibleForTesting
|
||||||
|
@Deprecated(
|
||||||
|
'Use openDocument(pageCount: x) for synthetic docs or with bytes+knownPageCount',
|
||||||
|
)
|
||||||
|
void openPickedWithPageCount({required int pageCount, Uint8List? bytes}) =>
|
||||||
|
openDocument(bytes: bytes, pageCount: pageCount, knownPageCount: true);
|
||||||
|
|
||||||
void close() {
|
void close() {
|
||||||
state = Document.initial();
|
state = Document.initial();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setPageCount(int count) {
|
void setPageCount(int count) {
|
||||||
if (!state.loaded) return;
|
if (!state.loaded) return;
|
||||||
|
debugPrint(
|
||||||
|
'[DocumentRepository] setPageCount called: $count (current: ${state.pageCount})',
|
||||||
|
);
|
||||||
state = state.copyWith(pageCount: count.clamp(1, 9999));
|
state = state.copyWith(pageCount: count.clamp(1, 9999));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,21 +149,12 @@ class DocumentStateNotifier extends StateNotifier<Document> {
|
||||||
// signature bytes were provided.
|
// signature bytes were provided.
|
||||||
static final img.Image _singleTransparentPng = img.Image(width: 1, height: 1);
|
static final img.Image _singleTransparentPng = img.Image(width: 1, height: 1);
|
||||||
|
|
||||||
|
@Deprecated('Use modifyPlacement')
|
||||||
void updatePlacementRotation({
|
void updatePlacementRotation({
|
||||||
required int page,
|
required int page,
|
||||||
required int index,
|
required int index,
|
||||||
required double rotationDeg,
|
required double rotationDeg,
|
||||||
}) {
|
}) => modifyPlacement(page: page, index: index, rotationDeg: rotationDeg);
|
||||||
if (!state.loaded) return;
|
|
||||||
final p = page.clamp(1, state.pageCount);
|
|
||||||
final map = Map<int, List<SignaturePlacement>>.from(state.placementsByPage);
|
|
||||||
final list = List<SignaturePlacement>.from(map[p] ?? const []);
|
|
||||||
if (index >= 0 && index < list.length) {
|
|
||||||
list[index] = list[index].copyWith(rotationDeg: rotationDeg);
|
|
||||||
map[p] = list;
|
|
||||||
state = state.copyWith(placementsByPage: map);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void removePlacement({required int page, required int index}) {
|
void removePlacement({required int page, required int index}) {
|
||||||
if (!state.loaded) return;
|
if (!state.loaded) return;
|
||||||
|
|
@ -108,22 +173,37 @@ class DocumentStateNotifier extends StateNotifier<Document> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the rect of an existing placement on a page.
|
// Update the rect of an existing placement on a page.
|
||||||
|
@Deprecated('Use modifyPlacement')
|
||||||
void updatePlacementRect({
|
void updatePlacementRect({
|
||||||
required int page,
|
required int page,
|
||||||
required int index,
|
required int index,
|
||||||
required Rect rect,
|
required Rect rect,
|
||||||
|
}) => modifyPlacement(page: page, index: index, rect: rect);
|
||||||
|
|
||||||
|
/// Generic partial update for a placement. Any non-null field is applied.
|
||||||
|
void modifyPlacement({
|
||||||
|
required int page,
|
||||||
|
required int index,
|
||||||
|
Rect? rect,
|
||||||
|
double? rotationDeg,
|
||||||
|
SignatureAsset? asset,
|
||||||
|
GraphicAdjust? graphicAdjust,
|
||||||
}) {
|
}) {
|
||||||
if (!state.loaded) return;
|
if (!state.loaded) return;
|
||||||
final p = page.clamp(1, state.pageCount);
|
final p = page.clamp(1, state.pageCount);
|
||||||
final map = Map<int, List<SignaturePlacement>>.from(state.placementsByPage);
|
final map = Map<int, List<SignaturePlacement>>.from(state.placementsByPage);
|
||||||
final list = List<SignaturePlacement>.from(map[p] ?? const []);
|
final list = List<SignaturePlacement>.from(map[p] ?? const []);
|
||||||
if (index >= 0 && index < list.length) {
|
if (index < 0 || index >= list.length) return;
|
||||||
final existing = list[index];
|
final current = list[index];
|
||||||
list[index] = existing.copyWith(rect: rect);
|
list[index] = current.copyWith(
|
||||||
|
rect: rect ?? current.rect,
|
||||||
|
rotationDeg: rotationDeg ?? current.rotationDeg,
|
||||||
|
asset: asset ?? current.asset,
|
||||||
|
graphicAdjust: graphicAdjust ?? current.graphicAdjust,
|
||||||
|
);
|
||||||
map[p] = list;
|
map[p] = list;
|
||||||
state = state.copyWith(placementsByPage: map);
|
state = state.copyWith(placementsByPage: map);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
List<SignaturePlacement> placementsOn(int page) {
|
List<SignaturePlacement> placementsOn(int page) {
|
||||||
return List<SignaturePlacement>.from(
|
return List<SignaturePlacement>.from(
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:pdfrx/pdfrx.dart';
|
||||||
import 'package:pdf_signature/app.dart';
|
import 'package:pdf_signature/app.dart';
|
||||||
|
import 'package:pdf_signature/utils/pdfrx_cache_init/pdfrx_cache_init.dart';
|
||||||
export 'package:pdf_signature/app.dart';
|
export 'package:pdf_signature/app.dart';
|
||||||
|
|
||||||
void main() {
|
Future<void> main() async {
|
||||||
// Ensure Flutter bindings are initialized before platform channel usage
|
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
// Initialize pdfrx core (safe to call multiple times) and set up cache directory.
|
||||||
|
pdfrxFlutterInitialize();
|
||||||
|
await initPdfrxCache();
|
||||||
// Disable right-click context menu on web using Flutter API
|
// Disable right-click context menu on web using Flutter API
|
||||||
if (kReleaseMode) {
|
if (kReleaseMode) {
|
||||||
debugPrint = (String? message, {int? wrapWidth}) {
|
debugPrint = (String? message, {int? wrapWidth}) {
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||||
({String? path, Uint8List? bytes, String? fileName}) =>
|
({String? path, Uint8List? bytes, String? fileName}) =>
|
||||||
sessionVm.openPdf(
|
sessionVm.openPdf(
|
||||||
path: path,
|
path: path,
|
||||||
bytes: bytes,
|
bytes: bytes ?? Uint8List(0),
|
||||||
fileName: fileName,
|
fileName: fileName,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'document_version.freezed.dart';
|
||||||
|
|
||||||
|
/// Internal data model for tracking document versions in the UI layer.
|
||||||
|
/// This is separate from the domain Document model to avoid coupling UI concerns with business logic.
|
||||||
|
@freezed
|
||||||
|
abstract class DocumentVersion with _$DocumentVersion {
|
||||||
|
const factory DocumentVersion({
|
||||||
|
@Default(0) int version,
|
||||||
|
Uint8List? lastBytes,
|
||||||
|
}) = _DocumentVersion;
|
||||||
|
|
||||||
|
factory DocumentVersion.initial() => const DocumentVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DocumentVersionMethods on DocumentVersion {
|
||||||
|
/// Generate the source name for PdfDocumentRefData based on version
|
||||||
|
String get sourceName => 'document_v$version.pdf';
|
||||||
|
|
||||||
|
/// Check if bytes have changed and need version increment
|
||||||
|
bool shouldIncrementVersion(Uint8List? newBytes) {
|
||||||
|
return !identical(lastBytes, newBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Increment version and update bytes
|
||||||
|
DocumentVersion incrementVersion(Uint8List? newBytes) {
|
||||||
|
return copyWith(version: version + 1, lastBytes: newBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
// ignore_for_file: unnecessary_import
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'package:cross_file/cross_file.dart';
|
import 'package:cross_file/cross_file.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
@ -5,9 +6,12 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:pdf_signature/data/repositories/document_repository.dart';
|
import 'package:pdf_signature/data/repositories/document_repository.dart';
|
||||||
import 'package:pdf_signature/data/repositories/signature_card_repository.dart';
|
import 'package:pdf_signature/data/repositories/signature_card_repository.dart';
|
||||||
import 'package:pdf_signature/domain/models/model.dart';
|
import 'package:pdf_signature/domain/models/model.dart';
|
||||||
|
import 'package:pdf_signature/ui/features/pdf/view_model/document_version.dart';
|
||||||
import 'package:pdfrx/pdfrx.dart';
|
import 'package:pdfrx/pdfrx.dart';
|
||||||
import 'package:file_picker/file_picker.dart' as fp;
|
import 'package:file_picker/file_picker.dart' as fp;
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
class PdfViewModel extends ChangeNotifier {
|
class PdfViewModel extends ChangeNotifier {
|
||||||
final Ref ref;
|
final Ref ref;
|
||||||
|
|
@ -31,6 +35,26 @@ class PdfViewModel extends ChangeNotifier {
|
||||||
final Set<String> _lockedPlacements = {};
|
final Set<String> _lockedPlacements = {};
|
||||||
Set<String> get lockedPlacements => Set.unmodifiable(_lockedPlacements);
|
Set<String> get lockedPlacements => Set.unmodifiable(_lockedPlacements);
|
||||||
|
|
||||||
|
// Document version tracking for UI consistency
|
||||||
|
DocumentVersion _documentVersion = DocumentVersion.initial();
|
||||||
|
|
||||||
|
// Get current document source name for PdfDocumentRefData
|
||||||
|
String get documentSourceName {
|
||||||
|
// Ensure document version is up to date, but only update if really needed
|
||||||
|
_updateDocumentVersionIfNeeded();
|
||||||
|
return _documentVersion.sourceName;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateDocumentVersionIfNeeded() {
|
||||||
|
final document = ref.read(documentRepositoryProvider);
|
||||||
|
if (!identical(_documentVersion.lastBytes, document.pickedPdfBytes)) {
|
||||||
|
_documentVersion = DocumentVersion(
|
||||||
|
version: _documentVersion.version + 1,
|
||||||
|
lastBytes: document.pickedPdfBytes,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// const bool.fromEnvironment('FLUTTER_TEST', defaultValue: false);
|
// const bool.fromEnvironment('FLUTTER_TEST', defaultValue: false);
|
||||||
PdfViewModel(this.ref, {bool? useMockViewer})
|
PdfViewModel(this.ref, {bool? useMockViewer})
|
||||||
: _useMockViewer =
|
: _useMockViewer =
|
||||||
|
|
@ -114,11 +138,7 @@ class PdfViewModel extends ChangeNotifier {
|
||||||
}) {
|
}) {
|
||||||
ref
|
ref
|
||||||
.read(documentRepositoryProvider.notifier)
|
.read(documentRepositoryProvider.notifier)
|
||||||
.updatePlacementRotation(
|
.modifyPlacement(page: page, index: index, rotationDeg: rotationDeg);
|
||||||
page: page,
|
|
||||||
index: index,
|
|
||||||
rotationDeg: rotationDeg,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void removePlacement({required int page, required int index}) {
|
void removePlacement({required int page, required int index}) {
|
||||||
|
|
@ -139,7 +159,7 @@ class PdfViewModel extends ChangeNotifier {
|
||||||
}) {
|
}) {
|
||||||
ref
|
ref
|
||||||
.read(documentRepositoryProvider.notifier)
|
.read(documentRepositoryProvider.notifier)
|
||||||
.updatePlacementRect(page: page, index: index, rect: rect);
|
.modifyPlacement(page: page, index: index, rect: rect);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<SignaturePlacement> placementsOn(int page) {
|
List<SignaturePlacement> placementsOn(int page) {
|
||||||
|
|
@ -275,30 +295,69 @@ class PdfSessionViewModel extends ChangeNotifier {
|
||||||
if (effectiveBytes == null && path != null && path.isNotEmpty) {
|
if (effectiveBytes == null && path != null && path.isNotEmpty) {
|
||||||
try {
|
try {
|
||||||
effectiveBytes = await XFile(path).readAsBytes();
|
effectiveBytes = await XFile(path).readAsBytes();
|
||||||
} catch (_) {
|
} catch (e, st) {
|
||||||
effectiveBytes = null;
|
effectiveBytes = null;
|
||||||
|
debugPrint(
|
||||||
|
'[PdfSessionViewModel] Failed to read PDF data from path=$path error=$e',
|
||||||
|
);
|
||||||
|
debugPrint(st.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (effectiveBytes != null) {
|
||||||
await openPdf(path: path, bytes: effectiveBytes, fileName: name);
|
await openPdf(path: path, bytes: effectiveBytes, fileName: name);
|
||||||
|
} else {
|
||||||
|
debugPrint(
|
||||||
|
'[PdfSessionViewModel] No PDF data available to open (path=$path, name=$name)',
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> openPdf({
|
Future<void> openPdf({
|
||||||
String? path,
|
String? path,
|
||||||
Uint8List? bytes,
|
required Uint8List bytes,
|
||||||
String? fileName,
|
String? fileName,
|
||||||
}) async {
|
}) async {
|
||||||
int pageCount = 1; // default
|
int pageCount = 1; // default
|
||||||
if (bytes != null) {
|
|
||||||
try {
|
try {
|
||||||
|
// Defensive: ensure Pdfrx cache directory set (in case main init skipped in tests)
|
||||||
|
if (Pdfrx.getCacheDirectory == null && !kIsWeb) {
|
||||||
|
debugPrint('[PdfSessionViewModel] Setting Pdfrx cache directory (io)');
|
||||||
|
try {
|
||||||
|
final temp = await getTemporaryDirectory();
|
||||||
|
Pdfrx.getCacheDirectory = () async => temp.path;
|
||||||
|
} catch (e, st) {
|
||||||
|
debugPrint(
|
||||||
|
'[PdfSessionViewModel] Failed to set fallback cache dir error=$e',
|
||||||
|
);
|
||||||
|
debugPrint(st.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Ensure engine initialized (safe multiple calls)
|
||||||
|
pdfrxFlutterInitialize();
|
||||||
final doc = await PdfDocument.openData(bytes);
|
final doc = await PdfDocument.openData(bytes);
|
||||||
pageCount = doc.pages.length;
|
pageCount = doc.pages.length;
|
||||||
} catch (_) {
|
debugPrint(
|
||||||
// ignore invalid bytes
|
'[PdfSessionViewModel] Opened PDF bytes length=${bytes.length} pages=$pageCount',
|
||||||
}
|
);
|
||||||
|
// Use fast path to populate repository BEFORE navigation so the first
|
||||||
|
// build of PdfViewerWidget sees a loaded document and avoids showing
|
||||||
|
// transient "No PDF loaded".
|
||||||
|
ref
|
||||||
|
.read(documentRepositoryProvider.notifier)
|
||||||
|
.openDocument(
|
||||||
|
bytes: bytes,
|
||||||
|
pageCount: pageCount,
|
||||||
|
knownPageCount: true,
|
||||||
|
);
|
||||||
|
} catch (e, st) {
|
||||||
|
debugPrint(
|
||||||
|
'[PdfSessionViewModel] Failed to read PDF data from bytes error=$e',
|
||||||
|
);
|
||||||
|
debugPrint(st.toString());
|
||||||
}
|
}
|
||||||
if (path != null && path.isNotEmpty) {
|
if (path != null && path.isNotEmpty) {
|
||||||
_currentFile = XFile(path);
|
_currentFile = XFile(path);
|
||||||
} else if (bytes != null && (fileName != null && fileName.isNotEmpty)) {
|
} else if ((fileName != null && fileName.isNotEmpty)) {
|
||||||
// Keep in-memory XFile so .name is available for suggestion
|
// Keep in-memory XFile so .name is available for suggestion
|
||||||
try {
|
try {
|
||||||
_currentFile = XFile.fromData(
|
_currentFile = XFile.fromData(
|
||||||
|
|
@ -306,8 +365,12 @@ class PdfSessionViewModel extends ChangeNotifier {
|
||||||
name: fileName,
|
name: fileName,
|
||||||
mimeType: 'application/pdf',
|
mimeType: 'application/pdf',
|
||||||
);
|
);
|
||||||
} catch (_) {
|
} catch (e, st) {
|
||||||
_currentFile = XFile(fileName);
|
_currentFile = XFile(fileName);
|
||||||
|
debugPrint(
|
||||||
|
'[PdfSessionViewModel] Failed to create XFile.fromData name=$fileName error=$e',
|
||||||
|
);
|
||||||
|
debugPrint(st.toString());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_currentFile = XFile('');
|
_currentFile = XFile('');
|
||||||
|
|
@ -322,13 +385,19 @@ class PdfSessionViewModel extends ChangeNotifier {
|
||||||
} else {
|
} else {
|
||||||
_displayFileName = '';
|
_displayFileName = '';
|
||||||
}
|
}
|
||||||
ref
|
// If fast path failed to set repository (e.g., exception earlier), fallback to async derive.
|
||||||
.read(documentRepositoryProvider.notifier)
|
if (ref.read(documentRepositoryProvider).pickedPdfBytes != bytes) {
|
||||||
.openPicked(pageCount: pageCount, bytes: bytes);
|
debugPrint(
|
||||||
|
'[PdfSessionViewModel] Fallback deriving page count via openDocument',
|
||||||
|
);
|
||||||
|
ref.read(documentRepositoryProvider.notifier).openDocument(bytes: bytes);
|
||||||
|
}
|
||||||
// Keep existing signature cards when opening a new document.
|
// Keep existing signature cards when opening a new document.
|
||||||
// The feature "Open a different document will reset signature placements but keep signature cards"
|
// The feature "Open a different document will reset signature placements but keep signature cards"
|
||||||
// relies on this behavior. Placements are reset by openPicked() above.
|
// relies on this behavior. Placements are reset by openPicked() above.
|
||||||
|
debugPrint('[PdfSessionViewModel] Navigating to /pdf');
|
||||||
router.go('/pdf');
|
router.go('/pdf');
|
||||||
|
debugPrint('[PdfSessionViewModel] Notifying listeners after open');
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,12 @@ class PdfPageArea extends ConsumerStatefulWidget {
|
||||||
super.key,
|
super.key,
|
||||||
required this.pageSize,
|
required this.pageSize,
|
||||||
required this.controller,
|
required this.controller,
|
||||||
|
this.onDocumentChanged,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Size pageSize;
|
final Size pageSize;
|
||||||
final PdfViewerController controller;
|
final PdfViewerController controller;
|
||||||
|
final void Function(PdfDocument?)? onDocumentChanged;
|
||||||
@override
|
@override
|
||||||
ConsumerState<PdfPageArea> createState() => _PdfPageAreaState();
|
ConsumerState<PdfPageArea> createState() => _PdfPageAreaState();
|
||||||
}
|
}
|
||||||
|
|
@ -163,7 +165,9 @@ class _PdfPageAreaState extends ConsumerState<PdfPageArea> {
|
||||||
pageKeyBuilder: _pageKey,
|
pageKeyBuilder: _pageKey,
|
||||||
scrollToPage: _scrollToPage,
|
scrollToPage: _scrollToPage,
|
||||||
controller: widget.controller,
|
controller: widget.controller,
|
||||||
innerViewerKey: const ValueKey('viewer_idle'),
|
// Remove fixed innerViewerKey to allow PdfViewerWidget to generate dynamic keys
|
||||||
|
// innerViewerKey: const ValueKey('viewer_idle'),
|
||||||
|
onDocumentChanged: widget.onDocumentChanged,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ class PdfSignatureHomePage extends ConsumerStatefulWidget {
|
||||||
// available, this name preserves the user-selected filename so we can
|
// available, this name preserves the user-selected filename so we can
|
||||||
// suggest a proper "signed_*.pdf" on save.
|
// suggest a proper "signed_*.pdf" on save.
|
||||||
final String? currentFileName;
|
final String? currentFileName;
|
||||||
|
// Optional listener for underlying Pdfrx document change events.
|
||||||
|
final void Function(PdfDocument?)? onDocumentChanged;
|
||||||
|
|
||||||
const PdfSignatureHomePage({
|
const PdfSignatureHomePage({
|
||||||
super.key,
|
super.key,
|
||||||
|
|
@ -36,6 +38,7 @@ class PdfSignatureHomePage extends ConsumerStatefulWidget {
|
||||||
required this.onClosePdf,
|
required this.onClosePdf,
|
||||||
required this.currentFile,
|
required this.currentFile,
|
||||||
this.currentFileName,
|
this.currentFileName,
|
||||||
|
this.onDocumentChanged,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -333,7 +336,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
pdf.loaded && pdf.pickedPdfBytes != null
|
pdf.loaded && pdf.pickedPdfBytes != null
|
||||||
? PdfDocumentRefData(
|
? PdfDocumentRefData(
|
||||||
pdf.pickedPdfBytes!,
|
pdf.pickedPdfBytes!,
|
||||||
sourceName: 'document.pdf',
|
sourceName: pdfViewModel.documentSourceName,
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
|
@ -354,6 +357,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
||||||
controller: _viewModel.controller,
|
controller: _viewModel.controller,
|
||||||
key: const ValueKey('pdf_page_area'),
|
key: const ValueKey('pdf_page_area'),
|
||||||
pageSize: _pageSize,
|
pageSize: _pageSize,
|
||||||
|
onDocumentChanged: widget.onDocumentChanged,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import 'package:pdf_signature/l10n/app_localizations.dart';
|
||||||
import 'package:responsive_framework/responsive_framework.dart';
|
import 'package:responsive_framework/responsive_framework.dart';
|
||||||
|
|
||||||
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart';
|
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart';
|
||||||
|
import 'package:pdf_signature/data/repositories/document_repository.dart';
|
||||||
|
|
||||||
class PdfToolbar extends ConsumerStatefulWidget {
|
class PdfToolbar extends ConsumerStatefulWidget {
|
||||||
const PdfToolbar({
|
const PdfToolbar({
|
||||||
|
|
@ -59,7 +60,9 @@ class _PdfToolbarState extends ConsumerState<PdfToolbar> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final pdfViewModel = ref.watch(pdfViewModelProvider);
|
final pdfViewModel = ref.watch(pdfViewModelProvider);
|
||||||
final pdf = pdfViewModel.document;
|
final pdf = ref.watch(
|
||||||
|
documentRepositoryProvider,
|
||||||
|
); // Watch document directly for updates
|
||||||
final currentPage = pdfViewModel.currentPage;
|
final currentPage = pdfViewModel.currentPage;
|
||||||
final l = AppLocalizations.of(context);
|
final l = AppLocalizations.of(context);
|
||||||
final pageInfo = l.pageInfo(currentPage, pdf.pageCount);
|
final pageInfo = l.pageInfo(currentPage, pdf.pageCount);
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,9 @@ import 'package:pdf_signature/l10n/app_localizations.dart';
|
||||||
import 'pdf_page_overlays.dart';
|
import 'pdf_page_overlays.dart';
|
||||||
import './pdf_mock_continuous_list.dart';
|
import './pdf_mock_continuous_list.dart';
|
||||||
import '../view_model/pdf_view_model.dart';
|
import '../view_model/pdf_view_model.dart';
|
||||||
|
import 'package:pdf_signature/domain/models/document.dart';
|
||||||
|
import 'package:pdf_signature/data/repositories/document_repository.dart';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
// Provider to control whether viewer overlays (like scroll thumbs) are enabled.
|
// Provider to control whether viewer overlays (like scroll thumbs) are enabled.
|
||||||
// Integration tests can override this to false to avoid long-running animations.
|
// Integration tests can override this to false to avoid long-running animations.
|
||||||
|
|
@ -18,6 +21,7 @@ class PdfViewerWidget extends ConsumerStatefulWidget {
|
||||||
this.scrollToPage,
|
this.scrollToPage,
|
||||||
required this.controller,
|
required this.controller,
|
||||||
this.innerViewerKey,
|
this.innerViewerKey,
|
||||||
|
this.onDocumentChanged,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Size pageSize;
|
final Size pageSize;
|
||||||
|
|
@ -26,13 +30,43 @@ class PdfViewerWidget extends ConsumerStatefulWidget {
|
||||||
final PdfViewerController controller;
|
final PdfViewerController controller;
|
||||||
// Optional key applied to the inner Pdfrx PdfViewer to force disposal/rebuild
|
// Optional key applied to the inner Pdfrx PdfViewer to force disposal/rebuild
|
||||||
final Key? innerViewerKey;
|
final Key? innerViewerKey;
|
||||||
|
// External hook to observe document changes (forwarded from Pdfrx onDocumentChanged)
|
||||||
|
final void Function(PdfDocument?)? onDocumentChanged;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<PdfViewerWidget> createState() => _PdfViewerWidgetState();
|
ConsumerState<PdfViewerWidget> createState() => _PdfViewerWidgetState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PdfViewerWidgetState extends ConsumerState<PdfViewerWidget> {
|
class _PdfViewerWidgetState extends ConsumerState<PdfViewerWidget> {
|
||||||
PdfDocumentRef? _documentRef;
|
final ValueNotifier<PdfDocumentRef?> _docRefNotifier = ValueNotifier(null);
|
||||||
|
Uint8List? _lastBytes;
|
||||||
|
void _updateDocRef(Document doc) {
|
||||||
|
if (!doc.loaded || doc.pickedPdfBytes == null) {
|
||||||
|
if (_docRefNotifier.value != null) {
|
||||||
|
debugPrint('[PdfViewerWidget] Clearing docRef (no document loaded)');
|
||||||
|
_docRefNotifier.value = null;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final bytes = doc.pickedPdfBytes!;
|
||||||
|
if (!identical(bytes, _lastBytes)) {
|
||||||
|
_lastBytes = bytes;
|
||||||
|
final viewModel = ref.read(pdfViewModelProvider);
|
||||||
|
debugPrint(
|
||||||
|
'[PdfViewerWidget] New PDF bytes detected -> ${viewModel.documentSourceName}',
|
||||||
|
);
|
||||||
|
// Force a full detach by setting null first so PdfViewer unmounts even if the
|
||||||
|
// framework would otherwise optimize rebuilds with same key ordering.
|
||||||
|
if (_docRefNotifier.value != null) {
|
||||||
|
_docRefNotifier.value = null;
|
||||||
|
}
|
||||||
|
final newRef = PdfDocumentRefData(
|
||||||
|
bytes,
|
||||||
|
sourceName: viewModel.documentSourceName,
|
||||||
|
);
|
||||||
|
_docRefNotifier.value = newRef;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Public getter for testing the actual viewer page
|
// Public getter for testing the actual viewer page
|
||||||
int? get viewerCurrentPage => widget.controller.pageNumber;
|
int? get viewerCurrentPage => widget.controller.pageNumber;
|
||||||
|
|
@ -51,31 +85,9 @@ class _PdfViewerWidgetState extends ConsumerState<PdfViewerWidget> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final pdfViewModel = ref.watch(pdfViewModelProvider);
|
final pdfViewModel = ref.watch(pdfViewModelProvider);
|
||||||
final document = pdfViewModel.document;
|
final document = ref.watch(documentRepositoryProvider);
|
||||||
final useMock = pdfViewModel.useMockViewer;
|
final useMock = pdfViewModel.useMockViewer;
|
||||||
// trigger rebuild when active rect changes
|
_updateDocRef(document);
|
||||||
|
|
||||||
// Update document ref when document changes
|
|
||||||
if (document.loaded && document.pickedPdfBytes != null) {
|
|
||||||
if (_documentRef == null) {
|
|
||||||
_documentRef = PdfDocumentRefData(
|
|
||||||
document.pickedPdfBytes!,
|
|
||||||
sourceName: 'document.pdf',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_documentRef = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_documentRef == null && !useMock) {
|
|
||||||
String text;
|
|
||||||
try {
|
|
||||||
text = AppLocalizations.of(context).noPdfLoaded;
|
|
||||||
} catch (_) {
|
|
||||||
text = 'No PDF loaded';
|
|
||||||
}
|
|
||||||
return Center(child: Text(text));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (useMock) {
|
if (useMock) {
|
||||||
return PdfMockContinuousList(
|
return PdfMockContinuousList(
|
||||||
|
|
@ -89,9 +101,26 @@ class _PdfViewerWidgetState extends ConsumerState<PdfViewerWidget> {
|
||||||
}
|
}
|
||||||
|
|
||||||
final overlaysEnabled = ref.watch(viewerOverlaysEnabledProvider);
|
final overlaysEnabled = ref.watch(viewerOverlaysEnabledProvider);
|
||||||
|
return ValueListenableBuilder<PdfDocumentRef?>(
|
||||||
|
valueListenable: _docRefNotifier,
|
||||||
|
builder: (context, docRef, _) {
|
||||||
|
if (docRef == null) {
|
||||||
|
String text;
|
||||||
|
try {
|
||||||
|
text = AppLocalizations.of(context).noPdfLoaded;
|
||||||
|
} catch (_) {
|
||||||
|
text = 'No PDF loaded';
|
||||||
|
}
|
||||||
|
return Center(child: Text(text));
|
||||||
|
}
|
||||||
|
final pdfViewModel = ref.read(pdfViewModelProvider);
|
||||||
|
final viewerKey =
|
||||||
|
widget.innerViewerKey ??
|
||||||
|
Key('pdf_viewer_${pdfViewModel.documentSourceName}');
|
||||||
|
|
||||||
return PdfViewer(
|
return PdfViewer(
|
||||||
_documentRef!,
|
docRef,
|
||||||
key: widget.innerViewerKey ?? const Key('pdf_continuous_mock_list'),
|
key: viewerKey,
|
||||||
controller: widget.controller,
|
controller: widget.controller,
|
||||||
params: PdfViewerParams(
|
params: PdfViewerParams(
|
||||||
onViewerReady: (document, controller) {
|
onViewerReady: (document, controller) {
|
||||||
|
|
@ -106,6 +135,26 @@ class _PdfViewerWidgetState extends ConsumerState<PdfViewerWidget> {
|
||||||
ref.read(pdfViewModelProvider.notifier).jumpToPage(page);
|
ref.read(pdfViewModelProvider.notifier).jumpToPage(page);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onDocumentChanged: (doc) async {
|
||||||
|
final pc = doc?.pages.length;
|
||||||
|
debugPrint(
|
||||||
|
'[PdfViewerWidget] onDocumentChanged called (pages=$pc)',
|
||||||
|
);
|
||||||
|
if (doc != null) {
|
||||||
|
// Update internal page count state
|
||||||
|
ref
|
||||||
|
.read(pdfViewModelProvider.notifier)
|
||||||
|
.setPageCount(doc.pages.length);
|
||||||
|
}
|
||||||
|
// Invoke external listener after internal handling
|
||||||
|
try {
|
||||||
|
widget.onDocumentChanged?.call(doc);
|
||||||
|
} catch (e, st) {
|
||||||
|
debugPrint(
|
||||||
|
'[PdfViewerWidget] external onDocumentChanged threw: $e\n$st',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
viewerOverlayBuilder:
|
viewerOverlayBuilder:
|
||||||
overlaysEnabled
|
overlaysEnabled
|
||||||
? (context, size, handle) {
|
? (context, size, handle) {
|
||||||
|
|
@ -164,5 +213,7 @@ class _PdfViewerWidgetState extends ConsumerState<PdfViewerWidget> {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import 'dart:typed_data';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart';
|
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
@ -11,6 +11,14 @@ class WelcomeViewModel {
|
||||||
WelcomeViewModel(this.ref, this.router);
|
WelcomeViewModel(this.ref, this.router);
|
||||||
|
|
||||||
Future<void> openPdf({required String path, Uint8List? bytes}) async {
|
Future<void> openPdf({required String path, Uint8List? bytes}) async {
|
||||||
|
// Return early if no bytes provided - can't open PDF without data
|
||||||
|
if (bytes == null) {
|
||||||
|
debugPrint(
|
||||||
|
'[WelcomeViewModel] Cannot open PDF: no bytes provided for $path',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Use PdfSessionViewModel to open and navigate.
|
// Use PdfSessionViewModel to open and navigate.
|
||||||
final session = ref.read(pdfSessionViewModelProvider(router));
|
final session = ref.read(pdfSessionViewModelProvider(router));
|
||||||
await session.openPdf(path: path, bytes: bytes);
|
await session.openPdf(path: path, bytes: bytes);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
// Conditional export: use IO implementation except when compiling for web (html).
|
||||||
|
export 'pdfrx_cache_init_io.dart'
|
||||||
|
if (dart.library.html) 'pdfrx_cache_init_web.dart';
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:pdfrx/pdfrx.dart';
|
||||||
|
|
||||||
|
/// Initialize Pdfrx cache directory for IO platforms (mobile/desktop). No-op on web.
|
||||||
|
Future<void> initPdfrxCache() async {
|
||||||
|
try {
|
||||||
|
if (kIsWeb) return; // Guard (should not be used on web, but extra safety)
|
||||||
|
if (Pdfrx.getCacheDirectory != null) return; // Already set
|
||||||
|
final dir = await getTemporaryDirectory();
|
||||||
|
final cacheDir = Directory('${dir.path}/pdfrx_cache');
|
||||||
|
if (!await cacheDir.exists()) {
|
||||||
|
await cacheDir.create(recursive: true);
|
||||||
|
}
|
||||||
|
Pdfrx.getCacheDirectory = () async => cacheDir.path;
|
||||||
|
debugPrint(
|
||||||
|
'[pdfrx_cache_init_io] Pdfrx cache directory set to ${cacheDir.path}',
|
||||||
|
);
|
||||||
|
} catch (e, st) {
|
||||||
|
debugPrint(
|
||||||
|
'[pdfrx_cache_init_io] Failed to initialize Pdfrx cache directory: $e',
|
||||||
|
);
|
||||||
|
debugPrint(st.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:pdfrx/pdfrx.dart';
|
||||||
|
|
||||||
|
/// Web stub: pdfrx can operate without a filesystem cache; leave getCacheDirectory null.
|
||||||
|
Future<void> initPdfrxCache() async {
|
||||||
|
// Intentionally no-op. If desired, could set an in-memory indicator.
|
||||||
|
debugPrint(
|
||||||
|
'[pdfrx_cache_init_web] Skipping Pdfrx cache directory setup on web',
|
||||||
|
);
|
||||||
|
// Ensure any previous (hot-reload) IO assignment isn't kept when switching target.
|
||||||
|
if (kIsWeb && Pdfrx.getCacheDirectory != null) {
|
||||||
|
// Leave as-is; clearing could break existing references. Merely log.
|
||||||
|
debugPrint(
|
||||||
|
'[pdfrx_cache_init_web] Existing getCacheDirectory left unchanged',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 992 B After Width: | Height: | Size: 555 B |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
|
@ -135,10 +135,11 @@ flutter:
|
||||||
|
|
||||||
|
|
||||||
flutter_launcher_icons:
|
flutter_launcher_icons:
|
||||||
android: "launcher_icon"
|
|
||||||
ios: true
|
|
||||||
image_path: "assets/icon/pdf_signature-icon.png"
|
image_path: "assets/icon/pdf_signature-icon.png"
|
||||||
|
android: true
|
||||||
|
ios: true
|
||||||
min_sdk_android: 21 # android min sdk min:16, default 21
|
min_sdk_android: 21 # android min sdk min:16, default 21
|
||||||
|
remove_alpha_ios: true
|
||||||
web:
|
web:
|
||||||
generate: true
|
generate: true
|
||||||
image_path: "assets/icon/pdf_signature-icon.png"
|
image_path: "assets/icon/pdf_signature-icon.png"
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,9 @@ Future<void> aDocumentIsOpenAndContainsAtLeastOneSignaturePlacement(
|
||||||
) async {
|
) async {
|
||||||
final container = TestWorld.container ?? ProviderContainer();
|
final container = TestWorld.container ?? ProviderContainer();
|
||||||
TestWorld.container = container;
|
TestWorld.container = container;
|
||||||
container.read(documentRepositoryProvider.notifier).openPicked(pageCount: 5);
|
container
|
||||||
|
.read(documentRepositoryProvider.notifier)
|
||||||
|
.openDocument(pageCount: 5);
|
||||||
container
|
container
|
||||||
.read(documentRepositoryProvider.notifier)
|
.read(documentRepositoryProvider.notifier)
|
||||||
.addPlacement(
|
.addPlacement(
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,9 @@ aDocumentIsOpenAndContainsMultiplePlacedSignaturePlacementsAcrossPages(
|
||||||
) async {
|
) async {
|
||||||
final container = TestWorld.container ?? ProviderContainer();
|
final container = TestWorld.container ?? ProviderContainer();
|
||||||
TestWorld.container = container;
|
TestWorld.container = container;
|
||||||
container.read(documentRepositoryProvider.notifier).openPicked(pageCount: 5);
|
container
|
||||||
|
.read(documentRepositoryProvider.notifier)
|
||||||
|
.openDocument(pageCount: 5);
|
||||||
container
|
container
|
||||||
.read(documentRepositoryProvider.notifier)
|
.read(documentRepositoryProvider.notifier)
|
||||||
.addPlacement(
|
.addPlacement(
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,6 @@ Future<void> aDocumentIsOpenWithNoSignaturePlacementsPlaced(
|
||||||
TestWorld.container = container;
|
TestWorld.container = container;
|
||||||
container
|
container
|
||||||
.read(documentRepositoryProvider.notifier)
|
.read(documentRepositoryProvider.notifier)
|
||||||
.openPicked(pageCount: 5);
|
.openDocument(pageCount: 5);
|
||||||
// No placements added
|
// No placements added
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ Future<void> aDocumentPageIsSelectedForSigning(WidgetTester tester) async {
|
||||||
// Ensure a document is open
|
// Ensure a document is open
|
||||||
final repo = container.read(documentRepositoryProvider.notifier);
|
final repo = container.read(documentRepositoryProvider.notifier);
|
||||||
if (!container.read(documentRepositoryProvider).loaded) {
|
if (!container.read(documentRepositoryProvider).loaded) {
|
||||||
repo.openPicked(pageCount: 5);
|
repo.openDocument(pageCount: 5);
|
||||||
}
|
}
|
||||||
// Ensure current page is 1 for consistent subsequent steps
|
// Ensure current page is 1 for consistent subsequent steps
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,9 @@ Future<void> aMultipageDocumentIsOpen(WidgetTester tester) async {
|
||||||
container.read(signatureCardRepositoryProvider.notifier).state = [
|
container.read(signatureCardRepositoryProvider.notifier).state = [
|
||||||
SignatureCard.initial(),
|
SignatureCard.initial(),
|
||||||
];
|
];
|
||||||
container.read(documentRepositoryProvider.notifier).openPicked(pageCount: 5);
|
container
|
||||||
|
.read(documentRepositoryProvider.notifier)
|
||||||
|
.openDocument(pageCount: 5);
|
||||||
// Reset page state providers
|
// Reset page state providers
|
||||||
try {
|
try {
|
||||||
container.read(pdfViewModelProvider.notifier).jumpToPage(1);
|
container.read(pdfViewModelProvider.notifier).jumpToPage(1);
|
||||||
|
|
|
||||||
|
|
@ -11,5 +11,5 @@ Future<void> aSampleMultipageDocument5PagesIsAvailable(
|
||||||
TestWorld.container = container;
|
TestWorld.container = container;
|
||||||
container
|
container
|
||||||
.read(documentRepositoryProvider.notifier)
|
.read(documentRepositoryProvider.notifier)
|
||||||
.openPicked(pageCount: 5);
|
.openDocument(pageCount: 5);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ Future<void> aSignatureAssetIsPlacedOnThePage(WidgetTester tester) async {
|
||||||
if (!container.read(documentRepositoryProvider).loaded) {
|
if (!container.read(documentRepositoryProvider).loaded) {
|
||||||
container
|
container
|
||||||
.read(documentRepositoryProvider.notifier)
|
.read(documentRepositoryProvider.notifier)
|
||||||
.openPicked(pageCount: 5);
|
.openDocument(pageCount: 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get or create an asset
|
// Get or create an asset
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ Future<void> aSignaturePlacementIsPlacedOnPage(
|
||||||
if (!container.read(documentRepositoryProvider).loaded) {
|
if (!container.read(documentRepositoryProvider).loaded) {
|
||||||
container
|
container
|
||||||
.read(documentRepositoryProvider.notifier)
|
.read(documentRepositoryProvider.notifier)
|
||||||
.openPicked(pageCount: 5);
|
.openDocument(pageCount: 5);
|
||||||
}
|
}
|
||||||
final page = param1.toInt();
|
final page = param1.toInt();
|
||||||
container
|
container
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ Future<void> aSignaturePlacementIsPlacedWithAPositionAndSizeRelativeToThePage(
|
||||||
if (!container.read(documentRepositoryProvider).loaded) {
|
if (!container.read(documentRepositoryProvider).loaded) {
|
||||||
container
|
container
|
||||||
.read(documentRepositoryProvider.notifier)
|
.read(documentRepositoryProvider.notifier)
|
||||||
.openPicked(pageCount: 5);
|
.openDocument(pageCount: 5);
|
||||||
}
|
}
|
||||||
final currentPage = container.read(pdfViewModelProvider).currentPage;
|
final currentPage = container.read(pdfViewModelProvider).currentPage;
|
||||||
container
|
container
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ Future<void> draggingOrResizingOneDoesNotChangeTheOther(
|
||||||
final changedFirst = firstRectBefore.inflate(5);
|
final changedFirst = firstRectBefore.inflate(5);
|
||||||
container
|
container
|
||||||
.read(documentRepositoryProvider.notifier)
|
.read(documentRepositoryProvider.notifier)
|
||||||
.updatePlacementRect(page: page, index: 0, rect: changedFirst);
|
.modifyPlacement(page: page, index: 0, rect: changedFirst);
|
||||||
|
|
||||||
final after = container
|
final after = container
|
||||||
.read(documentRepositoryProvider.notifier)
|
.read(documentRepositoryProvider.notifier)
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,6 @@ Future<void> theUserDragsHandlesToResizeAndDragsToReposition(
|
||||||
height: currentRect.height + 30,
|
height: currentRect.height + 30,
|
||||||
);
|
);
|
||||||
|
|
||||||
pdfN.updatePlacementRect(page: currentPage, index: 0, rect: newRect);
|
pdfN.modifyPlacement(page: currentPage, index: 0, rect: newRect);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ theUserDragsItOnThePageOfTheDocumentToPlaceSignaturePlacementsInMultipleLocation
|
||||||
if (!container.read(documentRepositoryProvider).loaded) {
|
if (!container.read(documentRepositoryProvider).loaded) {
|
||||||
container
|
container
|
||||||
.read(documentRepositoryProvider.notifier)
|
.read(documentRepositoryProvider.notifier)
|
||||||
.openPicked(pageCount: 5);
|
.openDocument(pageCount: 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
container
|
container
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ theUserDragsThisSignatureCardOnThePageOfTheDocumentToPlaceASignaturePlacement(
|
||||||
if (!container.read(documentRepositoryProvider).loaded) {
|
if (!container.read(documentRepositoryProvider).loaded) {
|
||||||
container
|
container
|
||||||
.read(documentRepositoryProvider.notifier)
|
.read(documentRepositoryProvider.notifier)
|
||||||
.openPicked(pageCount: 5);
|
.openDocument(pageCount: 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get or create an asset
|
// Get or create an asset
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ Future<void> theUserOpensADifferentDocumentWithPages(
|
||||||
// Simulate "open a different document": reset placements and set page count.
|
// Simulate "open a different document": reset placements and set page count.
|
||||||
container
|
container
|
||||||
.read(documentRepositoryProvider.notifier)
|
.read(documentRepositoryProvider.notifier)
|
||||||
.openPicked(pageCount: pageCount);
|
.openDocument(pageCount: pageCount);
|
||||||
// Ensure there are 2 signature cards available as per scenario.
|
// Ensure there are 2 signature cards available as per scenario.
|
||||||
final cards = container.read(signatureCardRepositoryProvider);
|
final cards = container.read(signatureCardRepositoryProvider);
|
||||||
if (cards.length < 2) {
|
if (cards.length < 2) {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ Future<void> theUserPlacesItInMultipleLocationsInTheDocument(
|
||||||
TestWorld.container = container;
|
TestWorld.container = container;
|
||||||
final notifier = container.read(documentRepositoryProvider.notifier);
|
final notifier = container.read(documentRepositoryProvider.notifier);
|
||||||
// Always open a fresh doc to avoid state bleed between scenarios
|
// Always open a fresh doc to avoid state bleed between scenarios
|
||||||
notifier.openPicked(pageCount: 6);
|
notifier.openDocument(pageCount: 6);
|
||||||
// Place two on page 2 and one on page 4
|
// 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(10, 10, 80, 40));
|
||||||
notifier.addPlacement(page: 2, rect: const Rect.fromLTWH(120, 50, 80, 40));
|
notifier.addPlacement(page: 2, rect: const Rect.fromLTWH(120, 50, 80, 40));
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ Future<void> theUserSavesexportsTheDocument(WidgetTester tester) async {
|
||||||
// Load a minimal sample so the expectation passes in logic-only tests
|
// Load a minimal sample so the expectation passes in logic-only tests
|
||||||
container
|
container
|
||||||
.read(documentRepositoryProvider.notifier)
|
.read(documentRepositoryProvider.notifier)
|
||||||
.openPicked(pageCount: 2, bytes: Uint8List(10));
|
.openDocument(bytes: Uint8List(10), pageCount: 2, knownPageCount: true);
|
||||||
}
|
}
|
||||||
expect(pdf.loaded, isTrue, reason: 'PDF must be loaded before export');
|
expect(pdf.loaded, isTrue, reason: 'PDF must be loaded before export');
|
||||||
// Check if there are placements
|
// Check if there are placements
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,9 @@ Future<void> theUserSelects(WidgetTester tester, dynamic file) async {
|
||||||
final container = ProviderContainer();
|
final container = ProviderContainer();
|
||||||
TestWorld.container = container;
|
TestWorld.container = container;
|
||||||
// Mark page for signing to enable signature ops
|
// Mark page for signing to enable signature ops
|
||||||
container.read(documentRepositoryProvider.notifier).openPicked(pageCount: 1);
|
container
|
||||||
|
.read(documentRepositoryProvider.notifier)
|
||||||
|
.openDocument(pageCount: 1);
|
||||||
// For invalid/unsupported/empty selections we do NOT set image bytes.
|
// For invalid/unsupported/empty selections we do NOT set image bytes.
|
||||||
// This simulates a failed load and keeps rect null.
|
// This simulates a failed load and keeps rect null.
|
||||||
final token = file.toString();
|
final token = file.toString();
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,6 @@ Future<void> theUserUsesRotateControls(WidgetTester tester) async {
|
||||||
final currentPage = container.read(pdfViewModelProvider).currentPage;
|
final currentPage = container.read(pdfViewModelProvider).currentPage;
|
||||||
final placements = pdfN.placementsOn(currentPage);
|
final placements = pdfN.placementsOn(currentPage);
|
||||||
if (placements.isNotEmpty) {
|
if (placements.isNotEmpty) {
|
||||||
pdfN.updatePlacementRotation(
|
pdfN.modifyPlacement(page: currentPage, index: 0, rotationDeg: 45.0);
|
||||||
page: currentPage,
|
|
||||||
index: 0,
|
|
||||||
rotationDeg: 45.0,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,9 @@ Future<void> threeSignaturePlacementsArePlacedOnTheCurrentPage(
|
||||||
container.read(signatureCardRepositoryProvider.notifier).state = [
|
container.read(signatureCardRepositoryProvider.notifier).state = [
|
||||||
SignatureCard.initial(),
|
SignatureCard.initial(),
|
||||||
];
|
];
|
||||||
container.read(documentRepositoryProvider.notifier).openPicked(pageCount: 5);
|
container
|
||||||
|
.read(documentRepositoryProvider.notifier)
|
||||||
|
.openDocument(pageCount: 5);
|
||||||
final pdfN = container.read(documentRepositoryProvider.notifier);
|
final pdfN = container.read(documentRepositoryProvider.notifier);
|
||||||
final page = container.read(pdfViewModelProvider).currentPage;
|
final page = container.read(pdfViewModelProvider).currentPage;
|
||||||
pdfN.addPlacement(
|
pdfN.addPlacement(
|
||||||
|
|
|
||||||
|
|
@ -48,8 +48,11 @@ void main() {
|
||||||
),
|
),
|
||||||
documentRepositoryProvider.overrideWith(
|
documentRepositoryProvider.overrideWith(
|
||||||
(ref) =>
|
(ref) =>
|
||||||
DocumentStateNotifier()
|
DocumentStateNotifier()..openDocument(
|
||||||
..openPicked(pageCount: 5, bytes: Uint8List(0)),
|
bytes: Uint8List(0),
|
||||||
|
pageCount: 5,
|
||||||
|
knownPageCount: true,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
pdfViewModelProvider.overrideWith(
|
pdfViewModelProvider.overrideWith(
|
||||||
(ref) => PdfViewModel(ref, useMockViewer: true),
|
(ref) => PdfViewModel(ref, useMockViewer: true),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pdf_signature/ui/features/pdf/view_model/document_version.dart';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('DocumentVersion', () {
|
||||||
|
test('should generate consistent source names', () {
|
||||||
|
final version1 = DocumentVersion(version: 1);
|
||||||
|
final version2 = DocumentVersion(version: 2);
|
||||||
|
|
||||||
|
expect(version1.sourceName, 'document_v1.pdf');
|
||||||
|
expect(version2.sourceName, 'document_v2.pdf');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should increment version when bytes change', () {
|
||||||
|
final bytes1 = Uint8List.fromList([1, 2, 3]);
|
||||||
|
final bytes2 = Uint8List.fromList([4, 5, 6]);
|
||||||
|
|
||||||
|
final version = DocumentVersion(version: 1, lastBytes: bytes1);
|
||||||
|
|
||||||
|
expect(version.shouldIncrementVersion(bytes2), true);
|
||||||
|
expect(version.shouldIncrementVersion(bytes1), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should detect identical bytes correctly', () {
|
||||||
|
final bytes = Uint8List.fromList([1, 2, 3]);
|
||||||
|
final version = DocumentVersion(version: 1, lastBytes: bytes);
|
||||||
|
|
||||||
|
// Same bytes object should not trigger increment
|
||||||
|
expect(version.shouldIncrementVersion(bytes), false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -51,7 +51,7 @@ void main() {
|
||||||
}) async {
|
}) async {
|
||||||
final container = ProviderScope.containerOf(stateful.context);
|
final container = ProviderScope.containerOf(stateful.context);
|
||||||
final repo = container.read(documentRepositoryProvider.notifier);
|
final repo = container.read(documentRepositoryProvider.notifier);
|
||||||
repo.openPicked(pageCount: 1, bytes: bytes);
|
repo.openDocument(bytes: bytes, pageCount: 1, knownPageCount: true);
|
||||||
}, [fake]);
|
}, [fake]);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
|
|
|
||||||
BIN
web/favicon.png
|
Before Width: | Height: | Size: 992 B After Width: | Height: | Size: 555 B |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 2.0 KiB |