diff --git a/lib/src/engine/epub/epub_reader_engine.dart b/lib/src/engine/epub/epub_reader_engine.dart index 7a59521..341c281 100644 --- a/lib/src/engine/epub/epub_reader_engine.dart +++ b/lib/src/engine/epub/epub_reader_engine.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:math' as math; import 'package:flutter/widgets.dart'; @@ -356,7 +357,10 @@ final class EpubReaderEngine extends ReaderEngine { await _goTo( _locatorFor( position.chapter, - local: progress == 1 ? 1 : 0, + local: progress == 1 + ? 1 + : position.offset / + math.max(1, _progressIndex!.lengths[position.chapter]), textOffset: position.offset, ), seek: seek, diff --git a/lib/src/engine/epub/epub_viewport.dart b/lib/src/engine/epub/epub_viewport.dart index 8a49623..b09bfd6 100644 --- a/lib/src/engine/epub/epub_viewport.dart +++ b/lib/src/engine/epub/epub_viewport.dart @@ -522,7 +522,11 @@ final class _EpubViewportState extends State { return true; } - void _publishPage(int index, {int? restoredOffset}) { + void _publishPage( + int index, { + int? restoredOffset, + bool schedulePrefetch = true, + }) { if (_restoring || _busy || _pages.isEmpty) { return; } @@ -558,7 +562,9 @@ final class _EpubViewportState extends State { coverageEnd: coverageEnd, atEnd: complete && after == _pages.length, ); - _schedulePrefetch(); + if (schedulePrefetch) { + _schedulePrefetch(); + } } void _scrollChanged() { @@ -835,7 +841,19 @@ final class _EpubViewportState extends State { wasComplete != current.complete) { setState(() {}); } - _publishPageCount(current); + if (!wasComplete && + current.complete && + widget.engine.preferences.layoutMode == + ReaderLayoutMode.paginated) { + _publishPage( + _pageIndex, + restoredOffset: widget.engine.locator.textOffset, + schedulePrefetch: false, + ); + } else { + _publishPageCount(current); + } + _warmImages( current.pages, (_pageIndex - _spreadsBehind) * _columns, diff --git a/test/engine/epub_content_renderer_test.dart b/test/engine/epub_content_renderer_test.dart index 24ffd99..aea6798 100644 --- a/test/engine/epub_content_renderer_test.dart +++ b/test/engine/epub_content_renderer_test.dart @@ -1,8 +1,52 @@ -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:papyrus_reader/papyrus_reader.dart'; +import '../support/in_process_epub_worker.dart'; +import '../support/synthetic_epub.dart'; + void main() { + testWidgets('progress seeks restore the custom renderer within a chapter', ( + tester, + ) async { + final renderer = _TrackingRenderer(); + final engine = EpubReaderEngine( + renderer: renderer, + workerFactory: () async => InProcessEpubWorker(), + ); + addTearDown(engine.dispose); + await engine.load( + ReaderDocument( + id: 'custom-scroll', + format: ReaderFormat.epub, + loadBytes: () async => syntheticEpub( + singleSpine: true, + firstChapterBody: + '

${List.filled(200, "Scrollable text").join(" ")}

', + ), + ), + preferences: const ReaderPreferences(layoutMode: ReaderLayoutMode.scroll), + ); + await tester.pumpWidget( + MaterialApp( + home: ListenableBuilder( + listenable: engine, + builder: (context, _) => engine.buildViewport(context), + ), + ), + ); + await tester.pumpAndSettle(); + for (final progress in [.25, .75, 1.0]) { + await pumpReaderCommand(tester, engine.goToProgress(progress)); + expect( + renderer.controller.offset / + renderer.controller.position.maxScrollExtent, + closeTo(progress, .01), + ); + expect(engine.locator.totalProgression, closeTo(progress, .01)); + } + }); + testWidgets('programmatic restoration does not publish progress', ( tester, ) async { diff --git a/test/presentation/epub_prefetch_test.dart b/test/presentation/epub_prefetch_test.dart index dbeee3e..e24ab49 100644 --- a/test/presentation/epub_prefetch_test.dart +++ b/test/presentation/epub_prefetch_test.dart @@ -9,6 +9,33 @@ import '../support/in_process_epub_worker.dart'; import '../support/synthetic_epub.dart'; void main() { + testWidgets( + 'prefetch reports completion when the entire book fits on one page', + (tester) async { + final engine = testEpubEngine(); + await engine.load( + ReaderDocument( + id: 'one-page', + format: ReaderFormat.epub, + loadBytes: () async => syntheticEpub( + singleSpine: true, + firstChapterBody: '

A complete short book.

', + ), + ), + preferences: const ReaderPreferences(pageTurnAnimation: false), + ); + await tester.pumpWidget( + MaterialApp(home: Builder(builder: engine.buildViewport)), + ); + await tester.pumpAndSettle(); + expect(engine.snapshot.contentReady, isTrue); + expect(engine.snapshot.atEnd, isTrue); + expect(engine.locator.totalProgression, 1); + expect(engine.snapshot.coverage.single.end, 1); + expect(engine.locator.textOffset, 0); + }, + ); + testWidgets( 'an immediate turn can use a page prepared before the viewport grows', (tester) async {