From 0c3817850223cb77aea8b553cafd4c857f03e74b Mon Sep 17 00:00:00 2001 From: insleker Date: Thu, 18 Sep 2025 21:31:30 +0800 Subject: [PATCH] refactor: ui_services.dart to PdfExportViewModel for export functionality --- integration_test/export_flow_test.dart | 34 +++++++----- .../pdf/view_model/pdf_export_view_model.dart | 53 +++++++++++++++++++ lib/ui/features/pdf/widgets/pdf_screen.dart | 13 +++-- .../pdf/widgets/signatures_sidebar.dart | 4 +- lib/ui/features/pdf/widgets/ui_services.dart | 23 -------- test/features/_test_helper.dart | 11 ++-- test/widget/export_flow_test.dart | 11 ++-- test/widget/helpers.dart | 10 ++-- 8 files changed, 104 insertions(+), 55 deletions(-) create mode 100644 lib/ui/features/pdf/view_model/pdf_export_view_model.dart delete mode 100644 lib/ui/features/pdf/widgets/ui_services.dart diff --git a/integration_test/export_flow_test.dart b/integration_test/export_flow_test.dart index de2d102..bc8bed1 100644 --- a/integration_test/export_flow_test.dart +++ b/integration_test/export_flow_test.dart @@ -14,7 +14,7 @@ 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/widgets/ui_services.dart'; +import 'package:pdf_signature/ui/features/pdf/view_model/pdf_export_view_model.dart'; import 'package:pdf_signature/ui/features/pdf/widgets/pages_sidebar.dart'; import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -77,12 +77,15 @@ void main() { pdfViewModelProvider.overrideWith( (ref) => PdfViewModel(ref, useMockViewer: false), ), - exportServiceProvider.overrideWith((_) => fake), - savePathPickerProvider.overrideWith( - (_) => () async { - final dir = Directory.systemTemp.createTempSync('pdfsig_'); - return '${dir.path}/output.pdf'; - }, + pdfExportViewModelProvider.overrideWith( + (ref) => PdfExportViewModel( + ref, + exporter: fake, + savePathPicker: () async { + final dir = Directory.systemTemp.createTempSync('pdfsig_'); + return '${dir.path}/output.pdf'; + }, + ), ), ], child: MaterialApp( @@ -432,12 +435,17 @@ void main() { pdfViewModelProvider.overrideWith( (ref) => PdfViewModel(ref, useMockViewer: false), ), - exportServiceProvider.overrideWith((ref) => LightweightExporter()), - savePathPickerProvider.overrideWith( - (_) => () async { - final dir = Directory.systemTemp.createTempSync('pdfsig_after_'); - return '${dir.path}/output-after-export.pdf'; - }, + pdfExportViewModelProvider.overrideWith( + (ref) => PdfExportViewModel( + ref, + exporter: LightweightExporter(), + savePathPicker: () async { + final dir = Directory.systemTemp.createTempSync( + 'pdfsig_after_', + ); + return '${dir.path}/output-after-export.pdf'; + }, + ), ), ], child: MaterialApp( diff --git a/lib/ui/features/pdf/view_model/pdf_export_view_model.dart b/lib/ui/features/pdf/view_model/pdf_export_view_model.dart new file mode 100644 index 0000000..a7bc643 --- /dev/null +++ b/lib/ui/features/pdf/view_model/pdf_export_view_model.dart @@ -0,0 +1,53 @@ +import 'package:file_selector/file_selector.dart' as fs; +import 'package:flutter/foundation.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:pdf_signature/data/services/export_service.dart'; + +/// ViewModel for export-related UI state and helpers. +class PdfExportViewModel extends ChangeNotifier { + final Ref ref; + bool _exporting = false; + + // Dependencies (injectable via constructor for tests) + final ExportService _exporter; + final Future Function() _savePathPicker; + + PdfExportViewModel( + this.ref, { + ExportService? exporter, + Future Function()? savePathPicker, + }) : _exporter = exporter ?? ExportService(), + _savePathPicker = savePathPicker ?? _defaultSavePathPicker; + + bool get exporting => _exporting; + + void setExporting(bool value) { + if (_exporting == value) return; + _exporting = value; + notifyListeners(); + } + + /// Get the export service (overridable in tests via constructor). + ExportService get exporter => _exporter; + + /// Show save dialog and return the chosen path (null if canceled). + Future pickSavePath() async { + return _savePathPicker(); + } + + static Future _defaultSavePathPicker() async { + final group = fs.XTypeGroup(label: 'PDF', extensions: ['pdf']); + final location = await fs.getSaveLocation( + acceptedTypeGroups: [group], + suggestedName: 'signed.pdf', + confirmButtonText: 'Save', + ); + return location?.path; // null if user cancels + } +} + +final pdfExportViewModelProvider = ChangeNotifierProvider(( + ref, +) { + return PdfExportViewModel(ref); +}); diff --git a/lib/ui/features/pdf/widgets/pdf_screen.dart b/lib/ui/features/pdf/widgets/pdf_screen.dart index cc79108..3045af3 100644 --- a/lib/ui/features/pdf/widgets/pdf_screen.dart +++ b/lib/ui/features/pdf/widgets/pdf_screen.dart @@ -12,7 +12,7 @@ import 'pdf_toolbar.dart'; import 'pdf_page_area.dart'; import 'pages_sidebar.dart'; import 'signatures_sidebar.dart'; -import 'ui_services.dart'; +import '../view_model/pdf_export_view_model.dart'; import 'package:pdf_signature/utils/download.dart'; import '../view_model/pdf_view_model.dart'; @@ -133,7 +133,7 @@ class _PdfSignatureHomePageState extends ConsumerState { } Future _saveSignedPdf() async { - ref.read(exportingProvider.notifier).state = true; + ref.read(pdfExportViewModelProvider.notifier).setExporting(true); try { final pdf = _viewModel.document; final messenger = ScaffoldMessenger.of(context); @@ -145,7 +145,7 @@ class _PdfSignatureHomePageState extends ConsumerState { ); return; } - final exporter = ref.read(exportServiceProvider); + final exporter = ref.read(pdfExportViewModelProvider).exporter; // get DPI from preferences final targetDpi = ref.read(preferencesRepositoryProvider).exportDpi; @@ -153,8 +153,7 @@ class _PdfSignatureHomePageState extends ConsumerState { String? savedPath; if (!kIsWeb) { - final pick = ref.read(savePathPickerProvider); - final path = await pick(); + final path = await ref.read(pdfExportViewModelProvider).pickSavePath(); if (path == null || path.trim().isEmpty) return; final fullPath = _ensurePdfExtension(path.trim()); savedPath = fullPath; @@ -216,7 +215,7 @@ class _PdfSignatureHomePageState extends ConsumerState { ); } } finally { - ref.read(exportingProvider.notifier).state = false; + ref.read(pdfExportViewModelProvider.notifier).setExporting(false); } } @@ -362,7 +361,7 @@ class _PdfSignatureHomePageState extends ConsumerState { } Widget _buildScaffold(BuildContext context) { - final isExporting = ref.watch(exportingProvider); + final isExporting = ref.watch(pdfExportViewModelProvider).exporting; final l = AppLocalizations.of(context); return Scaffold( body: Padding( diff --git a/lib/ui/features/pdf/widgets/signatures_sidebar.dart b/lib/ui/features/pdf/widgets/signatures_sidebar.dart index 874804e..5d0ed51 100644 --- a/lib/ui/features/pdf/widgets/signatures_sidebar.dart +++ b/lib/ui/features/pdf/widgets/signatures_sidebar.dart @@ -4,7 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:pdf_signature/l10n/app_localizations.dart'; import '../../signature/widgets/signature_drawer.dart'; -import 'ui_services.dart'; +import '../view_model/pdf_export_view_model.dart'; class SignaturesSidebar extends ConsumerWidget { const SignaturesSidebar({ @@ -21,7 +21,7 @@ class SignaturesSidebar extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final l = AppLocalizations.of(context); - final isExporting = ref.watch(exportingProvider); + final isExporting = ref.watch(pdfExportViewModelProvider).exporting; return AbsorbPointer( absorbing: isExporting, child: Card( diff --git a/lib/ui/features/pdf/widgets/ui_services.dart b/lib/ui/features/pdf/widgets/ui_services.dart deleted file mode 100644 index 687827e..0000000 --- a/lib/ui/features/pdf/widgets/ui_services.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:file_selector/file_selector.dart' as fs; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:pdf_signature/data/services/export_service.dart'; - -/// Global exporting flag used to disable parts of the UI during long tasks. -final exportingProvider = StateProvider((ref) => false); - -/// Provider for the export service. Can be overridden in tests. -final exportServiceProvider = Provider((ref) => ExportService()); - -/// Provider for a function that picks a save path. Tests may override. -final savePathPickerProvider = Provider Function()>((ref) { - return () async { - // Desktop save dialog with PDF filter; mobile platforms may not support this. - final group = fs.XTypeGroup(label: 'PDF', extensions: ['pdf']); - final location = await fs.getSaveLocation( - acceptedTypeGroups: [group], - suggestedName: 'signed.pdf', - confirmButtonText: 'Save', - ); - return location?.path; // null if user cancels - }; -}); diff --git a/test/features/_test_helper.dart b/test/features/_test_helper.dart index 697d31f..ba6b28e 100644 --- a/test/features/_test_helper.dart +++ b/test/features/_test_helper.dart @@ -7,7 +7,7 @@ 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/widgets/ui_services.dart'; +import 'package:pdf_signature/ui/features/pdf/view_model/pdf_export_view_model.dart'; import 'package:pdf_signature/data/services/export_service.dart'; import 'package:pdf_signature/domain/models/model.dart'; @@ -51,8 +51,13 @@ Future pumpApp( pdfViewModelProvider.overrideWith( (ref) => PdfViewModel(ref, useMockViewer: true), ), - exportServiceProvider.overrideWith((ref) => fakeExport), - savePathPickerProvider.overrideWith((ref) => () async => 'out.pdf'), + pdfExportViewModelProvider.overrideWith( + (ref) => PdfExportViewModel( + ref, + exporter: fakeExport, + savePathPicker: () async => 'out.pdf', + ), + ), ], ); await tester.pumpWidget( diff --git a/test/widget/export_flow_test.dart b/test/widget/export_flow_test.dart index f5d0282..f27cba0 100644 --- a/test/widget/export_flow_test.dart +++ b/test/widget/export_flow_test.dart @@ -7,7 +7,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; 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/ui/features/pdf/view_model/pdf_export_view_model.dart'; import 'package:pdf_signature/data/repositories/preferences_repository.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart'; @@ -62,9 +62,12 @@ void main() { pdfViewModelProvider.overrideWith( (ref) => PdfViewModel(ref, useMockViewer: true), ), - exportServiceProvider.overrideWith((_) => fake), - savePathPickerProvider.overrideWith( - (_) => () async => 'C:/tmp/output.pdf', + pdfExportViewModelProvider.overrideWith( + (ref) => PdfExportViewModel( + ref, + exporter: fake, + savePathPicker: () async => 'C:/tmp/output.pdf', + ), ), ], child: MaterialApp( diff --git a/test/widget/helpers.dart b/test/widget/helpers.dart index 1820bf7..231d3ad 100644 --- a/test/widget/helpers.dart +++ b/test/widget/helpers.dart @@ -7,7 +7,7 @@ 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_view_model.dart'; -import 'package:pdf_signature/ui/features/pdf/widgets/ui_services.dart'; +import 'package:pdf_signature/ui/features/pdf/view_model/pdf_export_view_model.dart'; import 'package:pdf_signature/data/repositories/document_repository.dart'; import 'package:pdf_signature/data/repositories/signature_asset_repository.dart'; import 'package:pdf_signature/data/repositories/signature_card_repository.dart'; @@ -26,7 +26,9 @@ Future pumpWithOpenPdf(WidgetTester tester) async { pdfViewModelProvider.overrideWith( (ref) => PdfViewModel(ref, useMockViewer: true), ), - exportingProvider.overrideWith((ref) => false), + pdfExportViewModelProvider.overrideWith( + (ref) => PdfExportViewModel(ref), + ), ], child: MaterialApp( localizationsDelegates: AppLocalizations.localizationsDelegates, @@ -398,7 +400,9 @@ Future pumpWithOpenPdfAndSig(WidgetTester tester) async { pdfViewModelProvider.overrideWith( (ref) => PdfViewModel(ref, useMockViewer: true), ), - exportingProvider.overrideWith((ref) => false), + pdfExportViewModelProvider.overrideWith( + (ref) => PdfExportViewModel(ref), + ), ], child: MaterialApp( localizationsDelegates: AppLocalizations.localizationsDelegates,