import 'dart:convert'; import 'dart:typed_data'; import 'package:halkyon_learn_flutter/src/feature/pdf_source/data/pdf_engine.dart'; import 'package:material_ui/material_ui.dart'; /// A [PdfEngine] without PDFium, which `flutter test` can't load. A PDF's /// bytes say what it holds: `pages:12` its page count (10 without), and /// `chapter::;` each chapter. Its viewer shows "Page N of M" /// and does what it is asked at once; [select] plays a selection. class FakePdfEngine() implements PdfEngine { /// The viewers built, the last one showing. final viewers = <FakePdfViewer>[]; /// The regions drawn, in order. final rendered = <({int page, Rect region})>[]; /// What [PdfDocumentView.renderRegion] gives: a PNG's bytes. Uint8List image = Uint8List.fromList(const [137, 80, 78, 71, 1, 2, 3]); /// Whether the next PDF opened can't be read. bool failNext = false; /// Selects [text] in the viewer showing. void select(String text) => viewers.last.controller.report(selectedText: text); @override Future<PdfDocumentView> open(Uint8List bytes) async { final text = latin1.decode(bytes, allowInvalid: true); if (failNext || !text.startsWith('%PDF-')) { failNext = false; throw const FormatException('Not a PDF.'); } final pages = int.tryParse( RegExp(r'pages:(\d+)').firstMatch(text)?.group(1) ?? '', ) ?? 10; return _FakeDocument(this, pages, [ for (final match in RegExp(r'chapter:(\d+):([^;]+);').allMatches(text)) (title: match[2]!, page: int.parse(match[1]!)), ]); } } class _FakeDocument( final FakePdfEngine _engine, final int _pages, final List<PdfChapter> _chapters, ) implements PdfDocumentView { @override int get pageCount => _pages; @override Future<List<PdfChapter>> chapters() async => _chapters; @override Future<Uint8List?> renderRegion(int page, Rect region) async { _engine.rendered.add((page: page, region: region)); return _engine.image; } final _viewers = <PdfReaderController, FakePdfViewer>{}; @override Widget viewer(PdfReaderController controller) { // One viewer per controller, however often the reader is built. final viewer = _viewers.putIfAbsent(controller, () { final viewer = FakePdfViewer(controller, _pages); _engine.viewers.add(viewer); return viewer; }); return _FakeViewerWidget(viewer); } @override Future<void> dispose() async {} } /// The fake viewer's handle: pages 1000 high, 700 wide, one below the other. class FakePdfViewer(final PdfReaderController controller, final int pages) implements PdfViewerHandle { /// The texts searched for, in order. final searched = <String>[]; @override Future<void> goToPage(int page) async => controller.report(page: page.clamp(1, pages)); @override Future<void> zoomIn() async => controller.report(zoom: controller.zoom * 1.25); @override Future<void> zoomOut() async => controller.report(zoom: controller.zoom / 1.25); @override Future<void> fitWidth() async => controller.report(zoom: 1); @override Future<void> fitHeight() async => controller.report(zoom: 0.5); @override Future<void> setZoom(double zoom) async => controller.report(zoom: zoom); @override void search(String text) { searched.add(text); controller.report(matchCount: 3, currentMatch: 0); } @override Future<void> nextMatch() async => controller.report( currentMatch: ((controller.currentMatch ?? -1) + 1) % 3, ); @override Future<void> previousMatch() async => controller.report( currentMatch: ((controller.currentMatch ?? 1) + 2) % 3, ); @override void clearSearch() => controller.report(matchCount: 0, noMatch: true); @override Rect? pageRect(int page) => Rect.fromLTWH(0, (page - controller.page) * 1000.0, 700, 1000); } class const _FakeViewerWidget(final FakePdfViewer _viewer) extends StatefulWidget { @override State<_FakeViewerWidget> createState() => _FakeViewerWidgetState(); } class _FakeViewerWidgetState() extends State<_FakeViewerWidget> { @override void initState() { super.initState(); widget._viewer.controller.addListener(_changed); // As pdfrx's viewer, ready once laid out. WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { widget._viewer.controller.attach(widget._viewer); } }); } @override void dispose() { widget._viewer.controller ..removeListener(_changed) ..attach(null); super.dispose(); } void _changed() => setState(() {}); @override Widget build(BuildContext context) => ColoredBox( color: const Color(0xffdddddd), child: Center( child: Text( 'Page ${widget._viewer.controller.page} of ${widget._viewer.pages}', ), ), ); }