Skip to content

Commit 0585363

Browse files
committed
fix: stop truncated pagination from skipping chapter pages
Neighbor preloads no longer cancel in-flight layout; incomplete progressive results are fully re-paginated and not cached. Page-turn commit clamps indices so a one-page leftover cannot jump chapters.
1 parent 03c45ff commit 0585363

4 files changed

Lines changed: 90 additions & 19 deletions

File tree

lib/model/reader/chapter_content_loader.dart

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,9 +149,15 @@ class ChapterContentLoader {
149149
'native=${BookPager.isAvailable}',
150150
);
151151
} else {
152+
var layoutComplete = false;
152153
try {
153-
final outcome = await _paginator.paginateProgressive(r);
154+
// Neighbor preloads must not cancel each other / the open chapter.
155+
final outcome = await _paginator.paginateProgressive(
156+
r,
157+
cancelPrevious: false,
158+
);
154159
r.pages = outcome.pages;
160+
layoutComplete = outcome.complete;
155161
debugPrint(
156162
'[PagerEngine] progressive engine=${outcome.engine} '
157163
'pages=${r.pages.length} complete=${outcome.complete} '
@@ -162,25 +168,41 @@ class ChapterContentLoader {
162168
'progressive engine=${outcome.engine} pages=${r.pages.length} '
163169
'complete=${outcome.complete} reason=${outcome.fallbackReason ?? "-"}',
164170
);
171+
// Incomplete progressive results (cancelled mid-chapter) must not be
172+
// treated as final layout — one-page leftovers make "next" jump chapters.
173+
if (!outcome.complete) {
174+
AppLog.w(
175+
'Pager',
176+
'progressive incomplete idx=$idx pages=${r.pages.length} '
177+
'→ full re-paginate',
178+
);
179+
r.pages = await _paginator.paginate(r);
180+
layoutComplete = r.pages.isNotEmpty;
181+
}
165182
} catch (e, st) {
166183
AppLog.e('Read', 'paginateProgressive failed idx=$idx',
167184
error: e, stackTrace: st);
168185
r.pages = const [];
186+
layoutComplete = false;
169187
}
170188

171189
if (r.pages.isEmpty) {
172190
try {
173191
r.pages = await _paginator.paginate(r);
192+
layoutComplete = r.pages.isNotEmpty;
174193
} catch (e, st) {
175194
AppLog.e('Read', 'parseContentAsync retry failed idx=$idx',
176195
error: e, stackTrace: st);
177196
}
178197
if (r.pages.isEmpty) {
179198
r.pages = fallbackPages(r.chapterContent);
199+
layoutComplete = r.pages.isNotEmpty;
180200
}
181201
}
182202

183-
if (r.pages.isNotEmpty &&
203+
// Only persist complete layouts — never cache a truncated first page.
204+
if (layoutComplete &&
205+
r.pages.isNotEmpty &&
184206
contentSource != 'fail' &&
185207
contentSource != 'network-error-text' &&
186208
!r.chapterContent.startsWith('章节内容加载失败')) {

lib/model/reader/page_picture_resolver.dart

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -118,18 +118,25 @@ class PagePictureResolver {
118118
final current = curPageOf();
119119
if (b == null || current == null) return null;
120120
final i = b.pageIndex - 1;
121-
final key = _key(b.id, b.chapterIndex, i);
122-
if (cache.containsKey(key)) return cache[key];
121+
final inChapter = i >= 0;
123122

124-
final sw = kDebugMode ? (Stopwatch()..start()) : null;
125-
final ui.Picture pic;
126-
if (i < 0) {
127-
final previous = prePageOf();
123+
final String key;
124+
final previous = inChapter ? null : prePageOf();
125+
if (inChapter) {
126+
key = _key(b.id, b.chapterIndex, i);
127+
} else {
128128
if (previous == null || previous.pages.isEmpty) {
129129
_log('miss previous (no pre chapter)');
130130
return null;
131131
}
132-
pic = drawContent(previous, previous.pageOffsets - 1);
132+
key = _key(b.id, b.chapterIndex - 1, previous.pageOffsets - 1);
133+
}
134+
if (cache.containsKey(key)) return cache[key];
135+
136+
final sw = kDebugMode ? (Stopwatch()..start()) : null;
137+
final ui.Picture pic;
138+
if (!inChapter) {
139+
pic = drawContent(previous!, previous.pageOffsets - 1);
133140
} else {
134141
pic = drawContent(current, i);
135142
}
@@ -171,12 +178,21 @@ class PagePictureResolver {
171178
final current = curPageOf();
172179
if (b == null || current == null) return null;
173180
final i = b.pageIndex + 1;
174-
final key = _key(b.id, b.chapterIndex, i);
181+
final inChapter = i >= 0 && i < current.pageOffsets;
182+
183+
// Cross-chapter next must use the next chapter's key — never store chapter
184+
// N+1 page 0 under `book|N|pageOffsets` (pollutes cache / confuses warm).
185+
final String key;
186+
if (inChapter) {
187+
key = _key(b.id, b.chapterIndex, i);
188+
} else {
189+
key = _key(b.id, b.chapterIndex + 1, 0);
190+
}
175191
if (cache.containsKey(key)) return cache[key];
176192

177193
final sw = kDebugMode ? (Stopwatch()..start()) : null;
178194
final ui.Picture pic;
179-
if (i >= current.pageOffsets) {
195+
if (!inChapter) {
180196
final following = nextPageOf();
181197
if (following == null) {
182198
final target = b.chapterIndex + 1;

lib/model/reader/page_turn_committer.dart

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,24 +54,49 @@ class PageTurnCommitter {
5454
final dir = (offsetDifference is num)
5555
? offsetDifference.toDouble()
5656
: double.tryParse(offsetDifference?.toString() ?? '') ?? 0;
57+
if (dir == 0) return;
5758
final beforeCur = b.chapterIndex;
5859
final beforeIdx = b.pageIndex;
5960
final idx = b.pageIndex;
6061
final chapters = chaptersOf();
6162
final curLen = curPageOf()?.pageOffsets ?? 0;
6263

63-
if (idx == curLen - 1 && dir > 0) {
64-
_turnToNextChapter(b, chapters, beforeCur, beforeIdx, dir, curLen);
64+
// No laid-out pages yet — never treat as "last page → next chapter".
65+
if (curLen <= 0) {
66+
if (kDebugMode) {
67+
debugPrint(
68+
'[ReadModel] commitPageTurn ignored (no pages) '
69+
'$beforeCur:$beforeIdx dir=$dir',
70+
);
71+
}
6572
return;
6673
}
67-
if (idx == 0 && dir < 0) {
68-
_turnToPreviousChapter(b, beforeCur, beforeIdx, dir);
69-
return;
74+
75+
// Clamp a stale index before deciding chapter vs in-chapter turn.
76+
final safeIdx = idx < 0 ? 0 : (idx >= curLen ? curLen - 1 : idx);
77+
if (safeIdx != idx) {
78+
b.pageIndex = safeIdx;
79+
if (kDebugMode) {
80+
debugPrint(
81+
'[ReadModel] commitPageTurn clamp index $idx → $safeIdx '
82+
'(pages=$curLen)',
83+
);
84+
}
7085
}
86+
7187
if (dir > 0) {
72-
b.pageIndex += 1;
88+
// Only leave the chapter from its real last page.
89+
if (safeIdx >= curLen - 1) {
90+
_turnToNextChapter(b, chapters, beforeCur, safeIdx, dir, curLen);
91+
return;
92+
}
93+
b.pageIndex = safeIdx + 1;
7394
} else {
74-
b.pageIndex -= 1;
95+
if (safeIdx <= 0) {
96+
_turnToPreviousChapter(b, beforeCur, safeIdx, dir);
97+
return;
98+
}
99+
b.pageIndex = safeIdx - 1;
75100
}
76101
if (kDebugMode) {
77102
debugPrint(

lib/model/reader/text_paginator.dart

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,17 @@ class TextPaginator {
7575
bool shouldJustifyHeight = false,
7676
void Function(List<TextPage> pages, bool complete)? onProgress,
7777
bool firstPageFirst = true,
78+
/// When true (default for explicit user actions like font change), cancel
79+
/// any in-flight progressive job. Neighbor preloads pass false so they
80+
/// do not abort the chapter currently being laid out.
81+
bool cancelPrevious = true,
7882
}) async {
79-
cancelActive();
83+
if (cancelPrevious) {
84+
cancelActive();
85+
}
8086
final jobId = _nextJobId++;
87+
// Track latest job for cancelActive(); concurrent neighbor jobs keep their
88+
// own jobId and only stop if explicitly cancelled or superseded.
8189
_activeJobId = jobId;
8290

8391
final p = layoutParams(shouldJustifyHeight: shouldJustifyHeight);

0 commit comments

Comments
 (0)