diff --git a/docs/NFRs.md b/docs/NFRs.md index 599f805..427e2f2 100644 --- a/docs/NFRs.md +++ b/docs/NFRs.md @@ -3,3 +3,4 @@ * support multiple platforms (windows, linux, android, web) * only FOSS libs can use * should not exceed 350 lines of code per file +* Direct Passing is better than Singleton(e.g.Provider) especially for `view`, `viewModel`. diff --git a/integration_test/export_flow_test.dart b/integration_test/export_flow_test.dart index b44224c..f2222c8 100644 --- a/integration_test/export_flow_test.dart +++ b/integration_test/export_flow_test.dart @@ -5,6 +5,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:image/image.dart' as img; import 'dart:io'; +import 'package:file_selector/file_selector.dart' as fs; import 'package:pdf_signature/data/services/export_service.dart'; @@ -13,7 +14,6 @@ import 'package:pdf_signature/data/repositories/signature_card_repository.dart'; import 'package:pdf_signature/data/repositories/document_repository.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/view_model/pdf_providers.dart'; import 'package:pdf_signature/ui/features/pdf/widgets/ui_services.dart'; import 'package:pdf_signature/ui/features/pdf/widgets/pages_sidebar.dart'; import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart'; @@ -50,17 +50,23 @@ void main() { documentRepositoryProvider.overrideWith( (ref) => DocumentStateNotifier()..openPicked(pageCount: 3), ), - useMockViewerProvider.overrideWith((ref) => false), + pdfViewModelProvider.overrideWith( + (ref) => PdfViewModel(ref, useMockViewer: false), + ), exportServiceProvider.overrideWith((_) => fake), savePathPickerProvider.overrideWith( (_) => () async => 'C:/tmp/output.pdf', ), ], - child: const MaterialApp( + child: MaterialApp( localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, locale: Locale('en'), - home: PdfSignatureHomePage(), + home: PdfSignatureHomePage( + onPickPdf: () async {}, + onClosePdf: () {}, + currentFile: fs.XFile('test.pdf'), + ), ), ), ); @@ -120,13 +126,19 @@ void main() { cardRepo.addWithAsset(asset, 0.0); return cardRepo; }), - useMockViewerProvider.overrideWithValue(false), + pdfViewModelProvider.overrideWith( + (ref) => PdfViewModel(ref, useMockViewer: false), + ), ], - child: const MaterialApp( + child: MaterialApp( localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, locale: Locale('en'), - home: PdfSignatureHomePage(), + home: PdfSignatureHomePage( + onPickPdf: () async {}, + onClosePdf: () {}, + currentFile: fs.XFile('test.pdf'), + ), ), ), ); @@ -145,17 +157,18 @@ void main() { // Programmatically simulate confirm: add placement with current rect and bound image, then clear active overlay. final ctx = tester.element(find.byType(PdfSignatureHomePage)); final container = ProviderScope.containerOf(ctx); - final r = container.read(activeRectProvider)!; + final r = container.read(pdfViewModelProvider).activeRect!; final lib = container.read(signatureAssetRepositoryProvider); final asset = lib.isNotEmpty ? lib.first : null; - final currentPage = container.read(pdfViewModelProvider); + final currentPage = container.read(pdfViewModelProvider).currentPage; container .read(documentRepositoryProvider.notifier) .addPlacement(page: currentPage, rect: r, asset: asset); // Clear active overlay by hiding signatures temporarily - container.read(signatureVisibilityProvider.notifier).state = false; + // Note: signatureVisibilityProvider was removed in migration + // container.read(signatureVisibilityProvider.notifier).state = false; await tester.pump(); - container.read(signatureVisibilityProvider.notifier).state = true; + // container.read(signatureVisibilityProvider.notifier).state = true; await tester.pumpAndSettle(); final placed = find.byKey(const Key('placed_signature_0')); @@ -192,13 +205,19 @@ void main() { DocumentStateNotifier() ..openPicked(pageCount: 3, bytes: pdfBytes), ), - useMockViewerProvider.overrideWithValue(false), + pdfViewModelProvider.overrideWith( + (ref) => PdfViewModel(ref, useMockViewer: false), + ), ], - child: const MaterialApp( + child: MaterialApp( localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, locale: Locale('en'), - home: PdfSignatureHomePage(), + home: PdfSignatureHomePage( + onPickPdf: () async {}, + onClosePdf: () {}, + currentFile: fs.XFile('test.pdf'), + ), ), ), ); @@ -232,13 +251,19 @@ void main() { DocumentStateNotifier() ..openPicked(pageCount: 3, bytes: pdfBytes), ), - useMockViewerProvider.overrideWithValue(false), + pdfViewModelProvider.overrideWith( + (ref) => PdfViewModel(ref, useMockViewer: false), + ), ], - child: const MaterialApp( + child: MaterialApp( localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, locale: Locale('en'), - home: PdfSignatureHomePage(), + home: PdfSignatureHomePage( + onPickPdf: () async {}, + onClosePdf: () {}, + currentFile: fs.XFile('test.pdf'), + ), ), ), ); @@ -275,13 +300,19 @@ void main() { DocumentStateNotifier() ..openPicked(pageCount: 3, bytes: pdfBytes), ), - useMockViewerProvider.overrideWithValue(false), + pdfViewModelProvider.overrideWith( + (ref) => PdfViewModel(ref, useMockViewer: false), + ), ], - child: const MaterialApp( + child: MaterialApp( localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, locale: Locale('en'), - home: PdfSignatureHomePage(), + home: PdfSignatureHomePage( + onPickPdf: () async {}, + onClosePdf: () {}, + currentFile: fs.XFile('test.pdf'), + ), ), ), ); @@ -321,13 +352,19 @@ void main() { DocumentStateNotifier() ..openPicked(pageCount: 3, bytes: pdfBytes), ), - useMockViewerProvider.overrideWithValue(false), + pdfViewModelProvider.overrideWith( + (ref) => PdfViewModel(ref, useMockViewer: false), + ), ], - child: const MaterialApp( + child: MaterialApp( localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, locale: Locale('en'), - home: PdfSignatureHomePage(), + home: PdfSignatureHomePage( + onPickPdf: () async {}, + onClosePdf: () {}, + currentFile: fs.XFile('test.pdf'), + ), ), ), ); diff --git a/integration_test/pdf_view_test.dart b/integration_test/pdf_view_test.dart index a5c9be1..438bc81 100644 --- a/integration_test/pdf_view_test.dart +++ b/integration_test/pdf_view_test.dart @@ -4,9 +4,9 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'dart:io'; import 'package:flutter/services.dart'; +import 'package:file_selector/file_selector.dart' as fs; import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart'; -import 'package:pdf_signature/ui/features/pdf/view_model/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'; @@ -37,13 +37,19 @@ void main() { DocumentStateNotifier() ..openPicked(pageCount: 3, bytes: pdfBytes), ), - useMockViewerProvider.overrideWithValue(false), + pdfViewModelProvider.overrideWith( + (ref) => PdfViewModel(ref, useMockViewer: false), + ), ], - child: const MaterialApp( + child: MaterialApp( localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, locale: Locale('en'), - home: PdfSignatureHomePage(), + home: PdfSignatureHomePage( + onPickPdf: () async {}, + onClosePdf: () {}, + currentFile: fs.XFile('test.pdf'), + ), ), ), ); @@ -85,13 +91,19 @@ void main() { DocumentStateNotifier() ..openPicked(pageCount: 3, bytes: pdfBytes), ), - useMockViewerProvider.overrideWithValue(false), + pdfViewModelProvider.overrideWith( + (ref) => PdfViewModel(ref, useMockViewer: false), + ), ], - child: const MaterialApp( + child: MaterialApp( localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, locale: Locale('en'), - home: PdfSignatureHomePage(), + home: PdfSignatureHomePage( + onPickPdf: () async {}, + onClosePdf: () {}, + currentFile: fs.XFile('test.pdf'), + ), ), ), ); @@ -133,13 +145,19 @@ void main() { DocumentStateNotifier() ..openPicked(pageCount: 3, bytes: pdfBytes), ), - useMockViewerProvider.overrideWithValue(false), + pdfViewModelProvider.overrideWith( + (ref) => PdfViewModel(ref, useMockViewer: false), + ), ], - child: const MaterialApp( + child: MaterialApp( localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, locale: Locale('en'), - home: PdfSignatureHomePage(), + home: PdfSignatureHomePage( + onPickPdf: () async {}, + onClosePdf: () {}, + currentFile: fs.XFile('test.pdf'), + ), ), ), ); @@ -182,13 +200,19 @@ void main() { DocumentStateNotifier() ..openPicked(pageCount: 3, bytes: pdfBytes), ), - useMockViewerProvider.overrideWithValue(false), + pdfViewModelProvider.overrideWith( + (ref) => PdfViewModel(ref, useMockViewer: false), + ), ], - child: const MaterialApp( + child: MaterialApp( localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, locale: Locale('en'), - home: PdfSignatureHomePage(), + home: PdfSignatureHomePage( + onPickPdf: () async {}, + onClosePdf: () {}, + currentFile: fs.XFile('test.pdf'), + ), ), ), ); diff --git a/lib/app.dart b/lib/app.dart index 7452577..b377ee2 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -2,11 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_localized_locales/flutter_localized_locales.dart'; import 'package:pdf_signature/l10n/app_localizations.dart'; -import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart'; -import 'package:pdf_signature/data/repositories/document_repository.dart'; -import 'package:pdf_signature/ui/features/welcome/widgets/welcome_screen.dart'; -import 'data/repositories/preferences_repository.dart'; +import 'package:pdf_signature/routing/router.dart'; import 'package:pdf_signature/ui/features/preferences/widgets/settings_screen.dart'; +import 'data/repositories/preferences_repository.dart'; class MyApp extends StatelessWidget { const MyApp({super.key}); @@ -42,7 +40,7 @@ class MyApp extends StatelessWidget { data: (_) { final themeMode = ref.watch(themeModeProvider); final appLocale = ref.watch(localeProvider); - return MaterialApp( + return MaterialApp.router( onGenerateTitle: (ctx) => AppLocalizations.of(ctx).appTitle, theme: ThemeData( colorScheme: ColorScheme.fromSeed( @@ -63,27 +61,27 @@ class MyApp extends StatelessWidget { ...AppLocalizations.localizationsDelegates, LocaleNamesLocalizationsDelegate(), ], - home: Builder( - builder: - (ctx) => Scaffold( - appBar: AppBar( - title: Text(AppLocalizations.of(ctx).appTitle), - actions: [ - OutlinedButton.icon( - key: const Key('btn_appbar_settings'), - icon: const Icon(Icons.settings), - label: Text(AppLocalizations.of(ctx).settings), - onPressed: - () => showDialog( - context: ctx, - builder: (_) => const SettingsDialog(), - ), - ), - ], + routerConfig: ref.watch(routerProvider), + builder: (context, child) { + return Scaffold( + appBar: AppBar( + title: Text(AppLocalizations.of(context).appTitle), + actions: [ + OutlinedButton.icon( + key: const Key('btn_appbar_settings'), + icon: const Icon(Icons.settings), + label: Text(AppLocalizations.of(context).settings), + onPressed: + () => showDialog( + context: context, + builder: (_) => const SettingsDialog(), + ), ), - body: const _RootHomeSwitcher(), - ), - ), + ], + ), + body: child, + ); + }, ); }, ); @@ -92,16 +90,3 @@ class MyApp extends StatelessWidget { ); } } - -class _RootHomeSwitcher extends ConsumerWidget { - const _RootHomeSwitcher(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final pdf = ref.watch(documentRepositoryProvider); - if (!pdf.loaded) { - return const WelcomeScreen(); - } - return const PdfSignatureHomePage(); - } -} diff --git a/lib/data/repositories/document_repository.dart b/lib/data/repositories/document_repository.dart index 225f08d..635cfec 100644 --- a/lib/data/repositories/document_repository.dart +++ b/lib/data/repositories/document_repository.dart @@ -15,10 +15,7 @@ class DocumentStateNotifier extends StateNotifier { state = state.copyWith(loaded: true, pageCount: 5, placementsByPage: {}); } - void openPicked({ - required int pageCount, - Uint8List? bytes, - }) { + void openPicked({required int pageCount, Uint8List? bytes}) { state = state.copyWith( loaded: true, pageCount: pageCount, @@ -27,6 +24,10 @@ class DocumentStateNotifier extends StateNotifier { ); } + void close() { + state = Document.initial(); + } + void setPageCount(int count) { if (!state.loaded) return; state = state.copyWith(pageCount: count.clamp(1, 9999)); diff --git a/lib/routing/router.dart b/lib/routing/router.dart new file mode 100644 index 0000000..fc455d2 --- /dev/null +++ b/lib/routing/router.dart @@ -0,0 +1,121 @@ +import 'dart:typed_data'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart'; +import 'package:pdf_signature/ui/features/welcome/widgets/welcome_screen.dart'; +import 'package:pdf_signature/data/repositories/document_repository.dart'; +import 'package:pdf_signature/data/repositories/signature_card_repository.dart'; +import 'package:file_selector/file_selector.dart' as fs; +import 'package:pdfrx/pdfrx.dart'; + +class PdfManager { + final DocumentStateNotifier _documentNotifier; + final SignatureCardStateNotifier _signatureCardNotifier; + final GoRouter _router; + + fs.XFile _currentFile = fs.XFile(''); + + PdfManager({ + required DocumentStateNotifier documentNotifier, + required SignatureCardStateNotifier signatureCardNotifier, + required GoRouter router, + }) : _documentNotifier = documentNotifier, + _signatureCardNotifier = signatureCardNotifier, + _router = router; + + fs.XFile get currentFile => _currentFile; + + Future openPdf({String? path, Uint8List? bytes}) async { + int pageCount = 1; // default + if (bytes != null) { + try { + final doc = await PdfDocument.openData(bytes); + pageCount = doc.pages.length; + } catch (_) { + // ignore + } + } + + // Update file reference if path is provided + if (path != null) { + _currentFile = fs.XFile(path); + } + + _documentNotifier.openPicked(pageCount: pageCount, bytes: bytes); + _signatureCardNotifier.clearAll(); + + // Navigate to PDF screen after successfully opening PDF + _router.go('/pdf'); + } + + void closePdf() { + _documentNotifier.close(); + _signatureCardNotifier.clearAll(); + _currentFile = fs.XFile(''); + + // Navigate back to welcome screen when closing PDF + _router.go('/'); + } + + Future pickAndOpenPdf() async { + final typeGroup = const fs.XTypeGroup(label: 'PDF', extensions: ['pdf']); + final file = await fs.openFile(acceptedTypeGroups: [typeGroup]); + if (file != null) { + Uint8List? bytes; + try { + bytes = await file.readAsBytes(); + } catch (_) { + bytes = null; + } + await openPdf(path: file.path, bytes: bytes); + } + } +} + +final routerProvider = Provider((ref) { + // Create PdfManager instance with dependencies + final documentNotifier = ref.read(documentRepositoryProvider.notifier); + final signatureCardNotifier = ref.read( + signatureCardRepositoryProvider.notifier, + ); + + // Create a late variable for the router + late final GoRouter router; + + // Create PdfManager with router dependency (will be set after router creation) + late final PdfManager pdfManager; + + router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: + (context, state) => WelcomeScreen( + onPickPdf: () => pdfManager.pickAndOpenPdf(), + onOpenPdf: + ({String? path, Uint8List? bytes, String? fileName}) => + pdfManager.openPdf(path: path, bytes: bytes), + ), + ), + GoRoute( + path: '/pdf', + builder: + (context, state) => PdfSignatureHomePage( + onPickPdf: () => pdfManager.pickAndOpenPdf(), + onClosePdf: () => pdfManager.closePdf(), + currentFile: pdfManager.currentFile, + ), + ), + ], + initialLocation: '/', + ); + + // Now create PdfManager with the router + pdfManager = PdfManager( + documentNotifier: documentNotifier, + signatureCardNotifier: signatureCardNotifier, + router: router, + ); + + return router; +}); diff --git a/lib/ui/features/pdf/view_model/pdf_providers.dart b/lib/ui/features/pdf/view_model/pdf_providers.dart deleted file mode 100644 index 3bfa648..0000000 --- a/lib/ui/features/pdf/view_model/pdf_providers.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:pdfrx/pdfrx.dart'; - -/// Whether to use a mock continuous viewer (ListView) instead of a real PDF viewer. -/// Tests will override this to true. -final useMockViewerProvider = Provider( - (ref) => const bool.fromEnvironment('FLUTTER_TEST', defaultValue: false), -); - -/// Global visibility toggle for signature overlays (placed items). Kept simple for tests. -final signatureVisibilityProvider = StateProvider((ref) => true); - -/// Whether resizing keeps the current aspect ratio for the active overlay -final aspectLockedProvider = StateProvider((ref) => false); - -/// Current active overlay rect (normalized 0..1) for the mock viewer. -/// Integration tests can read this to confirm or compute placements. -final activeRectProvider = StateProvider((ref) => null); - -/// Exposes the PdfViewerController so toolbar / thumbnails can invoke navigation. -/// It must be overridden at runtime by the hosting screen (e.g. `PdfSignatureHomePage`). -// Default controller (can be overridden by a screen to ensure a stable instance within its subtree). -final PdfViewerController _defaultPdfViewerController = PdfViewerController(); -final pdfViewerControllerProvider = Provider((ref) { - return _defaultPdfViewerController; -}); - -/// Current page (1-based). Updated by PdfViewer via onPageChanged. -final currentPageProvider = StateProvider((ref) => 1); diff --git a/lib/ui/features/pdf/view_model/pdf_view_model.dart b/lib/ui/features/pdf/view_model/pdf_view_model.dart index 457fc21..1c6ff87 100644 --- a/lib/ui/features/pdf/view_model/pdf_view_model.dart +++ b/lib/ui/features/pdf/view_model/pdf_view_model.dart @@ -6,15 +6,41 @@ import 'package:pdf_signature/data/repositories/signature_card_repository.dart'; import 'package:pdf_signature/domain/models/model.dart'; import 'package:pdfrx/pdfrx.dart'; -class PdfViewModel extends StateNotifier { +class PdfViewModel extends ChangeNotifier { final Ref ref; + PdfViewerController _controller = PdfViewerController(); + PdfViewerController get controller => _controller; + int _currentPage = 1; + late final bool _useMockViewer; - PdfViewModel(this.ref) : super(1); + // Active rect for signature placement overlay + Rect? _activeRect; + Rect? get activeRect => _activeRect; + set activeRect(Rect? value) { + _activeRect = value; + notifyListeners(); + } - Document get document => ref.read(documentRepositoryProvider); + // const bool.fromEnvironment('FLUTTER_TEST', defaultValue: false); + PdfViewModel(this.ref, {bool? useMockViewer}) + : _useMockViewer = + useMockViewer ?? + bool.fromEnvironment('FLUTTER_TEST', defaultValue: false); + + bool get useMockViewer => _useMockViewer; + + int get currentPage => _currentPage; + + set currentPage(int value) { + _currentPage = value.clamp(1, document.pageCount); + + notifyListeners(); + } + + Document get document => ref.watch(documentRepositoryProvider); void jumpToPage(int page) { - state = page.clamp(1, document.pageCount); + currentPage = page; } Future openPdf({required String path, Uint8List? bytes}) async { @@ -30,37 +56,125 @@ class PdfViewModel extends StateNotifier { ref .read(documentRepositoryProvider.notifier) .openPicked(pageCount: pageCount, bytes: bytes); + clearAllSignatureCards(); + + currentPage = 1; // Reset current page to 1 + } + + // Document repository methods + void closeDocument() { + ref.read(documentRepositoryProvider.notifier).close(); + } + + void setPageCount(int count) { + ref.read(documentRepositoryProvider.notifier).setPageCount(count); + } + + void addPlacement({ + required int page, + required Rect rect, + SignatureAsset? asset, + double rotationDeg = 0.0, + GraphicAdjust? graphicAdjust, + }) { + ref + .read(documentRepositoryProvider.notifier) + .addPlacement( + page: page, + rect: rect, + asset: asset, + rotationDeg: rotationDeg, + graphicAdjust: graphicAdjust, + ); + } + + void updatePlacementRotation({ + required int page, + required int index, + required double rotationDeg, + }) { + ref + .read(documentRepositoryProvider.notifier) + .updatePlacementRotation( + page: page, + index: index, + rotationDeg: rotationDeg, + ); + } + + void removePlacement({required int page, required int index}) { + ref + .read(documentRepositoryProvider.notifier) + .removePlacement(page: page, index: index); + } + + void updatePlacementRect({ + required int page, + required int index, + required Rect rect, + }) { + ref + .read(documentRepositoryProvider.notifier) + .updatePlacementRect(page: page, index: index, rect: rect); + } + + List placementsOn(int page) { + return ref.read(documentRepositoryProvider.notifier).placementsOn(page); + } + + SignatureAsset? assetOfPlacement({required int page, required int index}) { + return ref + .read(documentRepositoryProvider.notifier) + .assetOfPlacement(page: page, index: index); + } + + Future exportDocument({ + required String outputPath, + required Size uiPageSize, + required Uint8List? signatureImageBytes, + }) async { + await ref + .read(documentRepositoryProvider.notifier) + .exportDocument( + outputPath: outputPath, + uiPageSize: uiPageSize, + signatureImageBytes: signatureImageBytes, + ); + } + + // Signature card repository methods + List get signatureCards => + ref.read(signatureCardRepositoryProvider); + + void addSignatureCard(SignatureCard card) { + ref.read(signatureCardRepositoryProvider.notifier).add(card); + } + + void addSignatureCardWithAsset(SignatureAsset asset, double rotationDeg) { + ref + .read(signatureCardRepositoryProvider.notifier) + .addWithAsset(asset, rotationDeg); + } + + void updateSignatureCard( + SignatureCard card, + double? rotationDeg, + GraphicAdjust? graphicAdjust, + ) { + ref + .read(signatureCardRepositoryProvider.notifier) + .update(card, rotationDeg, graphicAdjust); + } + + void removeSignatureCard(SignatureCard card) { + ref.read(signatureCardRepositoryProvider.notifier).remove(card); + } + + void clearAllSignatureCards() { ref.read(signatureCardRepositoryProvider.notifier).clearAll(); - state = 1; // Reset current page to 1 - } - - Future loadSignatureFromFile() async { - // This would need file picker, but since it's UI logic, perhaps keep in widget - // For now, return null - return null; - } - - void confirmSignature() { - // Need to implement based on original logic - } - - void onDragSignature(Offset delta) { - // Implement drag - } - - void onResizeSignature(Offset delta) { - // Implement resize - } - - void onSelectPlaced(int? index) { - // ref.read(documentRepositoryProvider.notifier).selectPlacement(index); - } - - Future saveSignedPdf() async { - // Implement save logic } } -final pdfViewModelProvider = StateNotifierProvider((ref) { +final pdfViewModelProvider = ChangeNotifierProvider((ref) { return PdfViewModel(ref); }); diff --git a/lib/ui/features/pdf/widgets/pages_sidebar.dart b/lib/ui/features/pdf/widgets/pages_sidebar.dart index 8cc3380..a02d0ca 100644 --- a/lib/ui/features/pdf/widgets/pages_sidebar.dart +++ b/lib/ui/features/pdf/widgets/pages_sidebar.dart @@ -1,11 +1,112 @@ import 'package:flutter/material.dart'; -import 'thumbnails_view.dart'; +import 'package:pdfrx/pdfrx.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class ThumbnailsView extends ConsumerWidget { + const ThumbnailsView({ + super.key, + required this.documentRef, + required this.controller, + required this.currentPage, + }); + + final PdfDocumentRefData documentRef; + final PdfViewerController controller; + final int currentPage; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = Theme.of(context); + + return Container( + color: theme.colorScheme.surface, + child: PdfDocumentViewBuilder( + documentRef: documentRef, + builder: (context, document) { + final pageCount = document?.pages.length ?? 0; + return ListView.separated( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8), + itemCount: pageCount, + separatorBuilder: (_, _) => const SizedBox(height: 8), + itemBuilder: (context, index) { + final pageNumber = index + 1; + final isSelected = currentPage == pageNumber; + return InkWell( + onTap: () { + controller.goToPage( + pageNumber: pageNumber, + anchor: PdfPageAnchor.top, + ); + }, + child: DecoratedBox( + decoration: BoxDecoration( + color: + isSelected + ? theme.colorScheme.primaryContainer + : theme.cardColor, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: + isSelected + ? theme.colorScheme.primary + : theme.dividerColor, + ), + ), + child: Padding( + padding: const EdgeInsets.all(6), + child: Column( + children: [ + SizedBox( + height: 180, + child: ClipRRect( + borderRadius: BorderRadius.circular(4), + child: PdfPageView( + document: document, + pageNumber: pageNumber, + alignment: Alignment.center, + ), + ), + ), + const SizedBox(height: 4), + Text('$pageNumber', style: theme.textTheme.bodySmall), + ], + ), + ), + ), + ); + }, + ); + }, + ), + ); + } +} class PagesSidebar extends StatelessWidget { - const PagesSidebar({super.key}); + const PagesSidebar({ + super.key, + required this.documentRef, + required this.controller, + required this.currentPage, + }); + + final PdfDocumentRefData? documentRef; + final PdfViewerController controller; + final int currentPage; @override Widget build(BuildContext context) { - return Card(margin: EdgeInsets.zero, child: const ThumbnailsView()); + if (documentRef == null) { + return Card(margin: EdgeInsets.zero, child: const SizedBox.shrink()); + } + + return Card( + margin: EdgeInsets.zero, + child: ThumbnailsView( + documentRef: documentRef!, + controller: controller, + currentPage: currentPage, + ), + ); } } diff --git a/lib/ui/features/pdf/widgets/pdf_mock_continuous_list.dart b/lib/ui/features/pdf/widgets/pdf_mock_continuous_list.dart index 2fd7199..3fa5248 100644 --- a/lib/ui/features/pdf/widgets/pdf_mock_continuous_list.dart +++ b/lib/ui/features/pdf/widgets/pdf_mock_continuous_list.dart @@ -4,11 +4,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:pdf_signature/l10n/app_localizations.dart'; import 'pdf_page_overlays.dart'; -import '../view_model/pdf_providers.dart'; import 'package:pdf_signature/data/repositories/signature_asset_repository.dart'; // using only adjusted overlay, no direct model imports needed import '../../signature/widgets/signature_drag_data.dart'; -import 'package:pdf_signature/data/repositories/document_repository.dart'; +import '../view_model/pdf_view_model.dart'; /// Mocked continuous viewer for tests or platforms without real viewer. @visibleForTesting @@ -57,7 +56,6 @@ class _PdfMockContinuousListState extends ConsumerState { final pendingPage = widget.pendingPage; final scrollToPage = widget.scrollToPage; final clearPending = widget.clearPending; - final visible = ref.watch(signatureVisibilityProvider); final assets = ref.watch(signatureAssetRepositoryProvider); if (pendingPage != null) { WidgetsBinding.instance.addPostFrameCallback((_) { @@ -111,7 +109,7 @@ class _PdfMockContinuousListState extends ConsumerState { // Add placement to the document ref - .read(documentRepositoryProvider.notifier) + .read(pdfViewModelProvider.notifier) .addPlacement( page: pageNum, rect: rect, @@ -151,88 +149,75 @@ class _PdfMockContinuousListState extends ConsumerState { ); }, ), - visible - ? Stack( - children: [ - PdfPageOverlays( - pageSize: pageSize, - pageNumber: pageNum, - onDragSignature: widget.onDragSignature, - onResizeSignature: widget.onResizeSignature, - onConfirmSignature: widget.onConfirmSignature, - onClearActiveOverlay: widget.onClearActiveOverlay, - onSelectPlaced: widget.onSelectPlaced, - ), - // For tests expecting an active overlay, draw a mock - // overlay on page 1 when library has at least one asset - if (pageNum == 1 && assets.isNotEmpty) - 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; - // Publish rect for tests/other UI to observe - WidgetsBinding.instance.addPostFrameCallback(( - _, - ) { - if (!mounted) return; - ref - .read(activeRectProvider.notifier) - .state = _activeRect; - }); - 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(), + Stack( + children: [ + PdfPageOverlays( + pageSize: pageSize, + pageNumber: pageNum, + onDragSignature: widget.onDragSignature, + onResizeSignature: widget.onResizeSignature, + onConfirmSignature: widget.onConfirmSignature, + onClearActiveOverlay: widget.onClearActiveOverlay, + onSelectPlaced: widget.onSelectPlaced, + ), + // For tests expecting an active overlay, draw a mock + // overlay on page 1 when library has at least one asset + if (pageNum == 1 && assets.isNotEmpty) + 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; + // Publish rect for tests/other UI to observe + 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(), ), - // resize handle bottom-right - Positioned( - left: left + width - 14, - top: top + height - 14, - width: 14, - height: 14, - child: GestureDetector( - key: const Key('signature_handle'), - // Removed onPanUpdate to allow scrolling - child: DecoratedBox( - decoration: BoxDecoration( - color: Colors.white, - border: Border.all( - color: Colors.red, - ), - ), - ), + ), + ), + // resize handle bottom-right + Positioned( + left: left + width - 14, + top: top + height - 14, + width: 14, + height: 14, + child: GestureDetector( + key: const Key('signature_handle'), + // Removed onPanUpdate to allow scrolling + child: DecoratedBox( + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.red), ), ), - ], - ); - }, - ), - ], - ) - : const SizedBox.shrink(), + ), + ), + ], + ); + }, + ), + ], + ), ], ), ), diff --git a/lib/ui/features/pdf/widgets/pdf_page_area.dart b/lib/ui/features/pdf/widgets/pdf_page_area.dart index a333b05..9eccb9d 100644 --- a/lib/ui/features/pdf/widgets/pdf_page_area.dart +++ b/lib/ui/features/pdf/widgets/pdf_page_area.dart @@ -3,10 +3,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:pdf_signature/l10n/app_localizations.dart'; // Real viewer removed in migration; mock continuous list is used in tests. -import 'package:pdf_signature/data/repositories/document_repository.dart'; import 'pdf_viewer_widget.dart'; +import 'package:pdfrx/pdfrx.dart'; import '../view_model/pdf_view_model.dart'; -import '../view_model/pdf_providers.dart'; class PdfPageArea extends ConsumerStatefulWidget { const PdfPageArea({ @@ -17,6 +16,7 @@ class PdfPageArea extends ConsumerStatefulWidget { required this.onConfirmSignature, required this.onClearActiveOverlay, required this.onSelectPlaced, + required this.controller, }); final Size pageSize; @@ -26,6 +26,7 @@ class PdfPageArea extends ConsumerStatefulWidget { final VoidCallback onConfirmSignature; final VoidCallback onClearActiveOverlay; final ValueChanged onSelectPlaced; + final PdfViewerController controller; @override ConsumerState createState() => _PdfPageAreaState(); } @@ -117,20 +118,21 @@ class _PdfPageAreaState extends ConsumerState { @override Widget build(BuildContext context) { - final pdf = ref.watch(documentRepositoryProvider); + final pdfViewModel = ref.watch(pdfViewModelProvider); + final pdf = pdfViewModel.document; const pageViewMode = 'continuous'; // React to PdfViewModel (source of truth for current page) - ref.listen(pdfViewModelProvider, (prev, next) { - if (prev != next) { - _scrollToPage(next); + ref.listen(pdfViewModelProvider, (prev, next) { + if (prev?.currentPage != next.currentPage) { + _scrollToPage(next.currentPage); } }); // React to provider currentPage changes (e.g., user tapped overview) - ref.listen(currentPageProvider, (prev, next) { + ref.listen(pdfViewModelProvider, (prev, next) { if (_suppressProviderListen) return; - if (prev != next) { - final target = next; + if (prev?.currentPage != next.currentPage) { + final target = next.currentPage; // If we're already navigating to this target, ignore; otherwise allow new target. if (_programmaticTargetPage != null && _programmaticTargetPage == target) { @@ -159,7 +161,6 @@ class _PdfPageAreaState extends ConsumerState { // Use real PDF viewer if (isContinuous) { - final controller = ref.watch(pdfViewerControllerProvider); return PdfViewerWidget( pageSize: widget.pageSize, onDragSignature: widget.onDragSignature, @@ -169,7 +170,7 @@ class _PdfPageAreaState extends ConsumerState { onSelectPlaced: widget.onSelectPlaced, pageKeyBuilder: _pageKey, scrollToPage: _scrollToPage, - controller: controller, + controller: widget.controller, ); } return const SizedBox.shrink(); diff --git a/lib/ui/features/pdf/widgets/pdf_page_overlays.dart b/lib/ui/features/pdf/widgets/pdf_page_overlays.dart index 05b1af8..d8f3222 100644 --- a/lib/ui/features/pdf/widgets/pdf_page_overlays.dart +++ b/lib/ui/features/pdf/widgets/pdf_page_overlays.dart @@ -1,10 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart'; import '../../../../domain/models/model.dart'; -import 'package:pdf_signature/data/repositories/document_repository.dart'; import 'signature_overlay.dart'; -import '../view_model/pdf_providers.dart'; /// Builds all overlays for a given page: placed signatures and the active one. class PdfPageOverlays extends ConsumerWidget { @@ -29,9 +28,11 @@ class PdfPageOverlays extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final pdf = ref.watch(documentRepositoryProvider); + final pdfViewModel = ref.watch(pdfViewModelProvider); + final pdf = pdfViewModel.document; final placed = pdf.placementsByPage[pageNumber] ?? const []; + final activeRect = pdfViewModel.activeRect; final widgets = []; for (int i = 0; i < placed.length; i++) { @@ -48,9 +49,9 @@ 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); + // TODO:Add active overlay if present and not using mock (mock has its own) + + final useMock = pdfViewModel.useMockViewer; if (!useMock && activeRect != null) { widgets.add( LayoutBuilder( diff --git a/lib/ui/features/pdf/widgets/pdf_pages_overview.dart b/lib/ui/features/pdf/widgets/pdf_pages_overview.dart deleted file mode 100644 index 0473d32..0000000 --- a/lib/ui/features/pdf/widgets/pdf_pages_overview.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:pdfrx/pdfrx.dart'; -import 'package:pdf_signature/data/repositories/document_repository.dart'; -import '../view_model/pdf_providers.dart'; - -class PdfPagesOverview extends ConsumerWidget { - const PdfPagesOverview({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final pdf = ref.watch(documentRepositoryProvider); - final controller = ref.watch(pdfViewerControllerProvider); - final theme = Theme.of(context); - - if (!pdf.loaded || pdf.pickedPdfBytes == null) - return const SizedBox.shrink(); - - final documentRef = PdfDocumentRefData( - pdf.pickedPdfBytes!, - sourceName: 'document.pdf', - ); - - return Container( - color: theme.colorScheme.surface, - child: PdfDocumentViewBuilder( - documentRef: documentRef, - builder: (context, document) { - final pageCount = document?.pages.length ?? 0; - return ListView.separated( - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8), - itemCount: pageCount, - separatorBuilder: (_, _) => const SizedBox(height: 8), - itemBuilder: (context, index) { - final pageNumber = index + 1; - final isSelected = ref.watch(currentPageProvider) == pageNumber; - return InkWell( - onTap: () { - controller.goToPage( - pageNumber: pageNumber, - anchor: PdfPageAnchor.top, - ); - }, - child: DecoratedBox( - decoration: BoxDecoration( - color: - isSelected - ? theme.colorScheme.primaryContainer - : theme.cardColor, - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: - isSelected - ? theme.colorScheme.primary - : theme.dividerColor, - ), - ), - child: Padding( - padding: const EdgeInsets.all(6), - child: Column( - children: [ - SizedBox( - height: 180, - child: ClipRRect( - borderRadius: BorderRadius.circular(4), - child: PdfPageView( - document: document, - pageNumber: pageNumber, - alignment: Alignment.center, - ), - ), - ), - const SizedBox(height: 4), - Text('$pageNumber', style: theme.textTheme.bodySmall), - ], - ), - ), - ), - ); - }, - ); - }, - ), - ); - } -} diff --git a/lib/ui/features/pdf/widgets/pdf_screen.dart b/lib/ui/features/pdf/widgets/pdf_screen.dart index 69a2d1c..d758ae3 100644 --- a/lib/ui/features/pdf/widgets/pdf_screen.dart +++ b/lib/ui/features/pdf/widgets/pdf_screen.dart @@ -7,8 +7,6 @@ import 'package:pdf_signature/data/repositories/preferences_repository.dart'; import 'package:pdf_signature/l10n/app_localizations.dart'; import 'package:multi_split_view/multi_split_view.dart'; -import 'package:pdf_signature/data/repositories/document_repository.dart'; -import '../view_model/pdf_providers.dart'; import 'package:pdfrx/pdfrx.dart'; import 'draw_canvas.dart'; import 'pdf_toolbar.dart'; @@ -19,7 +17,16 @@ import 'ui_services.dart'; import '../view_model/pdf_view_model.dart'; class PdfSignatureHomePage extends ConsumerStatefulWidget { - const PdfSignatureHomePage({super.key}); + final Future Function() onPickPdf; + final VoidCallback onClosePdf; + final fs.XFile currentFile; + + const PdfSignatureHomePage({ + super.key, + required this.onPickPdf, + required this.onClosePdf, + required this.currentFile, + }); @override ConsumerState createState() => @@ -31,7 +38,6 @@ class _PdfSignatureHomePageState extends ConsumerState { bool _showPagesSidebar = true; bool _showSignaturesSidebar = true; int _zoomLevel = 100; // percentage for display only - fs.XFile _file = fs.XFile(''); // Split view controller to manage resizable sidebars without remounting the center area. late final MultiSplitViewController _splitController; @@ -43,6 +49,7 @@ class _PdfSignatureHomePageState extends ConsumerState { final double _pagesMax = 250; final double _signaturesMin = 140; final double _signaturesMax = 250; + late PdfViewModel _viewModel; // Exposed for tests to trigger the invalid-file SnackBar without UI. @visibleForTesting @@ -55,38 +62,17 @@ class _PdfSignatureHomePageState extends ConsumerState { } Future _pickPdf() async { - final typeGroup = const fs.XTypeGroup(label: 'PDF', extensions: ['pdf']); - final file = await fs.openFile(acceptedTypeGroups: [typeGroup]); - if (file != null) { - setState(() { - _file = file; - }); - Uint8List? bytes; - try { - bytes = await file.readAsBytes(); - } catch (_) { - bytes = null; - } - // infer page count if possible - int pageCount = 1; - if (bytes != null) { - try { - final doc = await PdfDocument.openData(bytes); - pageCount = doc.pages.length; - } catch (_) { - // ignore - } - } - ref - .read(documentRepositoryProvider.notifier) - .openPicked(pageCount: pageCount, bytes: bytes); - } + await widget.onPickPdf(); + } + + void _closePdf() { + widget.onClosePdf(); } void _jumpToPage(int page) { - final controller = ref.read(pdfViewerControllerProvider); - final current = ref.read(currentPageProvider); - final pdf = ref.read(documentRepositoryProvider); + final controller = _viewModel.controller; + final current = _viewModel.currentPage; + final pdf = _viewModel.document; int target; if (page == -1) { target = (current - 1).clamp(1, pdf.pageCount); @@ -95,10 +81,9 @@ class _PdfSignatureHomePageState extends ConsumerState { } // Update reactive page providers so UI/tests reflect navigation even if controller is a stub if (current != target) { - ref.read(currentPageProvider.notifier).state = target; // Also notify view model (if used elsewhere) via its public API try { - ref.read(pdfViewModelProvider.notifier).jumpToPage(target); + _viewModel.jumpToPage(target); } catch (_) { // ignore if provider not available } @@ -153,7 +138,7 @@ class _PdfSignatureHomePageState extends ConsumerState { Future _saveSignedPdf() async { ref.read(exportingProvider.notifier).state = true; try { - final pdf = ref.read(documentRepositoryProvider); + final pdf = _viewModel.document; final messenger = ScaffoldMessenger.of(context); if (!pdf.loaded) { messenger.showSnackBar( @@ -219,6 +204,7 @@ class _PdfSignatureHomePageState extends ConsumerState { void initState() { super.initState(); // Build areas once with builders; keep these instances stable. + _viewModel = ref.read(pdfViewModelProvider.notifier); _areas = [ Area( size: _lastPagesWidth, @@ -227,7 +213,26 @@ class _PdfSignatureHomePageState extends ConsumerState { builder: (context, area) => Offstage( offstage: !_showPagesSidebar, - child: const PagesSidebar(), + child: Consumer( + builder: (context, ref, child) { + final pdfViewModel = ref.watch(pdfViewModelProvider); + final pdf = pdfViewModel.document; + + final documentRef = + pdf.loaded && pdf.pickedPdfBytes != null + ? PdfDocumentRefData( + pdf.pickedPdfBytes!, + sourceName: 'document.pdf', + ) + : null; + + return PagesSidebar( + documentRef: documentRef, + controller: _viewModel.controller, + currentPage: _viewModel.currentPage, + ); + }, + ), ), ), Area( @@ -235,6 +240,7 @@ class _PdfSignatureHomePageState extends ConsumerState { builder: (context, area) => RepaintBoundary( child: PdfPageArea( + controller: _viewModel.controller, key: const ValueKey('pdf_page_area'), pageSize: _pageSize, onDragSignature: _onDragSignature, @@ -300,15 +306,9 @@ class _PdfSignatureHomePageState extends ConsumerState { @override Widget build(BuildContext context) { - // Provide controller override so descendants can access it. - return ProviderScope( - overrides: [pdfViewerControllerProvider.overrideWithValue(_controller)], - child: _buildScaffold(context), - ); + return _buildScaffold(context); } - late final PdfViewerController _controller = PdfViewerController(); - Widget _buildScaffold(BuildContext context) { final isExporting = ref.watch(exportingProvider); final l = AppLocalizations.of(context); @@ -323,6 +323,7 @@ class _PdfSignatureHomePageState extends ConsumerState { PdfToolbar( disabled: isExporting, onPickPdf: _pickPdf, + onClosePdf: _closePdf, onJumpToPage: _jumpToPage, onZoomOut: () { setState(() { @@ -335,7 +336,7 @@ class _PdfSignatureHomePageState extends ConsumerState { }); }, zoomLevel: _zoomLevel, - fileName: _file.name, + filePath: widget.currentFile.path, showPagesSidebar: _showPagesSidebar, showSignaturesSidebar: _showSignaturesSidebar, onTogglePagesSidebar: diff --git a/lib/ui/features/pdf/widgets/pdf_toolbar.dart b/lib/ui/features/pdf/widgets/pdf_toolbar.dart index a8350f7..d79e4fe 100644 --- a/lib/ui/features/pdf/widgets/pdf_toolbar.dart +++ b/lib/ui/features/pdf/widgets/pdf_toolbar.dart @@ -3,19 +3,19 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:pdf_signature/l10n/app_localizations.dart'; -import 'package:pdf_signature/data/repositories/document_repository.dart'; -import '../view_model/pdf_providers.dart'; +import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart'; class PdfToolbar extends ConsumerStatefulWidget { const PdfToolbar({ super.key, required this.disabled, required this.onPickPdf, + required this.onClosePdf, required this.onJumpToPage, required this.onZoomOut, required this.onZoomIn, this.zoomLevel, - this.fileName, + this.filePath, required this.showPagesSidebar, required this.showSignaturesSidebar, required this.onTogglePagesSidebar, @@ -24,8 +24,9 @@ class PdfToolbar extends ConsumerStatefulWidget { final bool disabled; final VoidCallback onPickPdf; + final VoidCallback onClosePdf; final ValueChanged onJumpToPage; - final String? fileName; + final String? filePath; final VoidCallback onZoomOut; final VoidCallback onZoomIn; // Current zoom level as a percentage (e.g., 100 for 100%) @@ -56,8 +57,9 @@ class _PdfToolbarState extends ConsumerState { @override Widget build(BuildContext context) { - final pdf = ref.watch(documentRepositoryProvider); - final currentPage = ref.watch(currentPageProvider); + final pdfViewModel = ref.watch(pdfViewModelProvider); + final pdf = pdfViewModel.document; + final currentPage = pdfViewModel.currentPage; final l = AppLocalizations.of(context); final pageInfo = l.pageInfo(currentPage, pdf.pageCount); @@ -83,9 +85,9 @@ class _PdfToolbarState extends ConsumerState { ConstrainedBox( constraints: const BoxConstraints(maxWidth: 220), child: Text( - // if filename not null - widget.fileName != null - ? widget.fileName! + // if filePath not null + widget.filePath != null + ? widget.filePath! : 'No file selected', overflow: TextOverflow.ellipsis, ), @@ -94,6 +96,12 @@ class _PdfToolbarState extends ConsumerState { ), ), if (pdf.loaded) ...[ + IconButton( + key: const Key('btn_close_pdf'), + onPressed: widget.disabled ? null : widget.onClosePdf, + icon: const Icon(Icons.close), + tooltip: l.close, + ), Wrap( spacing: 8, children: [ diff --git a/lib/ui/features/pdf/widgets/pdf_viewer_widget.dart b/lib/ui/features/pdf/widgets/pdf_viewer_widget.dart index 4b2fe92..8ddaf21 100644 --- a/lib/ui/features/pdf/widgets/pdf_viewer_widget.dart +++ b/lib/ui/features/pdf/widgets/pdf_viewer_widget.dart @@ -1,11 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:pdfrx/pdfrx.dart'; -import 'package:pdf_signature/data/repositories/document_repository.dart'; import 'package:pdf_signature/l10n/app_localizations.dart'; import 'pdf_page_overlays.dart'; import './pdf_mock_continuous_list.dart'; -import '../view_model/pdf_providers.dart'; import '../view_model/pdf_view_model.dart'; class PdfViewerWidget extends ConsumerStatefulWidget { @@ -55,11 +53,10 @@ class _PdfViewerWidgetState extends ConsumerState { @override Widget build(BuildContext context) { - final document = ref.watch(documentRepositoryProvider); - final useMock = ref.watch(useMockViewerProvider); - ref.watch(activeRectProvider); // trigger rebuild when active rect changes - // Watch to rebuild on page change - ref.watch(currentPageProvider); + final pdfViewModel = ref.watch(pdfViewModelProvider); + final document = pdfViewModel.document; + final useMock = pdfViewModel.useMockViewer; + // trigger rebuild when active rect changes // Update document ref when document changes if (document.loaded && document.pickedPdfBytes != null) { @@ -109,12 +106,11 @@ class _PdfViewerWidgetState extends ConsumerState { onViewerReady: (document, controller) { // Update page count in repository ref - .read(documentRepositoryProvider.notifier) + .read(pdfViewModelProvider.notifier) .setPageCount(document.pages.length); }, onPageChanged: (page) { if (page != null) { - ref.read(currentPageProvider.notifier).state = page; // Also update the view model to keep them in sync ref.read(pdfViewModelProvider.notifier).jumpToPage(page); } @@ -123,7 +119,7 @@ class _PdfViewerWidgetState extends ConsumerState { return [ PdfPageOverlays( pageSize: widget.pageSize, - pageNumber: ref.watch(currentPageProvider), + pageNumber: pdfViewModel.currentPage, onDragSignature: widget.onDragSignature, onResizeSignature: widget.onResizeSignature, onConfirmSignature: widget.onConfirmSignature, diff --git a/lib/ui/features/pdf/widgets/signatures_sidebar.dart b/lib/ui/features/pdf/widgets/signatures_sidebar.dart index 99fa74e..874804e 100644 --- a/lib/ui/features/pdf/widgets/signatures_sidebar.dart +++ b/lib/ui/features/pdf/widgets/signatures_sidebar.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:pdf_signature/l10n/app_localizations.dart'; -import 'signature_drawer.dart'; +import '../../signature/widgets/signature_drawer.dart'; import 'ui_services.dart'; class SignaturesSidebar extends ConsumerWidget { diff --git a/lib/ui/features/pdf/widgets/thumbnails_view.dart b/lib/ui/features/pdf/widgets/thumbnails_view.dart deleted file mode 100644 index d23041f..0000000 --- a/lib/ui/features/pdf/widgets/thumbnails_view.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:pdfrx/pdfrx.dart'; -import 'package:pdf_signature/data/repositories/document_repository.dart'; -import '../view_model/pdf_providers.dart'; - -class ThumbnailsView extends ConsumerWidget { - const ThumbnailsView({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final pdf = ref.watch(documentRepositoryProvider); - final controller = ref.watch(pdfViewerControllerProvider); - final theme = Theme.of(context); - - if (!pdf.loaded || pdf.pickedPdfBytes == null) - return const SizedBox.shrink(); - - final documentRef = PdfDocumentRefData( - pdf.pickedPdfBytes!, - sourceName: 'document.pdf', - ); - - return Container( - color: theme.colorScheme.surface, - child: PdfDocumentViewBuilder( - documentRef: documentRef, - builder: (context, document) { - final pageCount = document?.pages.length ?? 0; - return ListView.separated( - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8), - itemCount: pageCount, - separatorBuilder: (_, _) => const SizedBox(height: 8), - itemBuilder: (context, index) { - final pageNumber = index + 1; - final isSelected = ref.watch(currentPageProvider) == pageNumber; - return InkWell( - onTap: () { - controller.goToPage( - pageNumber: pageNumber, - anchor: PdfPageAnchor.top, - ); - }, - child: DecoratedBox( - decoration: BoxDecoration( - color: - isSelected - ? theme.colorScheme.primaryContainer - : theme.cardColor, - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: - isSelected - ? theme.colorScheme.primary - : theme.dividerColor, - ), - ), - child: Padding( - padding: const EdgeInsets.all(6), - child: Column( - children: [ - SizedBox( - height: 180, - child: ClipRRect( - borderRadius: BorderRadius.circular(4), - child: PdfPageView( - document: document, - pageNumber: pageNumber, - alignment: Alignment.center, - ), - ), - ), - const SizedBox(height: 4), - Text('$pageNumber', style: theme.textTheme.bodySmall), - ], - ), - ), - ), - ); - }, - ); - }, - ), - ); - } -} diff --git a/lib/ui/features/pdf/widgets/signature_drawer.dart b/lib/ui/features/signature/widgets/signature_drawer.dart similarity index 95% rename from lib/ui/features/pdf/widgets/signature_drawer.dart rename to lib/ui/features/signature/widgets/signature_drawer.dart index 74502d6..a3c23d0 100644 --- a/lib/ui/features/pdf/widgets/signature_drawer.dart +++ b/lib/ui/features/signature/widgets/signature_drawer.dart @@ -6,9 +6,8 @@ import 'package:pdf_signature/l10n/app_localizations.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 '../../signature/widgets/signature_card.dart'; -import '../view_model/pdf_providers.dart'; +import '../../pdf/widgets/image_editor_dialog.dart'; +import 'signature_card.dart'; /// Data for drag-and-drop is in signature_drag_data.dart @@ -66,9 +65,7 @@ class _SignatureDrawerState extends ConsumerState { ); }, onTap: () { - ref - .read(activeRectProvider.notifier) - .state = const Rect.fromLTWH(0.2, 0.2, 0.3, 0.15); + // state = const Rect.fromLTWH(0.2, 0.2, 0.3, 0.15); }, ), ), diff --git a/lib/ui/features/welcome/view_model/welcome_view_model.dart b/lib/ui/features/welcome/view_model/welcome_view_model.dart index 76acbd7..ecd3c16 100644 --- a/lib/ui/features/welcome/view_model/welcome_view_model.dart +++ b/lib/ui/features/welcome/view_model/welcome_view_model.dart @@ -1,8 +1,6 @@ import 'dart:typed_data'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:pdf_signature/data/repositories/document_repository.dart'; -import 'package:pdf_signature/data/repositories/signature_card_repository.dart'; -import 'package:pdfrx/pdfrx.dart'; +import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart'; class WelcomeViewModel { final Ref ref; @@ -10,19 +8,9 @@ class WelcomeViewModel { WelcomeViewModel(this.ref); Future openPdf({required String path, Uint8List? bytes}) async { - int pageCount = 1; // default - if (bytes != null) { - try { - final doc = await PdfDocument.openData(bytes); - pageCount = doc.pages.length; - } catch (_) { - // ignore - } - } - ref - .read(documentRepositoryProvider.notifier) - .openPicked(pageCount: pageCount, bytes: bytes); - ref.read(signatureCardRepositoryProvider.notifier).clearAll(); + await ref + .read(pdfViewModelProvider.notifier) + .openPdf(path: path, bytes: bytes); } } diff --git a/lib/ui/features/welcome/widgets/welcome_screen.dart b/lib/ui/features/welcome/widgets/welcome_screen.dart index bb3a488..09c59c7 100644 --- a/lib/ui/features/welcome/widgets/welcome_screen.dart +++ b/lib/ui/features/welcome/widgets/welcome_screen.dart @@ -1,12 +1,10 @@ import 'dart:typed_data'; import 'package:desktop_drop/desktop_drop.dart'; -import 'package:file_selector/file_selector.dart' as fs; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:pdf_signature/l10n/app_localizations.dart'; -import 'package:pdf_signature/ui/features/welcome/view_model/welcome_view_model.dart'; // Abstraction to make drop handling testable without constructing // platform-specific DropItem types in widget tests. @@ -32,7 +30,8 @@ typedef Reader = T Function(ProviderListenable provider); // Select first .pdf file (case-insensitive) or fall back to first entry. Future handleDroppedFiles( - Reader read, + Future Function({String? path, Uint8List? bytes, String? fileName}) + onOpenPdf, Iterable files, ) async { if (files.isEmpty) return; @@ -47,11 +46,23 @@ Future handleDroppedFiles( bytes = null; } final String path = pdf.path ?? pdf.name; - await read(welcomeViewModelProvider).openPdf(path: path, bytes: bytes); + await onOpenPdf(path: path, bytes: bytes); } class WelcomeScreen extends ConsumerStatefulWidget { - const WelcomeScreen({super.key}); + final Future Function() onPickPdf; + final Future Function({ + String? path, + Uint8List? bytes, + String? fileName, + }) + onOpenPdf; + + const WelcomeScreen({ + super.key, + required this.onPickPdf, + required this.onOpenPdf, + }); @override ConsumerState createState() => _WelcomeScreenState(); @@ -61,19 +72,7 @@ class _WelcomeScreenState extends ConsumerState { bool _dragging = false; Future _pickPdf() async { - final typeGroup = const fs.XTypeGroup(label: 'PDF', extensions: ['pdf']); - final file = await fs.openFile(acceptedTypeGroups: [typeGroup]); - if (file != null) { - Uint8List? bytes; - try { - bytes = await file.readAsBytes(); - } catch (_) { - bytes = null; - } - await ref - .read(welcomeViewModelProvider) - .openPdf(path: file.path, bytes: bytes); - } + await widget.onPickPdf(); } @override @@ -113,7 +112,7 @@ class _WelcomeScreenState extends ConsumerState { final adapters = desktopFiles.map( (f) => _DropReadableFromDesktop(f), ); - await handleDroppedFiles(ref.read, adapters); + await handleDroppedFiles(widget.onOpenPdf, adapters); }, child: AnimatedContainer( duration: const Duration(milliseconds: 150), diff --git a/pubspec.yaml b/pubspec.yaml index efc6edb..2feb0f0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -70,6 +70,7 @@ dev_dependencies: freezed: ^3.0.0 custom_lint: ^0.7.6 riverpod_lint: ^2.6.5 + go_router_builder: ^4.0.1 # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is @@ -80,6 +81,7 @@ dev_dependencies: msix: ^3.16.12 json_serializable: ^6.11.0 dead_code_analyzer: ^1.1.0 + faker_dart: ^0.2.3 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/test/features/_test_helper.dart b/test/features/_test_helper.dart index fd1cc9b..697d31f 100644 --- a/test/features/_test_helper.dart +++ b/test/features/_test_helper.dart @@ -2,11 +2,11 @@ import 'dart:typed_data'; import 'dart:ui'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.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/app.dart'; import 'package:pdf_signature/data/repositories/preferences_repository.dart'; import 'package:pdf_signature/data/repositories/document_repository.dart'; -import 'package:pdf_signature/ui/features/pdf/view_model/pdf_providers.dart'; import 'package:pdf_signature/ui/features/pdf/widgets/ui_services.dart'; import 'package:pdf_signature/data/services/export_service.dart'; import 'package:pdf_signature/domain/models/model.dart'; @@ -48,7 +48,9 @@ Future pumpApp( documentRepositoryProvider.overrideWith( (ref) => DocumentStateNotifier()..openSample(), ), - useMockViewerProvider.overrideWith((ref) => true), + pdfViewModelProvider.overrideWith( + (ref) => PdfViewModel(ref, useMockViewer: true), + ), exportServiceProvider.overrideWith((ref) => fakeExport), savePathPickerProvider.overrideWith((ref) => () async => 'out.pdf'), ], diff --git a/test/features/step/a_multipage_document_is_open.dart b/test/features/step/a_multipage_document_is_open.dart index 7bdfab1..fceb6ec 100644 --- a/test/features/step/a_multipage_document_is_open.dart +++ b/test/features/step/a_multipage_document_is_open.dart @@ -4,7 +4,7 @@ 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_asset_repository.dart'; import 'package:pdf_signature/domain/models/model.dart'; -import 'package:pdf_signature/ui/features/pdf/view_model/pdf_providers.dart'; + import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart'; import '_world.dart'; @@ -21,7 +21,7 @@ Future aMultipageDocumentIsOpen(WidgetTester tester) async { container.read(documentRepositoryProvider.notifier).openPicked(pageCount: 5); // Reset page state providers try { - container.read(currentPageProvider.notifier).state = 1; + container.read(pdfViewModelProvider.notifier).jumpToPage(1); } catch (_) {} try { container.read(pdfViewModelProvider.notifier).jumpToPage(1); diff --git a/test/features/step/a_signature_asset_is_placed_on_the_page.dart b/test/features/step/a_signature_asset_is_placed_on_the_page.dart index ec85fc6..655e738 100644 --- a/test/features/step/a_signature_asset_is_placed_on_the_page.dart +++ b/test/features/step/a_signature_asset_is_placed_on_the_page.dart @@ -36,7 +36,7 @@ Future aSignatureAssetIsPlacedOnThePage(WidgetTester tester) async { } // Place it on the current page - final currentPage = container.read(pdfViewModelProvider); + final currentPage = container.read(pdfViewModelProvider).currentPage; container .read(documentRepositoryProvider.notifier) .addPlacement( diff --git a/test/features/step/a_signature_placement_is_placed_with_a_position_and_size_relative_to_the_page.dart b/test/features/step/a_signature_placement_is_placed_with_a_position_and_size_relative_to_the_page.dart index 8b06334..c5a2d3b 100644 --- a/test/features/step/a_signature_placement_is_placed_with_a_position_and_size_relative_to_the_page.dart +++ b/test/features/step/a_signature_placement_is_placed_with_a_position_and_size_relative_to_the_page.dart @@ -13,7 +13,7 @@ Future aSignaturePlacementIsPlacedWithAPositionAndSizeRelativeToThePage( ) async { final container = TestWorld.container ?? ProviderContainer(); TestWorld.container = container; - final currentPage = container.read(pdfViewModelProvider); + final currentPage = container.read(pdfViewModelProvider).currentPage; container .read(documentRepositoryProvider.notifier) .addPlacement( diff --git a/test/features/step/dragging_or_resizing_one_does_not_change_the_other.dart b/test/features/step/dragging_or_resizing_one_does_not_change_the_other.dart index 2c3627f..8c62876 100644 --- a/test/features/step/dragging_or_resizing_one_does_not_change_the_other.dart +++ b/test/features/step/dragging_or_resizing_one_does_not_change_the_other.dart @@ -9,7 +9,7 @@ Future draggingOrResizingOneDoesNotChangeTheOther( WidgetTester tester, ) async { final container = TestWorld.container ?? ProviderContainer(); - final page = container.read(pdfViewModelProvider); + final page = container.read(pdfViewModelProvider).currentPage; final list = container .read(documentRepositoryProvider.notifier) .placementsOn(page); diff --git a/test/features/step/page_is_displayed.dart b/test/features/step/page_is_displayed.dart index 80921b0..46a326c 100644 --- a/test/features/step/page_is_displayed.dart +++ b/test/features/step/page_is_displayed.dart @@ -1,6 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:pdf_signature/ui/features/pdf/view_model/pdf_providers.dart'; + import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart'; import '_world.dart'; @@ -8,11 +8,10 @@ import '_world.dart'; Future pageIsDisplayed(WidgetTester tester, num param1) async { final expected = param1.toInt(); final c = TestWorld.container ?? ProviderContainer(); - final vm = c.read(pdfViewModelProvider); - final legacy = c.read(currentPageProvider); + final currentPage = c.read(pdfViewModelProvider).currentPage; expect( - vm == expected || legacy == expected, + currentPage == expected, true, - reason: 'Expected page $expected but got vm=$vm current=$legacy', + reason: 'Expected page $expected but got current=$currentPage', ); } diff --git a/test/features/step/the_app_launches.dart b/test/features/step/the_app_launches.dart index 153421c..2d67810 100644 --- a/test/features/step/the_app_launches.dart +++ b/test/features/step/the_app_launches.dart @@ -7,7 +7,8 @@ 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_card_repository.dart'; import 'package:pdf_signature/domain/models/model.dart'; -import 'package:pdf_signature/ui/features/pdf/view_model/pdf_providers.dart'; +import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart'; + import '_world.dart'; class _BridgedSignatureCardStateNotifier extends SignatureCardStateNotifier { @@ -37,7 +38,9 @@ Future theAppLaunches(WidgetTester tester) async { documentRepositoryProvider.overrideWith( (ref) => DocumentStateNotifier()..openSample(), ), - useMockViewerProvider.overrideWith((ref) => true), + pdfViewModelProvider.overrideWith( + (ref) => PdfViewModel(ref, useMockViewer: true), + ), // Bridge: automatically mirror assets into signature cards so legacy // feature steps that expect SignatureCard widgets keep working even // though the production UI currently only stores raw assets. diff --git a/test/features/step/the_last_page_is_displayed_page.dart b/test/features/step/the_last_page_is_displayed_page.dart index 82eac65..c07e657 100644 --- a/test/features/step/the_last_page_is_displayed_page.dart +++ b/test/features/step/the_last_page_is_displayed_page.dart @@ -2,7 +2,7 @@ import 'package:flutter_test/flutter_test.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 'package:pdf_signature/ui/features/pdf/view_model/pdf_providers.dart'; + import '_world.dart'; /// Usage: the last page is displayed (page {5}) @@ -11,11 +11,10 @@ Future theLastPageIsDisplayedPage(WidgetTester tester, num param1) async { final c = TestWorld.container ?? ProviderContainer(); final pdf = c.read(documentRepositoryProvider); expect(pdf.pageCount, last); - final vm = c.read(pdfViewModelProvider); - final legacy = c.read(currentPageProvider); + final currentPage = c.read(pdfViewModelProvider).currentPage; expect( - vm == last || legacy == last, + currentPage == last, true, - reason: 'Expected last page $last but got vm=$vm current=$legacy', + reason: 'Expected last page $last but got current=$currentPage', ); } diff --git a/test/features/step/the_user_clicks_the_go_to_apply_button.dart b/test/features/step/the_user_clicks_the_go_to_apply_button.dart index d0540fd..4bdef9e 100644 --- a/test/features/step/the_user_clicks_the_go_to_apply_button.dart +++ b/test/features/step/the_user_clicks_the_go_to_apply_button.dart @@ -1,6 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:pdf_signature/ui/features/pdf/view_model/pdf_providers.dart'; + import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart'; import '_world.dart'; @@ -9,12 +9,10 @@ Future theUserClicksTheGoToApplyButton(WidgetTester tester) async { final c = TestWorld.container ?? ProviderContainer(); final pending = TestWorld.pendingGoTo; if (pending != null) { - try { - c.read(currentPageProvider.notifier).state = pending; - } catch (_) {} try { c.read(pdfViewModelProvider.notifier).jumpToPage(pending); } catch (_) {} + assert(c.read(pdfViewModelProvider).currentPage == pending); await tester.pump(); } } diff --git a/test/features/step/the_user_clicks_the_thumbnail_for_page.dart b/test/features/step/the_user_clicks_the_thumbnail_for_page.dart index 7d56eb4..20d659e 100644 --- a/test/features/step/the_user_clicks_the_thumbnail_for_page.dart +++ b/test/features/step/the_user_clicks_the_thumbnail_for_page.dart @@ -1,6 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:pdf_signature/ui/features/pdf/view_model/pdf_providers.dart'; + import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart'; import '_world.dart'; @@ -11,9 +11,6 @@ Future theUserClicksTheThumbnailForPage( ) async { final page = param1.toInt(); final c = TestWorld.container ?? ProviderContainer(); - try { - c.read(currentPageProvider.notifier).state = page; - } catch (_) {} try { c.read(pdfViewModelProvider.notifier).jumpToPage(page); } catch (_) {} diff --git a/test/features/step/the_user_deletes_one_selected_signature_placement.dart b/test/features/step/the_user_deletes_one_selected_signature_placement.dart index ade508f..324b1ee 100644 --- a/test/features/step/the_user_deletes_one_selected_signature_placement.dart +++ b/test/features/step/the_user_deletes_one_selected_signature_placement.dart @@ -10,7 +10,7 @@ Future theUserDeletesOneSelectedSignaturePlacement( ) async { final container = TestWorld.container ?? ProviderContainer(); TestWorld.container = container; - final currentPage = container.read(pdfViewModelProvider); + final currentPage = container.read(pdfViewModelProvider).currentPage; final placements = container .read(documentRepositoryProvider.notifier) .placementsOn(currentPage); diff --git a/test/features/step/the_user_drags_handles_to_resize_and_drags_to_reposition.dart b/test/features/step/the_user_drags_handles_to_resize_and_drags_to_reposition.dart index 37919b9..5f4c1bb 100644 --- a/test/features/step/the_user_drags_handles_to_resize_and_drags_to_reposition.dart +++ b/test/features/step/the_user_drags_handles_to_resize_and_drags_to_reposition.dart @@ -12,7 +12,7 @@ Future theUserDragsHandlesToResizeAndDragsToReposition( final container = TestWorld.container ?? ProviderContainer(); TestWorld.container = container; final pdfN = container.read(documentRepositoryProvider.notifier); - final currentPage = container.read(pdfViewModelProvider); + final currentPage = container.read(pdfViewModelProvider).currentPage; final placements = pdfN.placementsOn(currentPage); if (placements.isNotEmpty) { diff --git a/test/features/step/the_user_drags_this_signature_card_on_the_page_of_the_document_to_place_a_signature_placement.dart b/test/features/step/the_user_drags_this_signature_card_on_the_page_of_the_document_to_place_a_signature_placement.dart index 96e55df..104cf8f 100644 --- a/test/features/step/the_user_drags_this_signature_card_on_the_page_of_the_document_to_place_a_signature_placement.dart +++ b/test/features/step/the_user_drags_this_signature_card_on_the_page_of_the_document_to_place_a_signature_placement.dart @@ -48,7 +48,7 @@ theUserDragsThisSignatureCardOnThePageOfTheDocumentToPlaceASignaturePlacement( final drop_card = temp_card; // Place it on the current page - final currentPage = container.read(pdfViewModelProvider); + final currentPage = container.read(pdfViewModelProvider).currentPage; container .read(documentRepositoryProvider.notifier) .addPlacement( diff --git a/test/features/step/the_user_enters_into_the_go_to_input_and_applies_it.dart b/test/features/step/the_user_enters_into_the_go_to_input_and_applies_it.dart index 1bbfa53..4bff165 100644 --- a/test/features/step/the_user_enters_into_the_go_to_input_and_applies_it.dart +++ b/test/features/step/the_user_enters_into_the_go_to_input_and_applies_it.dart @@ -1,6 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:pdf_signature/ui/features/pdf/view_model/pdf_providers.dart'; + import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart'; import '_world.dart'; @@ -15,7 +15,7 @@ Future theUserEntersIntoTheGoToInputAndAppliesIt( final clamped = value < 1 ? 1 : value; // upper bound validated in last-page check step try { - c.read(currentPageProvider.notifier).state = clamped; + c.read(pdfViewModelProvider.notifier).jumpToPage(clamped); } catch (_) {} try { c.read(pdfViewModelProvider.notifier).jumpToPage(clamped); diff --git a/test/features/step/the_user_jumps_to_page.dart b/test/features/step/the_user_jumps_to_page.dart index ba6c97d..6d74320 100644 --- a/test/features/step/the_user_jumps_to_page.dart +++ b/test/features/step/the_user_jumps_to_page.dart @@ -1,6 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:pdf_signature/ui/features/pdf/view_model/pdf_providers.dart'; + import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart'; import '_world.dart'; @@ -9,10 +9,7 @@ Future theUserJumpsToPage(WidgetTester tester, num param1) async { final page = param1.toInt(); final c = TestWorld.container ?? ProviderContainer(); try { - c.read(currentPageProvider.notifier).state = page; - } catch (_) {} - try { - c.read(pdfViewModelProvider.notifier).jumpToPage(page); + c.read(pdfViewModelProvider).jumpToPage(page); } catch (_) {} await tester.pump(); } diff --git a/test/features/step/the_user_navigates_to_page_and_places_another_signature_placement.dart b/test/features/step/the_user_navigates_to_page_and_places_another_signature_placement.dart index e42fdc3..c42b53d 100644 --- a/test/features/step/the_user_navigates_to_page_and_places_another_signature_placement.dart +++ b/test/features/step/the_user_navigates_to_page_and_places_another_signature_placement.dart @@ -4,7 +4,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:pdf_signature/data/repositories/document_repository.dart'; import 'package:pdf_signature/domain/models/model.dart'; -import 'package:pdf_signature/ui/features/pdf/view_model/pdf_providers.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart'; import '_world.dart'; @@ -17,9 +16,6 @@ Future theUserNavigatesToPageAndPlacesAnotherSignaturePlacement( TestWorld.container = container; final page = param1.toInt(); // Update page providers directly (repository jumpTo is a no-op now) - try { - container.read(currentPageProvider.notifier).state = page; - } catch (_) {} try { container.read(pdfViewModelProvider.notifier).jumpToPage(page); } catch (_) {} diff --git a/test/features/step/the_user_places_two_signature_placements_on_the_same_page.dart b/test/features/step/the_user_places_two_signature_placements_on_the_same_page.dart index 3c154bf..fba9a84 100644 --- a/test/features/step/the_user_places_two_signature_placements_on_the_same_page.dart +++ b/test/features/step/the_user_places_two_signature_placements_on_the_same_page.dart @@ -14,7 +14,7 @@ Future theUserPlacesTwoSignaturePlacementsOnTheSamePage( final container = TestWorld.container ?? ProviderContainer(); TestWorld.container = container; // pdfViewModelProvider returns 1-based current page - final page = container.read(pdfViewModelProvider); + final page = container.read(pdfViewModelProvider).currentPage; container .read(documentRepositoryProvider.notifier) .addPlacement( diff --git a/test/features/step/the_user_types_into_the_go_to_input_and_presses_enter.dart b/test/features/step/the_user_types_into_the_go_to_input_and_presses_enter.dart index 3af7065..60903e5 100644 --- a/test/features/step/the_user_types_into_the_go_to_input_and_presses_enter.dart +++ b/test/features/step/the_user_types_into_the_go_to_input_and_presses_enter.dart @@ -1,6 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:pdf_signature/ui/features/pdf/view_model/pdf_providers.dart'; + import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart'; import '_world.dart'; @@ -12,9 +12,6 @@ Future theUserTypesIntoTheGoToInputAndPressesEnter( final target = param1.toInt(); final c = TestWorld.container ?? ProviderContainer(); TestWorld.container = c; - try { - c.read(currentPageProvider.notifier).state = target; - } catch (_) {} try { c.read(pdfViewModelProvider.notifier).jumpToPage(target); } catch (_) {} diff --git a/test/features/step/the_user_uses_rotate_controls.dart b/test/features/step/the_user_uses_rotate_controls.dart index 77ae882..1ed82e1 100644 --- a/test/features/step/the_user_uses_rotate_controls.dart +++ b/test/features/step/the_user_uses_rotate_controls.dart @@ -8,7 +8,7 @@ import '_world.dart'; Future theUserUsesRotateControls(WidgetTester tester) async { final container = TestWorld.container ?? ProviderContainer(); final pdfN = container.read(documentRepositoryProvider.notifier); - final currentPage = container.read(pdfViewModelProvider); + final currentPage = container.read(pdfViewModelProvider).currentPage; final placements = pdfN.placementsOn(currentPage); if (placements.isNotEmpty) { pdfN.updatePlacementRotation( diff --git a/test/features/step/three_signature_placements_are_placed_on_the_current_page.dart b/test/features/step/three_signature_placements_are_placed_on_the_current_page.dart index e23ed58..383f902 100644 --- a/test/features/step/three_signature_placements_are_placed_on_the_current_page.dart +++ b/test/features/step/three_signature_placements_are_placed_on_the_current_page.dart @@ -24,7 +24,7 @@ Future threeSignaturePlacementsArePlacedOnTheCurrentPage( ]; container.read(documentRepositoryProvider.notifier).openPicked(pageCount: 5); final pdfN = container.read(documentRepositoryProvider.notifier); - final page = container.read(pdfViewModelProvider); + final page = container.read(pdfViewModelProvider).currentPage; pdfN.addPlacement( page: page, rect: Rect.fromLTWH(10, 10, 50, 50), diff --git a/test/widget/export_flow_test.dart b/test/widget/export_flow_test.dart index aa334fd..f5d0282 100644 --- a/test/widget/export_flow_test.dart +++ b/test/widget/export_flow_test.dart @@ -1,4 +1,5 @@ import 'dart:typed_data'; +import 'package:file_selector/file_selector.dart' as fs; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -8,7 +9,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:pdf_signature/data/services/export_service.dart'; import 'package:pdf_signature/ui/features/pdf/widgets/ui_services.dart'; import 'package:pdf_signature/data/repositories/preferences_repository.dart'; -import 'package:pdf_signature/ui/features/pdf/view_model/pdf_providers.dart'; +import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart'; import 'package:pdf_signature/data/repositories/document_repository.dart'; import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart'; @@ -58,7 +59,9 @@ void main() { DocumentStateNotifier() ..openPicked(pageCount: 5, bytes: Uint8List(0)), ), - useMockViewerProvider.overrideWith((ref) => true), + pdfViewModelProvider.overrideWith( + (ref) => PdfViewModel(ref, useMockViewer: true), + ), exportServiceProvider.overrideWith((_) => fake), savePathPickerProvider.overrideWith( (_) => () async => 'C:/tmp/output.pdf', @@ -67,7 +70,11 @@ void main() { child: MaterialApp( localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, - home: const PdfSignatureHomePage(), + home: PdfSignatureHomePage( + onPickPdf: () async {}, + onClosePdf: () {}, + currentFile: fs.XFile(''), + ), ), ), ); diff --git a/test/widget/helpers.dart b/test/widget/helpers.dart index a4b7c48..1820bf7 100644 --- a/test/widget/helpers.dart +++ b/test/widget/helpers.dart @@ -1,11 +1,12 @@ import 'dart:typed_data'; +import 'package:file_selector/file_selector.dart' as fs; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:image/image.dart' as img; import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart'; -import 'package:pdf_signature/ui/features/pdf/view_model/pdf_providers.dart'; +import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.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/signature_asset_repository.dart'; @@ -22,13 +23,19 @@ Future pumpWithOpenPdf(WidgetTester tester) async { documentRepositoryProvider.overrideWith( (ref) => DocumentStateNotifier()..openSample(), ), - useMockViewerProvider.overrideWithValue(true), + pdfViewModelProvider.overrideWith( + (ref) => PdfViewModel(ref, useMockViewer: true), + ), exportingProvider.overrideWith((ref) => false), ], child: MaterialApp( localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, - home: const PdfSignatureHomePage(), + home: PdfSignatureHomePage( + onPickPdf: () async {}, + onClosePdf: () {}, + currentFile: fs.XFile(''), + ), ), ), ); @@ -388,13 +395,19 @@ Future pumpWithOpenPdfAndSig(WidgetTester tester) async { return cardRepo; }), // In new model, interactive overlay not implemented; keep library empty - useMockViewerProvider.overrideWithValue(true), + pdfViewModelProvider.overrideWith( + (ref) => PdfViewModel(ref, useMockViewer: true), + ), exportingProvider.overrideWith((ref) => false), ], child: MaterialApp( localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, - home: const PdfSignatureHomePage(), + home: PdfSignatureHomePage( + onPickPdf: () async {}, + onClosePdf: () {}, + currentFile: fs.XFile(''), + ), ), ), ); diff --git a/test/widget/pdf_navigation_widget_test.dart b/test/widget/pdf_navigation_widget_test.dart index c575ccb..6110bdc 100644 --- a/test/widget/pdf_navigation_widget_test.dart +++ b/test/widget/pdf_navigation_widget_test.dart @@ -1,11 +1,12 @@ +import 'package:file_selector/file_selector.dart' as fs; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart'; +import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart'; import 'package:pdf_signature/data/repositories/document_repository.dart'; import 'package:pdf_signature/domain/models/model.dart'; -import 'package:pdf_signature/ui/features/pdf/view_model/pdf_providers.dart'; import 'package:pdf_signature/l10n/app_localizations.dart'; @@ -23,7 +24,9 @@ void main() { await tester.pumpWidget( ProviderScope( overrides: [ - useMockViewerProvider.overrideWithValue(true), + pdfViewModelProvider.overrideWith( + (ref) => PdfViewModel(ref, useMockViewer: true), + ), documentRepositoryProvider.overrideWith( (ref) => _TestPdfController(), ), @@ -32,7 +35,11 @@ void main() { localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, locale: const Locale('en'), - home: const PdfSignatureHomePage(), + home: PdfSignatureHomePage( + onPickPdf: () async {}, + onClosePdf: () {}, + currentFile: fs.XFile(''), + ), ), ), ); diff --git a/test/widget/pdf_page_area_early_jump_test.dart b/test/widget/pdf_page_area_early_jump_test.dart index c4d76b0..c7e5d6f 100644 --- a/test/widget/pdf_page_area_early_jump_test.dart +++ b/test/widget/pdf_page_area_early_jump_test.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:pdfrx/pdfrx.dart'; import 'package:pdf_signature/ui/features/pdf/widgets/pdf_page_area.dart'; import 'package:pdf_signature/data/repositories/document_repository.dart'; -import 'package:pdf_signature/ui/features/pdf/view_model/pdf_providers.dart'; + import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart'; import 'package:pdf_signature/l10n/app_localizations.dart'; @@ -25,14 +26,16 @@ void main() { await tester.pumpWidget( ProviderScope( overrides: [ - useMockViewerProvider.overrideWithValue(true), + pdfViewModelProvider.overrideWith( + (ref) => PdfViewModel(ref, useMockViewer: true), + ), documentRepositoryProvider.overrideWith((ref) => ctrl), ], child: MaterialApp( localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, locale: const Locale('en'), - home: const Scaffold( + home: Scaffold( body: Center( child: SizedBox( width: 800, @@ -44,6 +47,7 @@ void main() { onConfirmSignature: _noop, onClearActiveOverlay: _noop, onSelectPlaced: _noopInt, + controller: PdfViewerController(), ), ), ), diff --git a/test/widget/pdf_page_area_jump_test.dart b/test/widget/pdf_page_area_jump_test.dart index d68d078..da30aa1 100644 --- a/test/widget/pdf_page_area_jump_test.dart +++ b/test/widget/pdf_page_area_jump_test.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:pdfrx/pdfrx.dart'; import 'package:pdf_signature/ui/features/pdf/widgets/pdf_page_area.dart'; import 'package:pdf_signature/data/repositories/document_repository.dart'; -import 'package:pdf_signature/ui/features/pdf/view_model/pdf_providers.dart'; + import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart'; import 'package:pdf_signature/l10n/app_localizations.dart'; @@ -25,7 +26,9 @@ void main() { await tester.pumpWidget( ProviderScope( overrides: [ - useMockViewerProvider.overrideWithValue(true), + pdfViewModelProvider.overrideWith( + (ref) => PdfViewModel(ref, useMockViewer: true), + ), // Continuous mode is always-on; no page view override needed documentRepositoryProvider.overrideWith((ref) => ctrl), ], @@ -33,7 +36,7 @@ void main() { localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, locale: const Locale('en'), - home: const Scaffold( + home: Scaffold( body: Center( child: SizedBox( width: 800, @@ -45,6 +48,7 @@ void main() { onConfirmSignature: _noop, onClearActiveOverlay: _noop, onSelectPlaced: _noopInt, + controller: PdfViewerController(), ), ), ), diff --git a/test/widget/pdf_page_area_test.dart b/test/widget/pdf_page_area_test.dart index c70ed95..b4191e5 100644 --- a/test/widget/pdf_page_area_test.dart +++ b/test/widget/pdf_page_area_test.dart @@ -6,7 +6,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:pdf_signature/ui/features/pdf/widgets/pdf_page_area.dart'; import 'package:pdf_signature/data/repositories/document_repository.dart'; -import 'package:pdf_signature/ui/features/pdf/view_model/pdf_providers.dart'; +import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart'; +import 'package:pdfrx/pdfrx.dart'; import 'package:pdf_signature/l10n/app_localizations.dart'; import 'package:pdf_signature/domain/models/model.dart'; @@ -24,7 +25,9 @@ void main() { await tester.pumpWidget( ProviderScope( overrides: [ - useMockViewerProvider.overrideWithValue(true), + pdfViewModelProvider.overrideWith( + (ref) => PdfViewModel(ref, useMockViewer: true), + ), documentRepositoryProvider.overrideWith( (ref) => _TestPdfController(), ), @@ -33,18 +36,19 @@ void main() { localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, locale: const Locale('en'), - home: const Scaffold( + home: Scaffold( body: Center( child: SizedBox( width: 800, height: 520, child: PdfPageArea( - pageSize: Size(676, 400), + pageSize: const Size(676, 400), onDragSignature: _noopOffset, onResizeSignature: _noopOffset, onConfirmSignature: _noop, onClearActiveOverlay: _noop, onSelectPlaced: _noopInt, + controller: PdfViewerController(), ), ), ), @@ -66,7 +70,9 @@ void main() { // Use a persistent container across rebuilds final container = ProviderContainer( overrides: [ - useMockViewerProvider.overrideWithValue(true), + pdfViewModelProvider.overrideWith( + (ref) => PdfViewModel(ref, useMockViewer: true), + ), documentRepositoryProvider.overrideWith( (ref) => DocumentStateNotifier()..openSample(), ), @@ -83,13 +89,14 @@ void main() { child: SizedBox( width: width, // Keep aspect ratio consistent with uiPageSize - child: const PdfPageArea( + child: PdfPageArea( pageSize: uiPageSize, onDragSignature: _noopOffset, onResizeSignature: _noopOffset, onConfirmSignature: _noop, onClearActiveOverlay: _noop, onSelectPlaced: _noopInt, + controller: PdfViewerController(), ), ), ), diff --git a/test/widget/welcome_drop_test.dart b/test/widget/welcome_drop_test.dart index cc714df..3ada86d 100644 --- a/test/widget/welcome_drop_test.dart +++ b/test/widget/welcome_drop_test.dart @@ -26,11 +26,15 @@ void main() { tester, ) async { await tester.pumpWidget( - const ProviderScope( + ProviderScope( child: MaterialApp( localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, - home: WelcomeScreen(), + home: WelcomeScreen( + onPickPdf: () async {}, + onOpenPdf: + ({String? path, Uint8List? bytes, String? fileName}) async {}, + ), ), ), ); @@ -39,8 +43,16 @@ void main() { final bytes = Uint8List.fromList([1, 2, 3, 4]); final fake = _FakeDropReadable('sample.pdf', '/tmp/sample.pdf', bytes); - // Use the top-level helper with the WidgetRef.read function - await handleDroppedFiles(stateful.ref.read, [fake]); + // Call handleDroppedFiles with the onOpenPdf callback from the widget + await handleDroppedFiles(({ + String? path, + Uint8List? bytes, + String? fileName, + }) async { + final container = ProviderScope.containerOf(stateful.context); + final repo = container.read(documentRepositoryProvider.notifier); + repo.openPicked(pageCount: 1, bytes: bytes); + }, [fake]); await tester.pump(); final container = ProviderScope.containerOf(stateful.context);