feat: support export functionality on web by download
This commit is contained in:
parent
41eea5f00c
commit
a08f93e8d4
|
|
@ -30,6 +30,30 @@ class RecordingExporter extends ExportService {
|
|||
}
|
||||
}
|
||||
|
||||
// Lightweight fake exporter to avoid invoking heavy rasterization during tests
|
||||
class LightweightExporter extends ExportService {
|
||||
@override
|
||||
Future<Uint8List?> exportSignedPdfFromBytes({
|
||||
required Uint8List srcBytes,
|
||||
required Size uiPageSize,
|
||||
required Uint8List? signatureImageBytes,
|
||||
Map<int, List<SignaturePlacement>>? placementsByPage,
|
||||
Map<String, Uint8List>? libraryBytes,
|
||||
double targetDpi = 144.0,
|
||||
}) async {
|
||||
// Return minimal non-empty bytes; content isn't used further in tests
|
||||
return Uint8List.fromList([1, 2, 3]);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> saveBytesToFile({
|
||||
required Uint8List bytes,
|
||||
required String outputPath,
|
||||
}) async {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
|
|
@ -225,13 +249,13 @@ void main() {
|
|||
await tester.pumpAndSettle();
|
||||
final ctx = tester.element(find.byType(PdfSignatureHomePage));
|
||||
final container = ProviderScope.containerOf(ctx);
|
||||
expect(container.read(pdfViewModelProvider), 1);
|
||||
expect(container.read(pdfViewModelProvider).currentPage, 1);
|
||||
container.read(pdfViewModelProvider.notifier).jumpToPage(2);
|
||||
await tester.pumpAndSettle();
|
||||
expect(container.read(pdfViewModelProvider), 2);
|
||||
expect(container.read(pdfViewModelProvider).currentPage, 2);
|
||||
container.read(pdfViewModelProvider.notifier).jumpToPage(3);
|
||||
await tester.pumpAndSettle();
|
||||
expect(container.read(pdfViewModelProvider), 3);
|
||||
expect(container.read(pdfViewModelProvider).currentPage, 3);
|
||||
});
|
||||
|
||||
testWidgets('PDF View: zoom in/out', (tester) async {
|
||||
|
|
@ -319,7 +343,7 @@ void main() {
|
|||
await tester.pumpAndSettle();
|
||||
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);
|
||||
|
|
@ -332,7 +356,7 @@ void main() {
|
|||
expect(page3Thumb, findsOneWidget);
|
||||
await tester.tap(page3Thumb);
|
||||
await tester.pumpAndSettle();
|
||||
expect(container.read(pdfViewModelProvider), 3);
|
||||
expect(container.read(pdfViewModelProvider).currentPage, 3);
|
||||
});
|
||||
|
||||
testWidgets('PDF View: thumbnails scroll and select', (tester) async {
|
||||
|
|
@ -371,15 +395,70 @@ void main() {
|
|||
await tester.pumpAndSettle();
|
||||
final ctx = tester.element(find.byType(PdfSignatureHomePage));
|
||||
final container = ProviderScope.containerOf(ctx);
|
||||
expect(container.read(pdfViewModelProvider), 1);
|
||||
expect(container.read(pdfViewModelProvider).currentPage, 1);
|
||||
final sidebar = find.byType(PagesSidebar);
|
||||
expect(sidebar, findsOneWidget);
|
||||
await tester.drag(sidebar, const Offset(0, -200));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('1'), findsOneWidget);
|
||||
expect(container.read(pdfViewModelProvider), 1);
|
||||
expect(container.read(pdfViewModelProvider).currentPage, 1);
|
||||
await tester.tap(find.text('2'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(container.read(pdfViewModelProvider), 2);
|
||||
expect(container.read(pdfViewModelProvider).currentPage, 2);
|
||||
});
|
||||
|
||||
testWidgets('PDF View: tap viewer after export does not crash', (
|
||||
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),
|
||||
),
|
||||
exportServiceProvider.overrideWith((ref) => LightweightExporter()),
|
||||
savePathPickerProvider.overrideWith(
|
||||
(_) => () async => 'C:/tmp/output-after-export.pdf',
|
||||
),
|
||||
],
|
||||
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();
|
||||
|
||||
// Trigger export
|
||||
await tester.tap(find.byKey(const Key('btn_save_pdf')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Tap on the page area; should not crash
|
||||
final pageArea = find.byKey(const ValueKey('pdf_page_area'));
|
||||
expect(pageArea, findsOneWidget);
|
||||
await tester.tap(pageArea);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Still present and responsive
|
||||
expect(pageArea, findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class PdfViewModel extends ChangeNotifier {
|
|||
PdfViewModel(this.ref, {bool? useMockViewer})
|
||||
: _useMockViewer =
|
||||
useMockViewer ??
|
||||
bool.fromEnvironment('FLUTTER_TEST', defaultValue: false);
|
||||
const bool.fromEnvironment('FLUTTER_TEST', defaultValue: false);
|
||||
|
||||
bool get useMockViewer => _useMockViewer;
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import 'pdf_page_area.dart';
|
|||
import 'pages_sidebar.dart';
|
||||
import 'signatures_sidebar.dart';
|
||||
import 'ui_services.dart';
|
||||
import 'package:pdf_signature/utils/download.dart';
|
||||
import '../view_model/pdf_view_model.dart';
|
||||
|
||||
class PdfSignatureHomePage extends ConsumerStatefulWidget {
|
||||
|
|
@ -168,6 +169,21 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
|||
if (out != null) {
|
||||
ok = await exporter.saveBytesToFile(bytes: out, outputPath: fullPath);
|
||||
}
|
||||
} else {
|
||||
// Web: export and trigger browser download
|
||||
final src = pdf.pickedPdfBytes ?? Uint8List(0);
|
||||
final out = await exporter.exportSignedPdfFromBytes(
|
||||
srcBytes: src,
|
||||
uiPageSize: _pageSize,
|
||||
signatureImageBytes: null,
|
||||
placementsByPage: pdf.placementsByPage,
|
||||
targetDpi: targetDpi,
|
||||
);
|
||||
if (out != null) {
|
||||
// Use a sensible default filename (cannot prompt path on web)
|
||||
ok = await downloadBytes(out, filename: 'signed.pdf');
|
||||
savedPath = 'signed.pdf';
|
||||
}
|
||||
}
|
||||
if (!kIsWeb) {
|
||||
if (ok) {
|
||||
|
|
@ -185,6 +201,19 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
|
|||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Web: show a toast-like confirmation
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
ok
|
||||
? AppLocalizations.of(
|
||||
context,
|
||||
).savedWithPath(savedPath ?? 'signed.pdf')
|
||||
: AppLocalizations.of(context).failedToSavePdf,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
ref.read(exportingProvider.notifier).state = false;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'download_stub.dart' if (dart.library.html) 'download_web.dart' as impl;
|
||||
|
||||
/// Initiates a platform-appropriate download/save operation.
|
||||
///
|
||||
/// On Web: triggers a browser download with the provided filename.
|
||||
/// On non-Web: returns false (no-op). Use your existing IO save flow instead.
|
||||
Future<bool> downloadBytes(Uint8List bytes, {required String filename}) {
|
||||
return impl.downloadBytes(bytes, filename: filename);
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
Future<bool> downloadBytes(Uint8List bytes, {required String filename}) async {
|
||||
// Not supported on non-web. Return false so caller can fallback to file save.
|
||||
return false;
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
// ignore: avoid_web_libraries_in_flutter
|
||||
import 'dart:html' as html;
|
||||
import 'dart:typed_data';
|
||||
|
||||
Future<bool> downloadBytes(Uint8List bytes, {required String filename}) async {
|
||||
try {
|
||||
final blob = html.Blob([bytes], 'application/pdf');
|
||||
final url = html.Url.createObjectUrlFromBlob(blob);
|
||||
final anchor =
|
||||
html.document.createElement('a') as html.AnchorElement
|
||||
..href = url
|
||||
..download = filename
|
||||
..style.display = 'none';
|
||||
html.document.body?.children.add(anchor);
|
||||
anchor.click();
|
||||
anchor.remove();
|
||||
html.Url.revokeObjectUrl(url);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue