refactor: ui_services.dart to PdfExportViewModel for export functionality

This commit is contained in:
insleker 2025-09-18 21:31:30 +08:00
parent eee75f6fdb
commit 0c38178502
8 changed files with 104 additions and 55 deletions

View File

@ -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/data/repositories/document_repository.dart';
import 'package:pdf_signature/domain/models/model.dart'; import 'package:pdf_signature/domain/models/model.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_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/pages_sidebar.dart';
import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart'; import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
@ -77,13 +77,16 @@ void main() {
pdfViewModelProvider.overrideWith( pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: false), (ref) => PdfViewModel(ref, useMockViewer: false),
), ),
exportServiceProvider.overrideWith((_) => fake), pdfExportViewModelProvider.overrideWith(
savePathPickerProvider.overrideWith( (ref) => PdfExportViewModel(
(_) => () async { ref,
exporter: fake,
savePathPicker: () async {
final dir = Directory.systemTemp.createTempSync('pdfsig_'); final dir = Directory.systemTemp.createTempSync('pdfsig_');
return '${dir.path}/output.pdf'; return '${dir.path}/output.pdf';
}, },
), ),
),
], ],
child: MaterialApp( child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,
@ -432,13 +435,18 @@ void main() {
pdfViewModelProvider.overrideWith( pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: false), (ref) => PdfViewModel(ref, useMockViewer: false),
), ),
exportServiceProvider.overrideWith((ref) => LightweightExporter()), pdfExportViewModelProvider.overrideWith(
savePathPickerProvider.overrideWith( (ref) => PdfExportViewModel(
(_) => () async { ref,
final dir = Directory.systemTemp.createTempSync('pdfsig_after_'); exporter: LightweightExporter(),
savePathPicker: () async {
final dir = Directory.systemTemp.createTempSync(
'pdfsig_after_',
);
return '${dir.path}/output-after-export.pdf'; return '${dir.path}/output-after-export.pdf';
}, },
), ),
),
], ],
child: MaterialApp( child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,

View File

@ -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<String?> Function() _savePathPicker;
PdfExportViewModel(
this.ref, {
ExportService? exporter,
Future<String?> 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<String?> pickSavePath() async {
return _savePathPicker();
}
static Future<String?> _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<PdfExportViewModel>((
ref,
) {
return PdfExportViewModel(ref);
});

View File

@ -12,7 +12,7 @@ import 'pdf_toolbar.dart';
import 'pdf_page_area.dart'; import 'pdf_page_area.dart';
import 'pages_sidebar.dart'; import 'pages_sidebar.dart';
import 'signatures_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 'package:pdf_signature/utils/download.dart';
import '../view_model/pdf_view_model.dart'; import '../view_model/pdf_view_model.dart';
@ -133,7 +133,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
} }
Future<void> _saveSignedPdf() async { Future<void> _saveSignedPdf() async {
ref.read(exportingProvider.notifier).state = true; ref.read(pdfExportViewModelProvider.notifier).setExporting(true);
try { try {
final pdf = _viewModel.document; final pdf = _viewModel.document;
final messenger = ScaffoldMessenger.of(context); final messenger = ScaffoldMessenger.of(context);
@ -145,7 +145,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
); );
return; return;
} }
final exporter = ref.read(exportServiceProvider); final exporter = ref.read(pdfExportViewModelProvider).exporter;
// get DPI from preferences // get DPI from preferences
final targetDpi = ref.read(preferencesRepositoryProvider).exportDpi; final targetDpi = ref.read(preferencesRepositoryProvider).exportDpi;
@ -153,8 +153,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
String? savedPath; String? savedPath;
if (!kIsWeb) { if (!kIsWeb) {
final pick = ref.read(savePathPickerProvider); final path = await ref.read(pdfExportViewModelProvider).pickSavePath();
final path = await pick();
if (path == null || path.trim().isEmpty) return; if (path == null || path.trim().isEmpty) return;
final fullPath = _ensurePdfExtension(path.trim()); final fullPath = _ensurePdfExtension(path.trim());
savedPath = fullPath; savedPath = fullPath;
@ -216,7 +215,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
); );
} }
} finally { } finally {
ref.read(exportingProvider.notifier).state = false; ref.read(pdfExportViewModelProvider.notifier).setExporting(false);
} }
} }
@ -362,7 +361,7 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
} }
Widget _buildScaffold(BuildContext context) { Widget _buildScaffold(BuildContext context) {
final isExporting = ref.watch(exportingProvider); final isExporting = ref.watch(pdfExportViewModelProvider).exporting;
final l = AppLocalizations.of(context); final l = AppLocalizations.of(context);
return Scaffold( return Scaffold(
body: Padding( body: Padding(

View File

@ -4,7 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdf_signature/l10n/app_localizations.dart'; import 'package:pdf_signature/l10n/app_localizations.dart';
import '../../signature/widgets/signature_drawer.dart'; import '../../signature/widgets/signature_drawer.dart';
import 'ui_services.dart'; import '../view_model/pdf_export_view_model.dart';
class SignaturesSidebar extends ConsumerWidget { class SignaturesSidebar extends ConsumerWidget {
const SignaturesSidebar({ const SignaturesSidebar({
@ -21,7 +21,7 @@ class SignaturesSidebar extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final l = AppLocalizations.of(context); final l = AppLocalizations.of(context);
final isExporting = ref.watch(exportingProvider); final isExporting = ref.watch(pdfExportViewModelProvider).exporting;
return AbsorbPointer( return AbsorbPointer(
absorbing: isExporting, absorbing: isExporting,
child: Card( child: Card(

View File

@ -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<bool>((ref) => false);
/// Provider for the export service. Can be overridden in tests.
final exportServiceProvider = Provider<ExportService>((ref) => ExportService());
/// Provider for a function that picks a save path. Tests may override.
final savePathPickerProvider = Provider<Future<String?> 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
};
});

View File

@ -7,7 +7,7 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'package:pdf_signature/app.dart'; import 'package:pdf_signature/app.dart';
import 'package:pdf_signature/data/repositories/preferences_repository.dart'; import 'package:pdf_signature/data/repositories/preferences_repository.dart';
import 'package:pdf_signature/data/repositories/document_repository.dart'; import 'package:pdf_signature/data/repositories/document_repository.dart';
import 'package:pdf_signature/ui/features/pdf/widgets/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/data/services/export_service.dart';
import 'package:pdf_signature/domain/models/model.dart'; import 'package:pdf_signature/domain/models/model.dart';
@ -51,8 +51,13 @@ Future<ProviderContainer> pumpApp(
pdfViewModelProvider.overrideWith( pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: true), (ref) => PdfViewModel(ref, useMockViewer: true),
), ),
exportServiceProvider.overrideWith((ref) => fakeExport), pdfExportViewModelProvider.overrideWith(
savePathPickerProvider.overrideWith((ref) => () async => 'out.pdf'), (ref) => PdfExportViewModel(
ref,
exporter: fakeExport,
savePathPicker: () async => 'out.pdf',
),
),
], ],
); );
await tester.pumpWidget( await tester.pumpWidget(

View File

@ -7,7 +7,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:pdf_signature/data/services/export_service.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/data/repositories/preferences_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_view_model.dart';
@ -62,9 +62,12 @@ void main() {
pdfViewModelProvider.overrideWith( pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: true), (ref) => PdfViewModel(ref, useMockViewer: true),
), ),
exportServiceProvider.overrideWith((_) => fake), pdfExportViewModelProvider.overrideWith(
savePathPickerProvider.overrideWith( (ref) => PdfExportViewModel(
(_) => () async => 'C:/tmp/output.pdf', ref,
exporter: fake,
savePathPicker: () async => 'C:/tmp/output.pdf',
),
), ),
], ],
child: MaterialApp( child: MaterialApp(

View File

@ -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/widgets/pdf_screen.dart';
import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart'; import 'package:pdf_signature/ui/features/pdf/view_model/pdf_view_model.dart';
import 'package:pdf_signature/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/document_repository.dart';
import 'package:pdf_signature/data/repositories/signature_asset_repository.dart'; import 'package:pdf_signature/data/repositories/signature_asset_repository.dart';
import 'package:pdf_signature/data/repositories/signature_card_repository.dart'; import 'package:pdf_signature/data/repositories/signature_card_repository.dart';
@ -26,7 +26,9 @@ Future<void> pumpWithOpenPdf(WidgetTester tester) async {
pdfViewModelProvider.overrideWith( pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: true), (ref) => PdfViewModel(ref, useMockViewer: true),
), ),
exportingProvider.overrideWith((ref) => false), pdfExportViewModelProvider.overrideWith(
(ref) => PdfExportViewModel(ref),
),
], ],
child: MaterialApp( child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,
@ -398,7 +400,9 @@ Future<void> pumpWithOpenPdfAndSig(WidgetTester tester) async {
pdfViewModelProvider.overrideWith( pdfViewModelProvider.overrideWith(
(ref) => PdfViewModel(ref, useMockViewer: true), (ref) => PdfViewModel(ref, useMockViewer: true),
), ),
exportingProvider.overrideWith((ref) => false), pdfExportViewModelProvider.overrideWith(
(ref) => PdfExportViewModel(ref),
),
], ],
child: MaterialApp( child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,