From 6ac2ed62429378d1b62e38e90225420e962640fa Mon Sep 17 00:00:00 2001 From: chy5301 Date: Sun, 30 Aug 2026 15:35:17 +0800 Subject: [PATCH] =?UTF-8?q?fix(reader):=20=E4=BF=AE=E6=AD=A3=E7=AB=A0?= =?UTF-8?q?=E8=8A=82=E5=86=85=E9=A1=B5=E7=A0=81=E6=8D=A2=E7=AE=97=EF=BC=8C?= =?UTF-8?q?=E5=8E=BB=E6=8E=89=E4=B8=8D=E5=AD=98=E5=9C=A8=E7=9A=84=E9=A6=96?= =?UTF-8?q?=E5=B0=BE=20padding=20=E5=81=87=E8=AE=BE=20(#745)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit renderer.page 是 0 基索引,renderer.pages 就是章节真实页数,paginator 并不会在章节首尾插入 padding 页。但两端都按「首尾各有一页 padding」换算, 把 current 压平在 [1, pages-2] 区间,导致:章首第 1、2 页都显示 1, 章尾最后两页都显示满值,显示的总页数比实际少 2。 改为 current = page + 1、total = pages,保留 clamp 以防边界滚动时的 瞬时越界。门槛 rendererPages > 2 一并放宽为 > 0——它原本只是为了配合 减 2 不越界而设,现在 1~2 屏的短章节也能正常显示页码,不再回退成全书 百分比。 补充:桌面端与移动端的阅读量统计都用 detail.page.current 判断是否翻页, 此前章首两页 current 相同会被判成同一页而漏计,一并修正。 同步重建 assets/reader/reader.html。 --- packages/app-expo/assets/reader/reader.html | 196 +++++++++--------- .../assets/reader/reader.template.html | 8 +- .../chapter-page-numbering-contract.test.ts | 45 ++++ .../src/components/reader/FoliateViewer.tsx | 8 +- 4 files changed, 154 insertions(+), 103 deletions(-) create mode 100644 packages/app-expo/src/screens/reader/chapter-page-numbering-contract.test.ts diff --git a/packages/app-expo/assets/reader/reader.html b/packages/app-expo/assets/reader/reader.html index 03d2d186a..856675891 100644 --- a/packages/app-expo/assets/reader/reader.html +++ b/packages/app-expo/assets/reader/reader.html @@ -343,17 +343,29 @@ } } - function getMobileReflowMaxInlineSize() { - const viewportHeight = window.innerHeight || document.documentElement.clientHeight || 720; - const viewportWidth = window.innerWidth || document.documentElement.clientWidth || 393; - return Math.ceil(Math.max(720, viewportHeight, viewportWidth)); - } - function applyMobileReflowRendererSizing(renderer) { if (!renderer) return; renderer.setAttribute('max-column-count', '1'); renderer.setAttribute('max-block-size', '1440px'); - renderer.setAttribute('max-inline-size', getMobileReflowMaxInlineSize() + 'px'); + } + + // Mobile is always single-page (no spread), so there is no column gutter to + // balance — the foliate gap only contributes the page's internal side + // padding. Same derivation as the desktop reader: paper = 99% of the window + // (breathing room even at margin 0), outer text inset = margin + 0.5%·H. + // All against renderer.clientWidth (foliate resolves the gap % against it). + function applyMobileReflowMargins(renderer, margin) { + if (!renderer) return; + const windowWidth = + renderer.clientWidth || window.innerWidth || document.documentElement.clientWidth || 393; + const paper = windowWidth * 0.99; + const g = margin / paper; + renderer.setAttribute('margin-top', margin + 'px'); + renderer.setAttribute('margin-bottom', margin + 'px'); + renderer.setAttribute('margin-left', '0px'); + renderer.setAttribute('margin-right', '0px'); + renderer.setAttribute('gap', g * 100 + '%'); + renderer.setAttribute('max-inline-size', paper - margin + g * windowWidth + 'px'); } function applyRendererFlowMode(renderer) { @@ -1639,10 +1651,12 @@ const rendererPages = view && view.renderer && typeof view.renderer.pages === 'number' ? view.renderer.pages : null; - const actualPage = rendererPage != null && rendererPages != null && rendererPages > 2 + // renderer.page is 0-based and renderer.pages is the real page count: + // the renderer adds no padding pages around a section. + const actualPage = rendererPage != null && rendererPages != null && rendererPages > 0 ? { - current: Math.max(1, Math.min(rendererPage, rendererPages - 2)), - total: Math.max(1, rendererPages - 2), + current: Math.min(Math.max(rendererPage + 1, 1), rendererPages), + total: rendererPages, } : null; postToRN('relocate', { @@ -1781,11 +1795,8 @@ const renderer = el.renderer; if (renderer && !el.isFixedLayout) { applyMobileReflowRendererSizing(renderer); - // Default gap/margin - const margin = msg.pageMargin || 16; - const gapPercent = Math.max(1, Math.round((margin / 393) * 100)); - renderer.setAttribute('gap', gapPercent + '%'); - renderer.setAttribute('margin', margin + 'px'); + // Default margins (synchronized with the desktop reader formula) + applyMobileReflowMargins(renderer, msg.pageMargin || 16); } else if (renderer && el.isFixedLayout) { const paginatedLayout = msg.paginatedLayout === 'single' || msg.paginatedLayout === 'double' @@ -2047,9 +2058,7 @@ const BASELINE_FONT_SIZE = 16; const layoutScale = currentFontSize / BASELINE_FONT_SIZE; const margin = Math.round(settings.pageMargin * layoutScale); - const gapPercent = Math.max(1, Math.round((margin / 393) * 100)); - renderer.setAttribute('gap', gapPercent + '%'); - renderer.setAttribute('margin', margin + 'px'); + applyMobileReflowMargins(renderer, margin); } if (view && view.isFixedLayout) { @@ -5045,8 +5054,8 @@ diff --git a/packages/app-expo/assets/reader/reader.template.html b/packages/app-expo/assets/reader/reader.template.html index 8f0cf51f0..ebb03c00b 100644 --- a/packages/app-expo/assets/reader/reader.template.html +++ b/packages/app-expo/assets/reader/reader.template.html @@ -1604,10 +1604,12 @@ const rendererPages = view && view.renderer && typeof view.renderer.pages === 'number' ? view.renderer.pages : null; - const actualPage = rendererPage != null && rendererPages != null && rendererPages > 2 + // renderer.page is 0-based and renderer.pages is the real page count: + // the renderer adds no padding pages around a section. + const actualPage = rendererPage != null && rendererPages != null && rendererPages > 0 ? { - current: Math.max(1, Math.min(rendererPage, rendererPages - 2)), - total: Math.max(1, rendererPages - 2), + current: Math.min(Math.max(rendererPage + 1, 1), rendererPages), + total: rendererPages, } : null; postToRN('relocate', { diff --git a/packages/app-expo/src/screens/reader/chapter-page-numbering-contract.test.ts b/packages/app-expo/src/screens/reader/chapter-page-numbering-contract.test.ts new file mode 100644 index 000000000..9ec731ee5 --- /dev/null +++ b/packages/app-expo/src/screens/reader/chapter-page-numbering-contract.test.ts @@ -0,0 +1,45 @@ +import { readFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; + +const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../../../.."); + +function readSource(relativePath: string): string { + return readFileSync(resolve(repositoryRoot, relativePath), "utf8"); +} + +describe("chapter page numbering", () => { + it("maps the 0-based renderer page onto a 1-based page number on both platforms", () => { + const desktopViewer = readSource("packages/app/src/components/reader/FoliateViewer.tsx"); + const template = readSource("packages/app-expo/assets/reader/reader.template.html"); + + for (const source of [desktopViewer, template]) { + expect(source).toMatch( + /current: Math\.min\(Math\.max\(rendererPage \+ 1, 1\), rendererPages\)/, + ); + expect(source).toMatch(/total: rendererPages,/); + } + }); + + it("no longer assumes padding pages around a section", () => { + const desktopViewer = readSource("packages/app/src/components/reader/FoliateViewer.tsx"); + const template = readSource("packages/app-expo/assets/reader/reader.template.html"); + + // renderer.pages is the real page count, so the old "minus one padding page on + // each side" conversion clamped the first and last page of every section. + for (const source of [desktopViewer, template]) { + expect(source).not.toContain("rendererPages - 2"); + expect(source).toMatch(/rendererPages != null && rendererPages > 0/); + } + }); + + it("keeps the built reader in sync with the template", () => { + const builtReader = readSource("packages/app-expo/assets/reader/reader.html"); + + expect(builtReader).toMatch( + /current: Math\.min\(Math\.max\(rendererPage \+ 1, 1\), rendererPages\)/, + ); + expect(builtReader).not.toContain("rendererPages - 2"); + }); +}); diff --git a/packages/app/src/components/reader/FoliateViewer.tsx b/packages/app/src/components/reader/FoliateViewer.tsx index 2ef4c5244..0e6b8c250 100644 --- a/packages/app/src/components/reader/FoliateViewer.tsx +++ b/packages/app/src/components/reader/FoliateViewer.tsx @@ -2060,13 +2060,15 @@ export const FoliateViewer = forwardRef viewRef.current?.renderer && typeof viewRef.current.renderer.pages === "number" ? viewRef.current.renderer.pages : null; + // renderer.page is 0-based and renderer.pages is the real page count: + // the renderer adds no padding pages around a section. const detail: RelocateDetail = - rendererPage != null && rendererPages != null && rendererPages > 2 + rendererPage != null && rendererPages != null && rendererPages > 0 ? { ...rawDetail, page: { - current: Math.max(1, Math.min(rendererPage, rendererPages - 2)), - total: Math.max(1, rendererPages - 2), + current: Math.min(Math.max(rendererPage + 1, 1), rendererPages), + total: rendererPages, }, } : rawDetail;