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;