From feaf7aee9f7312fdbc3df2de1fac39a8c483e454 Mon Sep 17 00:00:00 2001 From: insleker Date: Wed, 17 Sep 2025 20:46:11 +0800 Subject: [PATCH] refactor: update PDF view model and routing for improved session management --- integration_test/pdf_view_test.dart | 93 +++++++++++-- lib/domain/models/graphic_adjust.dart | 2 +- lib/routing/router.dart | 128 ++++-------------- .../pdf/view_model/pdf_view_model.dart | 84 +++++++++--- .../view_model/welcome_view_model.dart | 14 +- 5 files changed, 181 insertions(+), 140 deletions(-) diff --git a/integration_test/pdf_view_test.dart b/integration_test/pdf_view_test.dart index 3bd554f..45adbac 100644 --- a/integration_test/pdf_view_test.dart +++ b/integration_test/pdf_view_test.dart @@ -61,17 +61,17 @@ void main() { final ctx = tester.element(find.byType(PdfSignatureHomePage)); final container = ProviderScope.containerOf(ctx); final vm = container.read(pdfViewModelProvider); - expect(vm, 1); + expect(vm.currentPage, 1); container.read(pdfViewModelProvider.notifier).jumpToPage(2); await tester.pumpAndSettle(); await tester.pump(const Duration(milliseconds: 120)); - expect(container.read(pdfViewModelProvider), 2); + expect(container.read(pdfViewModelProvider).currentPage, 2); container.read(pdfViewModelProvider.notifier).jumpToPage(3); await tester.pumpAndSettle(); await tester.pump(const Duration(milliseconds: 120)); - expect(container.read(pdfViewModelProvider), 3); + expect(container.read(pdfViewModelProvider).currentPage, 3); }); testWidgets('PDF View: zoom in/out', (tester) async { @@ -166,7 +166,7 @@ void main() { final ctx = tester.element(find.byType(PdfSignatureHomePage)); final container = ProviderScope.containerOf(ctx); - expect(container.read(pdfViewModelProvider), 1); + expect(container.read(pdfViewModelProvider).currentPage, 1); final pagesSidebar = find.byType(PagesSidebar); expect(pagesSidebar, findsOneWidget); @@ -180,7 +180,7 @@ void main() { await tester.tap(page3Thumbnail); await tester.pumpAndSettle(); - expect(container.read(pdfViewModelProvider), 3); + expect(container.read(pdfViewModelProvider).currentPage, 3); }); testWidgets('PDF View: thumbnails scroll and select', (tester) async { @@ -221,7 +221,7 @@ void main() { final ctx = tester.element(find.byType(PdfSignatureHomePage)); final container = ProviderScope.containerOf(ctx); - expect(container.read(pdfViewModelProvider), 1); + expect(container.read(pdfViewModelProvider).currentPage, 1); final pagesSidebar = find.byType(PagesSidebar); expect(pagesSidebar, findsOneWidget); @@ -229,14 +229,85 @@ void main() { await tester.drag(pagesSidebar, const Offset(0, -200)); await tester.pumpAndSettle(); - expect(find.text('1'), findsOneWidget); - expect(container.read(pdfViewModelProvider), 1); + // Page number '1' may appear in multiple text widgets (e.g., overlay/toolbar); restrict to sidebar. + final page1InSidebar = find.descendant( + of: pagesSidebar, + matching: find.text('1'), + ); + expect(page1InSidebar, findsOneWidget); + expect(container.read(pdfViewModelProvider).currentPage, 1); // Select page 2 thumbnail and verify page changes - await tester.tap(find.text('2')); + final page2InSidebar = find.descendant( + of: pagesSidebar, + matching: find.text('2'), + ); + await tester.tap(page2InSidebar); await tester.pumpAndSettle(); - expect(container.read(pdfViewModelProvider), 2); + expect(container.read(pdfViewModelProvider).currentPage, 2); }); - //TODO: Scroll Thumbs + testWidgets('PDF View: scroll thumbnails to reveal and select last page', ( + 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(pageCount: 3, bytes: pdfBytes), + ), + pdfViewModelProvider.overrideWith( + (ref) => PdfViewModel(ref, useMockViewer: false), + ), + ], + child: MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + locale: const Locale('en'), + home: PdfSignatureHomePage( + onPickPdf: () async {}, + onClosePdf: () {}, + currentFile: fs.XFile('test.pdf'), + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + final ctx = tester.element(find.byType(PdfSignatureHomePage)); + final container = ProviderScope.containerOf(ctx); + expect(container.read(pdfViewModelProvider).currentPage, 1); + + final pagesSidebar = find.byType(PagesSidebar); + expect(pagesSidebar, findsOneWidget); + + // Ensure page 3 not initially in view by trying to find it and allowing that it might be offstage. + // Perform a scroll/drag to bring page 3 into view. + await tester.drag(pagesSidebar, const Offset(0, -400)); + await tester.pumpAndSettle(); + + final page3 = find.descendant(of: pagesSidebar, matching: find.text('3')); + expect(page3, findsOneWidget); + await tester.tap(page3); + await tester.pumpAndSettle(); + expect(container.read(pdfViewModelProvider).currentPage, 3); + + // Scroll back upward and verify selection persists. + await tester.drag(pagesSidebar, const Offset(0, 300)); + await tester.pumpAndSettle(); + expect(container.read(pdfViewModelProvider).currentPage, 3); + }); + + //TODO: Scroll Thumbs } diff --git a/lib/domain/models/graphic_adjust.dart b/lib/domain/models/graphic_adjust.dart index f05bb12..acbd53e 100644 --- a/lib/domain/models/graphic_adjust.dart +++ b/lib/domain/models/graphic_adjust.dart @@ -5,7 +5,7 @@ class GraphicAdjust { const GraphicAdjust({ this.contrast = 1.0, - this.brightness = 0.0, + this.brightness = 1.0, this.bgRemoval = false, }); diff --git a/lib/routing/router.dart b/lib/routing/router.dart index 5b5d9b9..754e303 100644 --- a/lib/routing/router.dart +++ b/lib/routing/router.dart @@ -5,126 +5,50 @@ 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'; +import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.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); - } - } -} +// PdfManager removed: responsibilities moved into PdfSessionViewModel. final routerProvider = Provider((ref) { - // Create PdfManager instance with dependencies - final documentNotifier = ref.read(documentRepositoryProvider.notifier); - final signatureCardNotifier = ref.read( - signatureCardRepositoryProvider.notifier, - ); + // Determine initial location based on current document state. + // Access the state via the provider (not via the notifier's protected .state). + final docState = ref.read(documentRepositoryProvider); + final initialLocation = docState.loaded ? '/pdf' : '/'; + // Session view model will be obtained inside each route builder; no shared + // late variable (avoids LateInitializationError on rebuilds). - // Create a navigator key for the router final navigatorKey = GlobalKey(); - - // 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; - - // If tests pre-load a document, start at /pdf so sidebars and controls - // are present immediately. - final initialLocation = documentNotifier.debugState.loaded ? '/pdf' : '/'; + late final GoRouter router; // declare before use in builders router = GoRouter( navigatorKey: navigatorKey, routes: [ GoRoute( path: '/', - builder: - (context, state) => WelcomeScreen( - onPickPdf: () => pdfManager.pickAndOpenPdf(), - onOpenPdf: - ({String? path, Uint8List? bytes, String? fileName}) => - pdfManager.openPdf(path: path, bytes: bytes), - ), + builder: (context, state) { + final sessionVm = ref.read(pdfSessionViewModelProvider(router)); + return WelcomeScreen( + onPickPdf: () => sessionVm.pickAndOpenPdf(), + onOpenPdf: + ({String? path, Uint8List? bytes, String? fileName}) => + sessionVm.openPdf(path: path, bytes: bytes), + ); + }, ), GoRoute( path: '/pdf', - builder: - (context, state) => PdfSignatureHomePage( - onPickPdf: () => pdfManager.pickAndOpenPdf(), - onClosePdf: () => pdfManager.closePdf(), - currentFile: pdfManager.currentFile, - ), + builder: (context, state) { + final sessionVm = ref.read(pdfSessionViewModelProvider(router)); + return PdfSignatureHomePage( + onPickPdf: () => sessionVm.pickAndOpenPdf(), + onClosePdf: () => sessionVm.closePdf(), + currentFile: sessionVm.currentFile, + ); + }, ), ], initialLocation: 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_view_model.dart b/lib/ui/features/pdf/view_model/pdf_view_model.dart index a44469f..fdcd7ad 100644 --- a/lib/ui/features/pdf/view_model/pdf_view_model.dart +++ b/lib/ui/features/pdf/view_model/pdf_view_model.dart @@ -5,6 +5,8 @@ import 'package:pdf_signature/data/repositories/document_repository.dart'; import 'package:pdf_signature/data/repositories/signature_card_repository.dart'; import 'package:pdf_signature/domain/models/model.dart'; import 'package:pdfrx/pdfrx.dart'; +import 'package:file_selector/file_selector.dart' as fs; +import 'package:go_router/go_router.dart'; class PdfViewModel extends ChangeNotifier { final Ref ref; @@ -62,28 +64,8 @@ class PdfViewModel extends ChangeNotifier { notifyListeners(); } - Future openPdf({required String path, Uint8List? bytes}) async { - 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); - clearAllSignatureCards(); - - currentPage = 1; // Reset current page to 1 - } - // Document repository methods - void closeDocument() { - ref.read(documentRepositoryProvider.notifier).close(); - } + // Lifecycle (open/close) removed: handled exclusively by PdfSessionViewModel. void setPageCount(int count) { ref.read(documentRepositoryProvider.notifier).setPageCount(count); @@ -197,3 +179,63 @@ class PdfViewModel extends ChangeNotifier { final pdfViewModelProvider = ChangeNotifierProvider((ref) { return PdfViewModel(ref); }); + +/// ViewModel managing PDF session lifecycle (file picking/open/close) and +/// navigation. Replaces the previous PdfManager helper. +class PdfSessionViewModel extends ChangeNotifier { + final Ref ref; + final GoRouter router; + fs.XFile _currentFile = fs.XFile(''); + + PdfSessionViewModel({required this.ref, required this.router}); + + fs.XFile get currentFile => _currentFile; + + 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); + } + } + + 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 invalid bytes + } + } + if (path != null) { + _currentFile = fs.XFile(path); + } + ref + .read(documentRepositoryProvider.notifier) + .openPicked(pageCount: pageCount, bytes: bytes); + ref.read(signatureCardRepositoryProvider.notifier).clearAll(); + router.go('/pdf'); + notifyListeners(); + } + + void closePdf() { + ref.read(documentRepositoryProvider.notifier).close(); + ref.read(signatureCardRepositoryProvider.notifier).clearAll(); + _currentFile = fs.XFile(''); + router.go('/'); + notifyListeners(); + } +} + +final pdfSessionViewModelProvider = + ChangeNotifierProvider.family((ref, router) { + return PdfSessionViewModel(ref: ref, router: router); + }); 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 ecd3c16..a6037ed 100644 --- a/lib/ui/features/welcome/view_model/welcome_view_model.dart +++ b/lib/ui/features/welcome/view_model/welcome_view_model.dart @@ -1,19 +1,23 @@ import 'dart:typed_data'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart'; +import 'package:go_router/go_router.dart'; +import 'package:pdf_signature/routing/router.dart'; class WelcomeViewModel { final Ref ref; + final GoRouter router; - WelcomeViewModel(this.ref); + WelcomeViewModel(this.ref, this.router); Future openPdf({required String path, Uint8List? bytes}) async { - await ref - .read(pdfViewModelProvider.notifier) - .openPdf(path: path, bytes: bytes); + // Use PdfSessionViewModel to open and navigate. + final session = ref.read(pdfSessionViewModelProvider(router)); + await session.openPdf(path: path, bytes: bytes); } } final welcomeViewModelProvider = Provider((ref) { - return WelcomeViewModel(ref); + final router = ref.read(routerProvider); + return WelcomeViewModel(ref, router); });