feat: remove currentPage in Document model

This commit is contained in:
insleker 2025-09-11 20:54:31 +08:00
parent 545d3ad688
commit c46aca1331
19 changed files with 378 additions and 139 deletions

View File

@ -4,12 +4,15 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart'; import 'package:integration_test/integration_test.dart';
import 'package:image/image.dart' as img; import 'package:image/image.dart' as img;
import 'dart:io';
import 'package:pdf_signature/data/services/export_service.dart'; import 'package:pdf_signature/data/services/export_service.dart';
import 'package:pdf_signature/data/repositories/signature_asset_repository.dart'; import 'package:pdf_signature/data/repositories/signature_asset_repository.dart';
import 'package:pdf_signature/data/repositories/signature_card_repository.dart';
import 'package:pdf_signature/data/repositories/document_repository.dart'; import 'package:pdf_signature/data/repositories/document_repository.dart';
import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart'; import 'package:pdf_signature/domain/models/model.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart';
import 'package:pdf_signature/ui/features/pdf/widgets/pdf_providers.dart'; import 'package:pdf_signature/ui/features/pdf/widgets/pdf_providers.dart';
import 'package:pdf_signature/ui/features/pdf/widgets/ui_services.dart'; import 'package:pdf_signature/ui/features/pdf/widgets/ui_services.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
@ -34,6 +37,8 @@ void main() {
final fake = RecordingExporter(); final fake = RecordingExporter();
SharedPreferences.setMockInitialValues({}); SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
// For this test, we don't need the PDF bytes since it's not loaded
await tester.pumpWidget( await tester.pumpWidget(
ProviderScope( ProviderScope(
overrides: [ overrides: [
@ -44,7 +49,7 @@ void main() {
(ref) => (ref) =>
DocumentStateNotifier()..openPicked( DocumentStateNotifier()..openPicked(
path: 'integration_test/data/sample-local-pdf.pdf', path: 'integration_test/data/sample-local-pdf.pdf',
pageCount: 5, pageCount: 1, // Initial value, will be updated by viewer
), ),
), ),
useMockViewerProvider.overrideWith((ref) => false), useMockViewerProvider.overrideWith((ref) => false),
@ -90,6 +95,8 @@ void main() {
tester, tester,
) async { ) async {
final sigBytes = _makeSig(); final sigBytes = _makeSig();
final pdfBytes =
await File('integration_test/data/sample-local-pdf.pdf').readAsBytes();
SharedPreferences.setMockInitialValues({}); SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
@ -103,7 +110,8 @@ void main() {
(ref) => (ref) =>
DocumentStateNotifier()..openPicked( DocumentStateNotifier()..openPicked(
path: 'integration_test/data/sample-local-pdf.pdf', path: 'integration_test/data/sample-local-pdf.pdf',
pageCount: 5, pageCount: 1, // Initial value, will be updated by viewer
bytes: pdfBytes,
), ),
), ),
signatureAssetRepositoryProvider.overrideWith((ref) { signatureAssetRepositoryProvider.overrideWith((ref) {
@ -111,6 +119,12 @@ void main() {
c.add(sigBytes, name: 'image'); c.add(sigBytes, name: 'image');
return c; return c;
}), }),
signatureCardRepositoryProvider.overrideWith((ref) {
final cardRepo = SignatureCardStateNotifier();
final asset = SignatureAsset(bytes: sigBytes, name: 'image');
cardRepo.addWithAsset(asset, 0.0);
return cardRepo;
}),
useMockViewerProvider.overrideWithValue(false), useMockViewerProvider.overrideWithValue(false),
], ],
child: const MaterialApp( child: const MaterialApp(
@ -139,10 +153,10 @@ void main() {
final r = container.read(activeRectProvider)!; final r = container.read(activeRectProvider)!;
final lib = container.read(signatureAssetRepositoryProvider); final lib = container.read(signatureAssetRepositoryProvider);
final asset = lib.isNotEmpty ? lib.first : null; final asset = lib.isNotEmpty ? lib.first : null;
final pdf = container.read(documentRepositoryProvider); final currentPage = container.read(pdfViewModelProvider);
container container
.read(documentRepositoryProvider.notifier) .read(documentRepositoryProvider.notifier)
.addPlacement(page: pdf.currentPage, rect: r, asset: asset); .addPlacement(page: currentPage, rect: r, asset: asset);
// Clear active overlay by hiding signatures temporarily // Clear active overlay by hiding signatures temporarily
container.read(signatureVisibilityProvider.notifier).state = false; container.read(signatureVisibilityProvider.notifier).state = false;
await tester.pump(); await tester.pump();

View File

@ -0,0 +1,252 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart';
import 'package:pdf_signature/ui/features/pdf/widgets/pdf_providers.dart';
import 'package:pdf_signature/ui/features/pdf/widgets/pages_sidebar.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:pdf_signature/data/repositories/preferences_repository.dart';
import 'package:pdf_signature/data/repositories/document_repository.dart';
import 'package:pdf_signature/l10n/app_localizations.dart';
/// It has known that sample-local-pdf.pdf has 3 pages.
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('PDF View: wheel scroll (page down)', (tester) async {
final pdfBytes =
await File('integration_test/data/sample-local-pdf.pdf').readAsBytes();
SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance();
await tester.pumpWidget(
ProviderScope(
overrides: [
preferencesRepositoryProvider.overrideWith(
(ref) => PreferencesStateNotifier(prefs),
),
documentRepositoryProvider.overrideWith(
(ref) =>
DocumentStateNotifier()..openPicked(
path: 'integration_test/data/sample-local-pdf.pdf',
pageCount: 1,
bytes: pdfBytes,
),
),
useMockViewerProvider.overrideWithValue(false),
],
child: const MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: Locale('en'),
home: PdfSignatureHomePage(),
),
),
);
await tester.pumpAndSettle();
// Find the PDF viewer area
final pdfViewer = find.byKey(const ValueKey('pdf_page_area'));
expect(pdfViewer, findsOneWidget);
// Get initial state
final ctx = tester.element(find.byType(PdfSignatureHomePage));
final container = ProviderScope.containerOf(ctx);
final initialPage = container.read(pdfViewModelProvider);
expect(initialPage, 1);
// Simulate wheel scroll down (PageDown) to reach the last page
for (int i = 0; i < 3; i++) {
await tester.sendKeyEvent(LogicalKeyboardKey.pageDown);
await tester.pumpAndSettle();
}
// Verify that we reached the last page by checking the actual viewer state
final pdfViewerState = tester.state<_PdfViewerWidgetState>(
find.byType(PdfViewerWidget),
);
final actualPage = pdfViewerState.viewerCurrentPage;
expect(actualPage, 3); // Should be on last page (3 pages total)
});
testWidgets('PDF View: zoom in/out', (tester) async {
final pdfBytes =
await File('integration_test/data/sample-local-pdf.pdf').readAsBytes();
SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance();
await tester.pumpWidget(
ProviderScope(
overrides: [
preferencesRepositoryProvider.overrideWith(
(ref) => PreferencesStateNotifier(prefs),
),
documentRepositoryProvider.overrideWith(
(ref) =>
DocumentStateNotifier()..openPicked(
path: 'integration_test/data/sample-local-pdf.pdf',
pageCount: 1,
bytes: pdfBytes,
),
),
useMockViewerProvider.overrideWithValue(false),
],
child: const MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: Locale('en'),
home: PdfSignatureHomePage(),
),
),
);
await tester.pumpAndSettle();
// Find the PDF viewer
final pdfViewer = find.byKey(const ValueKey('pdf_page_area'));
expect(pdfViewer, findsOneWidget);
// Perform pinch to zoom in
final center = tester.getCenter(pdfViewer);
// Simulate pinch zoom
final gesture1 = await tester.createGesture();
final gesture2 = await tester.createGesture();
await gesture1.down(center - const Offset(10, 0));
await gesture2.down(center + const Offset(10, 0));
await gesture1.moveTo(center - const Offset(20, 0));
await gesture2.moveTo(center + const Offset(20, 0));
await gesture1.up();
await gesture2.up();
await tester.pumpAndSettle();
// Verify zoom worked (this might be hard to verify directly)
// We can check if the viewer is still there
expect(pdfViewer, findsOneWidget);
});
testWidgets('PDF View: jump to page by clicking thumbnail', (tester) async {
final pdfBytes =
await File('integration_test/data/sample-local-pdf.pdf').readAsBytes();
SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance();
await tester.pumpWidget(
ProviderScope(
overrides: [
preferencesRepositoryProvider.overrideWith(
(ref) => PreferencesStateNotifier(prefs),
),
documentRepositoryProvider.overrideWith(
(ref) =>
DocumentStateNotifier()..openPicked(
path: 'integration_test/data/sample-local-pdf.pdf',
pageCount: 1,
bytes: pdfBytes,
),
),
useMockViewerProvider.overrideWithValue(false),
],
child: const MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: Locale('en'),
home: PdfSignatureHomePage(),
),
),
);
await tester.pumpAndSettle();
// Verify initial page
final ctx = tester.element(find.byType(PdfSignatureHomePage));
final container = ProviderScope.containerOf(ctx);
final initialPdf = container.read(documentRepositoryProvider);
final initialPage = container.read(pdfViewModelProvider);
expect(initialPage, 1);
// Click on page 3 thumbnail (last page)
final page3Thumbnail = find.text('3');
expect(page3Thumbnail, findsOneWidget);
await tester.tap(page3Thumbnail);
await tester.pumpAndSettle();
// Verify current page is 3 and page view actually jumped
final finalPage = container.read(pdfViewModelProvider);
expect(finalPage, 3);
expect(finalPage, isNot(equals(1)));
});
testWidgets('PDF View: scroll thumbnails', (tester) async {
final pdfBytes =
await File('integration_test/data/sample-local-pdf.pdf').readAsBytes();
SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance();
await tester.pumpWidget(
ProviderScope(
overrides: [
preferencesRepositoryProvider.overrideWith(
(ref) => PreferencesStateNotifier(prefs),
),
documentRepositoryProvider.overrideWith(
(ref) =>
DocumentStateNotifier()..openPicked(
path: 'integration_test/data/sample-local-pdf.pdf',
pageCount: 1,
bytes: pdfBytes,
),
),
useMockViewerProvider.overrideWithValue(false),
],
child: const MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: Locale('en'),
home: PdfSignatureHomePage(),
),
),
);
await tester.pumpAndSettle();
// Get initial page
final ctx = tester.element(find.byType(PdfSignatureHomePage));
final container = ProviderScope.containerOf(ctx);
final initialPage = container.read(pdfViewModelProvider);
expect(initialPage, 1);
// Find the pages sidebar
final pagesSidebar = find.byType(PagesSidebar);
expect(pagesSidebar, findsOneWidget);
// Scroll the thumbnails vertically
await tester.drag(pagesSidebar, const Offset(0, -200));
await tester.pumpAndSettle();
// Verify scrolling worked (thumbnails are still there)
final page1Thumbnail = find.text('1');
expect(page1Thumbnail, findsOneWidget);
// Check if page view changed (it shouldn't for vertical scroll of thumbs)
final afterScrollPage = container.read(pdfViewModelProvider);
expect(afterScrollPage, initialPage);
// Now test horizontal scroll of PDF viewer
final pdfViewer = find.byKey(const ValueKey('pdf_page_area'));
expect(pdfViewer, findsOneWidget);
// Scroll horizontally (might not change page for fitted PDF)
await tester.drag(pdfViewer, const Offset(-100, 0)); // Scroll left
await tester.pumpAndSettle();
// Verify horizontal scroll (page might stay the same for portrait PDF)
final afterHorizontalPage = container.read(pdfViewModelProvider);
expect(afterHorizontalPage, greaterThan(1));
});
}

View File

@ -12,12 +12,7 @@ class DocumentStateNotifier extends StateNotifier<Document> {
@visibleForTesting @visibleForTesting
void openSample() { void openSample() {
state = state.copyWith( state = state.copyWith(loaded: true, pageCount: 5, placementsByPage: {});
loaded: true,
pageCount: 5,
currentPage: 1,
placementsByPage: {},
);
} }
void openPicked({ void openPicked({
@ -28,23 +23,20 @@ class DocumentStateNotifier extends StateNotifier<Document> {
state = state.copyWith( state = state.copyWith(
loaded: true, loaded: true,
pageCount: pageCount, pageCount: pageCount,
currentPage: 1,
pickedPdfBytes: bytes, pickedPdfBytes: bytes,
placementsByPage: {}, placementsByPage: {},
); );
} }
void jumpTo(int page) {
if (!state.loaded) return;
final clamped = page.clamp(1, state.pageCount);
state = state.copyWith(currentPage: clamped);
}
void setPageCount(int count) { void setPageCount(int count) {
if (!state.loaded) return; if (!state.loaded) return;
state = state.copyWith(pageCount: count.clamp(1, 9999)); state = state.copyWith(pageCount: count.clamp(1, 9999));
} }
void jumpTo(int page) {
// currentPage is now in view model, so jumpTo does nothing here
}
// Multiple-signature helpers (rects are stored in normalized fractions 0..1 // Multiple-signature helpers (rects are stored in normalized fractions 0..1
// relative to the page size: left/top/width/height are all 0..1) // relative to the page size: left/top/width/height are all 0..1)
void addPlacement({ void addPlacement({
@ -52,6 +44,7 @@ class DocumentStateNotifier extends StateNotifier<Document> {
required Rect rect, required Rect rect,
SignatureAsset? asset, SignatureAsset? asset,
double rotationDeg = 0.0, double rotationDeg = 0.0,
GraphicAdjust? graphicAdjust,
}) { }) {
if (!state.loaded) return; if (!state.loaded) return;
final p = page.clamp(1, state.pageCount); final p = page.clamp(1, state.pageCount);
@ -62,6 +55,7 @@ class DocumentStateNotifier extends StateNotifier<Document> {
rect: rect, rect: rect,
asset: asset ?? SignatureAsset(bytes: Uint8List(0)), asset: asset ?? SignatureAsset(bytes: Uint8List(0)),
rotationDeg: rotationDeg, rotationDeg: rotationDeg,
graphicAdjust: graphicAdjust ?? const GraphicAdjust(),
), ),
); );
map[p] = list; map[p] = list;

View File

@ -5,34 +5,29 @@ import 'signature_placement.dart';
class Document { class Document {
final bool loaded; final bool loaded;
final int pageCount; final int pageCount;
final int currentPage;
final Uint8List? pickedPdfBytes; final Uint8List? pickedPdfBytes;
// Multiple signature placements per page, each combines geometry and asset. // Multiple signature placements per page, each combines geometry and asset.
final Map<int, List<SignaturePlacement>> placementsByPage; final Map<int, List<SignaturePlacement>> placementsByPage;
const Document({ const Document({
required this.loaded, required this.loaded,
required this.pageCount, required this.pageCount,
required this.currentPage,
this.pickedPdfBytes, this.pickedPdfBytes,
this.placementsByPage = const {}, this.placementsByPage = const {},
}); });
factory Document.initial() => const Document( factory Document.initial() => const Document(
loaded: false, loaded: false,
pageCount: 0, pageCount: 0,
currentPage: 1,
pickedPdfBytes: null, pickedPdfBytes: null,
placementsByPage: {}, placementsByPage: {},
); );
Document copyWith({ Document copyWith({
bool? loaded, bool? loaded,
int? pageCount, int? pageCount,
int? currentPage,
Uint8List? pickedPdfBytes, Uint8List? pickedPdfBytes,
Map<int, List<SignaturePlacement>>? placementsByPage, Map<int, List<SignaturePlacement>>? placementsByPage,
}) => Document( }) => Document(
loaded: loaded ?? this.loaded, loaded: loaded ?? this.loaded,
pageCount: pageCount ?? this.pageCount, pageCount: pageCount ?? this.pageCount,
currentPage: currentPage ?? this.currentPage,
pickedPdfBytes: pickedPdfBytes ?? this.pickedPdfBytes, pickedPdfBytes: pickedPdfBytes ?? this.pickedPdfBytes,
placementsByPage: placementsByPage ?? this.placementsByPage, placementsByPage: placementsByPage ?? this.placementsByPage,
); );

View File

@ -6,15 +6,15 @@ 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:pdfrx/pdfrx.dart'; import 'package:pdfrx/pdfrx.dart';
class PdfViewModel { class PdfViewModel extends StateNotifier<int> {
final Ref ref; final Ref ref;
PdfViewModel(this.ref); PdfViewModel(this.ref) : super(1);
Document get document => ref.read(documentRepositoryProvider); Document get document => ref.read(documentRepositoryProvider);
void jumpToPage(int page) { void jumpToPage(int page) {
ref.read(documentRepositoryProvider.notifier).jumpTo(page); state = page.clamp(1, document.pageCount);
} }
Future<void> openPdf({required String path, Uint8List? bytes}) async { Future<void> openPdf({required String path, Uint8List? bytes}) async {
@ -31,6 +31,7 @@ class PdfViewModel {
.read(documentRepositoryProvider.notifier) .read(documentRepositoryProvider.notifier)
.openPicked(path: path, pageCount: pageCount, bytes: bytes); .openPicked(path: path, pageCount: pageCount, bytes: bytes);
ref.read(signatureCardRepositoryProvider.notifier).clearAll(); ref.read(signatureCardRepositoryProvider.notifier).clearAll();
state = 1; // Reset current page to 1
} }
Future<Uint8List?> loadSignatureFromFile() async { Future<Uint8List?> loadSignatureFromFile() async {
@ -60,6 +61,6 @@ class PdfViewModel {
} }
} }
final pdfViewModelProvider = Provider<PdfViewModel>((ref) { final pdfViewModelProvider = StateNotifierProvider<PdfViewModel, int>((ref) {
return PdfViewModel(ref); return PdfViewModel(ref);
}); });

View File

@ -59,7 +59,6 @@ class _PdfMockContinuousListState extends ConsumerState<PdfMockContinuousList> {
final clearPending = widget.clearPending; final clearPending = widget.clearPending;
final visible = ref.watch(signatureVisibilityProvider); final visible = ref.watch(signatureVisibilityProvider);
final assets = ref.watch(signatureAssetRepositoryProvider); final assets = ref.watch(signatureAssetRepositoryProvider);
final aspectLocked = ref.watch(aspectLockedProvider);
if (pendingPage != null) { if (pendingPage != null) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
final p = pendingPage; final p = pendingPage;
@ -118,6 +117,7 @@ class _PdfMockContinuousListState extends ConsumerState<PdfMockContinuousList> {
rect: rect, rect: rect,
asset: dragData.card?.asset, asset: dragData.card?.asset,
rotationDeg: dragData.card?.rotationDeg ?? 0.0, rotationDeg: dragData.card?.rotationDeg ?? 0.0,
graphicAdjust: dragData.card?.graphicAdjust,
); );
} }
}, },
@ -195,35 +195,7 @@ class _PdfMockContinuousListState extends ConsumerState<PdfMockContinuousList> {
height: height, height: height,
child: GestureDetector( child: GestureDetector(
key: const Key('signature_overlay'), key: const Key('signature_overlay'),
onPanUpdate: (d) { // Removed onPanUpdate to allow scrolling
final dx =
d.delta.dx /
constraints.maxWidth;
final dy =
d.delta.dy /
constraints.maxHeight;
setState(() {
double l = (_activeRect.left + dx)
.clamp(0.0, 1.0);
double t = (_activeRect.top + dy)
.clamp(0.0, 1.0);
// clamp so it stays within page
l = l.clamp(
0.0,
1.0 - _activeRect.width,
);
t = t.clamp(
0.0,
1.0 - _activeRect.height,
);
_activeRect = Rect.fromLTWH(
l,
t,
_activeRect.width,
_activeRect.height,
);
});
},
child: DecoratedBox( child: DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all( border: Border.all(
@ -243,48 +215,7 @@ class _PdfMockContinuousListState extends ConsumerState<PdfMockContinuousList> {
height: 14, height: 14,
child: GestureDetector( child: GestureDetector(
key: const Key('signature_handle'), key: const Key('signature_handle'),
onPanUpdate: (d) { // Removed onPanUpdate to allow scrolling
final dx =
d.delta.dx /
constraints.maxWidth;
final dy =
d.delta.dy /
constraints.maxHeight;
setState(() {
double newW = (_activeRect.width +
dx)
.clamp(0.05, 1.0);
double newH =
(_activeRect.height + dy)
.clamp(0.05, 1.0);
if (aspectLocked) {
final ratio =
_activeRect.width /
_activeRect.height;
// keep ratio; prefer width change driving height
newH = (newW /
(ratio == 0
? 1
: ratio))
.clamp(0.05, 1.0);
}
// clamp to page bounds
newW = newW.clamp(
0.05,
1.0 - _activeRect.left,
);
newH = newH.clamp(
0.05,
1.0 - _activeRect.top,
);
_activeRect = Rect.fromLTWH(
_activeRect.left,
_activeRect.top,
newW,
newH,
);
});
},
child: DecoratedBox( child: DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,

View File

@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../domain/models/model.dart'; import '../../../../domain/models/model.dart';
import 'package:pdf_signature/data/repositories/document_repository.dart'; import 'package:pdf_signature/data/repositories/document_repository.dart';
import 'signature_overlay.dart'; import 'signature_overlay.dart';
import 'pdf_providers.dart';
/// Builds all overlays for a given page: placed signatures and the active one. /// Builds all overlays for a given page: placed signatures and the active one.
class PdfPageOverlays extends ConsumerWidget { class PdfPageOverlays extends ConsumerWidget {
@ -47,6 +48,42 @@ class PdfPageOverlays extends ConsumerWidget {
); );
} }
// Add active overlay if present and not using mock (mock has its own)
final activeRect = ref.watch(activeRectProvider);
final useMock = ref.watch(useMockViewerProvider);
if (!useMock && activeRect != null) {
widgets.add(
LayoutBuilder(
builder: (context, constraints) {
final left = activeRect.left * constraints.maxWidth;
final top = activeRect.top * constraints.maxHeight;
final width = activeRect.width * constraints.maxWidth;
final height = activeRect.height * constraints.maxHeight;
return Stack(
children: [
Positioned(
left: left,
top: top,
width: width,
height: height,
child: GestureDetector(
key: const Key('signature_overlay'),
// Removed onPanUpdate to allow scrolling
child: DecoratedBox(
decoration: BoxDecoration(
border: Border.all(color: Colors.red, width: 2),
),
child: const SizedBox.expand(),
),
),
),
],
);
},
),
);
}
return Stack(children: widgets); return Stack(children: widgets);
} }
} }

View File

@ -7,6 +7,7 @@ import 'pdf_page_overlays.dart';
import 'pdf_providers.dart'; import 'pdf_providers.dart';
import './pdf_mock_continuous_list.dart'; import './pdf_mock_continuous_list.dart';
import '../../signature/widgets/signature_drag_data.dart'; import '../../signature/widgets/signature_drag_data.dart';
import '../view_model/pdf_view_model.dart';
class PdfViewerWidget extends ConsumerStatefulWidget { class PdfViewerWidget extends ConsumerStatefulWidget {
const PdfViewerWidget({ const PdfViewerWidget({
@ -38,6 +39,9 @@ class _PdfViewerWidgetState extends ConsumerState<PdfViewerWidget> {
PdfViewerController? _controller; PdfViewerController? _controller;
PdfDocumentRef? _documentRef; PdfDocumentRef? _documentRef;
// Public getter for testing the actual viewer page
int? get viewerCurrentPage => _controller?.pageNumber;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -54,6 +58,8 @@ class _PdfViewerWidgetState extends ConsumerState<PdfViewerWidget> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final document = ref.watch(documentRepositoryProvider); final document = ref.watch(documentRepositoryProvider);
final useMock = ref.watch(useMockViewerProvider); final useMock = ref.watch(useMockViewerProvider);
final activeRect = ref.watch(activeRectProvider);
final currentPage = ref.watch(pdfViewModelProvider);
// Update document ref when document changes // Update document ref when document changes
if (document.loaded && document.pickedPdfBytes != null) { if (document.loaded && document.pickedPdfBytes != null) {
@ -109,9 +115,9 @@ class _PdfViewerWidgetState extends ConsumerState<PdfViewerWidget> {
.setPageCount(document.pages.length); .setPageCount(document.pages.length);
}, },
onPageChanged: (page) { onPageChanged: (page) {
// Update current page in repository // Update current page in view model
if (page != null) { if (page != null) {
ref.read(documentRepositoryProvider.notifier).jumpTo(page); ref.read(pdfViewModelProvider.notifier).jumpToPage(page);
} }
}, },
), ),
@ -125,8 +131,7 @@ class _PdfViewerWidgetState extends ConsumerState<PdfViewerWidget> {
// For real PDF viewer, we need to calculate which page was dropped on // For real PDF viewer, we need to calculate which page was dropped on
// This is a simplified implementation - in a real app you'd need to // This is a simplified implementation - in a real app you'd need to
// determine the exact page and position within that page // determine the exact page and position within that page
final currentPage = final currentPage = ref.read(pdfViewModelProvider);
ref.read(documentRepositoryProvider).currentPage;
// Create a default rect for the signature (can be adjusted later) // Create a default rect for the signature (can be adjusted later)
final rect = const Rect.fromLTWH(0.1, 0.1, 0.2, 0.1); final rect = const Rect.fromLTWH(0.1, 0.1, 0.2, 0.1);
@ -139,6 +144,7 @@ class _PdfViewerWidgetState extends ConsumerState<PdfViewerWidget> {
rect: rect, rect: rect,
asset: dragData.card?.asset, asset: dragData.card?.asset,
rotationDeg: dragData.card?.rotationDeg ?? 0.0, rotationDeg: dragData.card?.rotationDeg ?? 0.0,
graphicAdjust: dragData.card?.graphicAdjust,
); );
}, },
builder: (context, candidateData, rejectedData) { builder: (context, candidateData, rejectedData) {
@ -163,7 +169,7 @@ class _PdfViewerWidgetState extends ConsumerState<PdfViewerWidget> {
// to handle overlays for each page properly // to handle overlays for each page properly
return PdfPageOverlays( return PdfPageOverlays(
pageSize: widget.pageSize, pageSize: widget.pageSize,
pageNumber: document.currentPage, pageNumber: ref.watch(pdfViewModelProvider),
onDragSignature: widget.onDragSignature, onDragSignature: widget.onDragSignature,
onResizeSignature: widget.onResizeSignature, onResizeSignature: widget.onResizeSignature,
onConfirmSignature: widget.onConfirmSignature, onConfirmSignature: widget.onConfirmSignature,

View File

@ -5,8 +5,10 @@ import 'package:pdf_signature/l10n/app_localizations.dart';
// No direct model construction needed here // No direct model construction needed here
import 'package:pdf_signature/data/repositories/signature_asset_repository.dart'; import 'package:pdf_signature/data/repositories/signature_asset_repository.dart';
import 'package:pdf_signature/data/repositories/signature_card_repository.dart';
import 'image_editor_dialog.dart'; import 'image_editor_dialog.dart';
import '../../signature/widgets/signature_card.dart'; import '../../signature/widgets/signature_card.dart';
import 'pdf_providers.dart';
/// Data for drag-and-drop is in signature_drag_data.dart /// Data for drag-and-drop is in signature_drag_data.dart
@ -32,7 +34,7 @@ class _SignatureDrawerState extends ConsumerState<SignatureDrawer> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l = AppLocalizations.of(context); final l = AppLocalizations.of(context);
final library = ref.watch(signatureAssetRepositoryProvider); final library = ref.watch(signatureCardRepositoryProvider);
// Exporting flag lives in ui_services; keep drawer interactive regardless here. // Exporting flag lives in ui_services; keep drawer interactive regardless here.
final isExporting = false; final isExporting = false;
final disabled = widget.disabled || isExporting; final disabled = widget.disabled || isExporting;
@ -41,20 +43,21 @@ class _SignatureDrawerState extends ConsumerState<SignatureDrawer> {
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
if (library.isNotEmpty) ...[ if (library.isNotEmpty) ...[
for (final a in library) ...[ for (final card in library) ...[
Card( Card(
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
child: Padding( child: Padding(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
child: SignatureCard( child: SignatureCard(
key: ValueKey('sig_card_${library.indexOf(a)}'), key: ValueKey('sig_card_${library.indexOf(card)}'),
asset: a, asset: card.asset,
rotationDeg: 0.0, rotationDeg: card.rotationDeg,
graphicAdjust: card.graphicAdjust,
disabled: disabled, disabled: disabled,
onDelete: onDelete:
() => ref () => ref
.read(signatureAssetRepositoryProvider.notifier) .read(signatureCardRepositoryProvider.notifier)
.remove(a), .remove(card),
onAdjust: () async { onAdjust: () async {
if (!mounted) return; if (!mounted) return;
await showDialog( await showDialog(
@ -62,7 +65,11 @@ class _SignatureDrawerState extends ConsumerState<SignatureDrawer> {
builder: (_) => const ImageEditorDialog(), builder: (_) => const ImageEditorDialog(),
); );
}, },
onTap: () {}, onTap: () {
ref
.read(activeRectProvider.notifier)
.state = const Rect.fromLTWH(0.2, 0.2, 0.3, 0.15);
},
), ),
), ),
), ),

View File

@ -14,6 +14,7 @@ class SignatureCard extends StatelessWidget {
this.onAdjust, this.onAdjust,
this.useCurrentBytesForDrag = false, this.useCurrentBytesForDrag = false,
this.rotationDeg = 0.0, this.rotationDeg = 0.0,
this.graphicAdjust = const domain.GraphicAdjust(),
}); });
final domain.SignatureAsset asset; final domain.SignatureAsset asset;
final bool disabled; final bool disabled;
@ -22,6 +23,7 @@ class SignatureCard extends StatelessWidget {
final VoidCallback? onAdjust; final VoidCallback? onAdjust;
final bool useCurrentBytesForDrag; final bool useCurrentBytesForDrag;
final double rotationDeg; final double rotationDeg;
final domain.GraphicAdjust graphicAdjust;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -146,6 +148,7 @@ class SignatureCard extends StatelessWidget {
card: domain.SignatureCard( card: domain.SignatureCard(
asset: asset, asset: asset,
rotationDeg: rotationDeg, rotationDeg: rotationDeg,
graphicAdjust: graphicAdjust,
), ),
), ),
feedback: Opacity( feedback: Opacity(

View File

@ -1,11 +1,11 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/data/repositories/document_repository.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart';
import '_world.dart'; import '_world.dart';
/// Usage: the first page is displayed /// Usage: the first page is displayed
Future<void> theFirstPageIsDisplayed(WidgetTester tester) async { Future<void> theFirstPageIsDisplayed(WidgetTester tester) async {
final container = TestWorld.container ?? ProviderContainer(); final container = TestWorld.container ?? ProviderContainer();
final pdf = container.read(documentRepositoryProvider); final currentPage = container.read(pdfViewModelProvider);
expect(pdf.currentPage, 1); expect(currentPage, 1);
} }

View File

@ -1,7 +1,7 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; 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 '_world.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart';
/// Usage: the signature placement rotates around its center in real time /// Usage: the signature placement rotates around its center in real time
Future<void> theSignaturePlacementRotatesAroundItsCenterInRealTime( Future<void> theSignaturePlacementRotatesAroundItsCenterInRealTime(
@ -9,8 +9,9 @@ Future<void> theSignaturePlacementRotatesAroundItsCenterInRealTime(
) async { ) async {
final container = TestWorld.container ?? ProviderContainer(); final container = TestWorld.container ?? ProviderContainer();
final pdf = container.read(documentRepositoryProvider); final pdf = container.read(documentRepositoryProvider);
final currentPage = container.read(pdfViewModelProvider);
final placements = pdf.placementsByPage[pdf.currentPage] ?? []; final placements = pdf.placementsByPage[currentPage] ?? [];
if (placements.isNotEmpty) { if (placements.isNotEmpty) {
final placement = placements[0]; final placement = placements[0];
expect(placement.rotationDeg, 45.0); expect(placement.rotationDeg, 45.0);

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; 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 '_world.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart';
/// Usage: the user drags handles to resize and drags to reposition /// Usage: the user drags handles to resize and drags to reposition
Future<void> theUserDragsHandlesToResizeAndDragsToReposition( Future<void> theUserDragsHandlesToResizeAndDragsToReposition(
@ -12,8 +12,9 @@ Future<void> theUserDragsHandlesToResizeAndDragsToReposition(
TestWorld.container = container; TestWorld.container = container;
final pdf = container.read(documentRepositoryProvider); final pdf = container.read(documentRepositoryProvider);
final pdfN = container.read(documentRepositoryProvider.notifier); final pdfN = container.read(documentRepositoryProvider.notifier);
final currentPage = container.read(pdfViewModelProvider);
final placements = pdfN.placementsOn(pdf.currentPage); final placements = pdfN.placementsOn(currentPage);
if (placements.isNotEmpty) { if (placements.isNotEmpty) {
final currentRect = placements[0].rect; final currentRect = placements[0].rect;
TestWorld.prevCenter = currentRect.center; TestWorld.prevCenter = currentRect.center;
@ -25,6 +26,6 @@ Future<void> theUserDragsHandlesToResizeAndDragsToReposition(
height: currentRect.height + 30, height: currentRect.height + 30,
); );
pdfN.updatePlacementRect(page: pdf.currentPage, index: 0, rect: newRect); pdfN.updatePlacementRect(page: currentPage, index: 0, rect: newRect);
} }
} }

View File

@ -55,5 +55,6 @@ theUserDragsThisSignatureCardOnThePageOfTheDocumentToPlaceASignaturePlacement(
rect: Rect.fromLTWH(100, 100, 100, 50), rect: Rect.fromLTWH(100, 100, 100, 50),
asset: drop_card.asset, asset: drop_card.asset,
rotationDeg: drop_card.rotationDeg, rotationDeg: drop_card.rotationDeg,
graphicAdjust: drop_card.graphicAdjust,
); );
} }

View File

@ -5,8 +5,7 @@ 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_asset_repository.dart'; import 'package:pdf_signature/data/repositories/signature_asset_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/ui/features/pdf/view_model/pdf_view_model.dart';
import '_world.dart';
/// Usage: three signature placements are placed on the current page /// Usage: three signature placements are placed on the current page
Future<void> threeSignaturePlacementsArePlacedOnTheCurrentPage( Future<void> threeSignaturePlacementsArePlacedOnTheCurrentPage(
@ -24,8 +23,7 @@ Future<void> threeSignaturePlacementsArePlacedOnTheCurrentPage(
.read(documentRepositoryProvider.notifier) .read(documentRepositoryProvider.notifier)
.openPicked(path: 'mock.pdf', pageCount: 5); .openPicked(path: 'mock.pdf', pageCount: 5);
final pdfN = container.read(documentRepositoryProvider.notifier); final pdfN = container.read(documentRepositoryProvider.notifier);
final pdf = container.read(documentRepositoryProvider); final page = container.read(pdfViewModelProvider);
final page = pdf.currentPage;
pdfN.addPlacement( pdfN.addPlacement(
page: page, page: page,
rect: Rect.fromLTWH(10, 10, 50, 50), rect: Rect.fromLTWH(10, 10, 50, 50),

View File

@ -9,6 +9,7 @@ import 'package:pdf_signature/ui/features/pdf/widgets/pdf_providers.dart';
import 'package:pdf_signature/ui/features/pdf/widgets/ui_services.dart'; import 'package:pdf_signature/ui/features/pdf/widgets/ui_services.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_asset_repository.dart'; import 'package:pdf_signature/data/repositories/signature_asset_repository.dart';
import 'package:pdf_signature/data/repositories/signature_card_repository.dart';
import 'package:pdf_signature/domain/models/signature_asset.dart'; import 'package:pdf_signature/domain/models/signature_asset.dart';
import 'package:pdf_signature/l10n/app_localizations.dart'; import 'package:pdf_signature/l10n/app_localizations.dart';
@ -377,6 +378,15 @@ Future<void> pumpWithOpenPdfAndSig(WidgetTester tester) async {
repo.add(Uint8List.fromList(bytes), name: 'test'); repo.add(Uint8List.fromList(bytes), name: 'test');
return repo; return repo;
}), }),
signatureCardRepositoryProvider.overrideWith((ref) {
final cardRepo = SignatureCardStateNotifier();
final asset = SignatureAsset(
bytes: Uint8List.fromList(bytes),
name: 'test',
);
cardRepo.addWithAsset(asset, 0.0);
return cardRepo;
}),
// In new model, interactive overlay not implemented; keep library empty // In new model, interactive overlay not implemented; keep library empty
useMockViewerProvider.overrideWithValue(true), useMockViewerProvider.overrideWithValue(true),
exportingProvider.overrideWith((ref) => false), exportingProvider.overrideWith((ref) => false),

View File

@ -12,11 +12,7 @@ import 'package:pdf_signature/l10n/app_localizations.dart';
class _TestPdfController extends DocumentStateNotifier { class _TestPdfController extends DocumentStateNotifier {
_TestPdfController() : super() { _TestPdfController() : super() {
// Start with a loaded multi-page doc, page 1 of 5 // Start with a loaded multi-page doc, page 1 of 5
state = Document.initial().copyWith( state = Document.initial().copyWith(loaded: true, pageCount: 5);
loaded: true,
pageCount: 5,
currentPage: 1,
);
} }
} }

View File

@ -11,11 +11,7 @@ import 'package:pdf_signature/domain/models/model.dart';
class _TestPdfController extends DocumentStateNotifier { class _TestPdfController extends DocumentStateNotifier {
_TestPdfController() : super() { _TestPdfController() : super() {
state = Document.initial().copyWith( state = Document.initial().copyWith(loaded: true, pageCount: 6);
loaded: true,
pageCount: 6,
currentPage: 1,
);
} }
} }

View File

@ -13,11 +13,7 @@ import 'package:pdf_signature/domain/models/model.dart';
class _TestPdfController extends DocumentStateNotifier { class _TestPdfController extends DocumentStateNotifier {
_TestPdfController() : super() { _TestPdfController() : super() {
state = Document.initial().copyWith( state = Document.initial().copyWith(loaded: true, pageCount: 6);
loaded: true,
pageCount: 6,
currentPage: 1,
);
} }
} }