Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 46 additions & 39 deletions packages/app-expo/assets/reader/reader.html

Large diffs are not rendered by default.

37 changes: 22 additions & 15 deletions packages/app-expo/assets/reader/reader.template.html
Original file line number Diff line number Diff line change
Expand Up @@ -292,17 +292,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) {
Expand Down Expand Up @@ -1715,11 +1727,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'
Expand Down Expand Up @@ -1973,9 +1982,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) {
Expand Down
63 changes: 51 additions & 12 deletions packages/app/src/components/reader/FoliateViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2964,20 +2964,29 @@ export const FoliateViewer = forwardRef<FoliateViewerHandle, FoliateViewerProps>
useEffect(() => {
const view = viewRef.current;
if (!view?.renderer) return;
if (isFixedLayout) {
applyRendererSettings(view, viewSettings, true, appTheme);
return;
}

applyReflowLayoutSettings(view, viewSettings);
applyRendererSettings(view, viewSettings, isFixedLayout, appTheme);
}, [
viewSettings.viewMode,
viewSettings.paginatedLayout,
viewSettings.fixedLayoutZoom,
viewSettings.pageMargin,
isFixedLayout,
appTheme,
]);

// Re-scale the content area when the window resizes (max-inline-size is
// computed from renderer.clientWidth at book-open).
useEffect(() => {
const handleResize = () => {
const view = viewRef.current;
if (view?.renderer && !isFixedLayout) {
applyRendererSettings(view, viewSettings, false, appTheme);
}
};
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, [viewSettings, isFixedLayout, appTheme]);

const handleViewerShellClick = useCallback(
(event: {
target: EventTarget | null;
Expand Down Expand Up @@ -3292,14 +3301,36 @@ function applyRendererSettings(
}
renderer.setAttribute("spread", spreadMode);
} else {
// Reflowable: columns, sizes, margins
// Reflowable: columns, sizes, margins.
// Foliate's `gap` is a percentage that drives BOTH the outer spacing
// (col1/col5 centering + page side padding) and the inner column gutter.
// The two pages of a double spread are two columns of ONE iframe, so the
// center gutter is the multicol column-gap = effective gap / 2. Solving
// gutter = margin and outer = margin + breathing room (paper = 99% of the
// window, so the pages never touch the window edges even at margin 0)
// yields the formulas below. All against the RENDERER width (foliate
// resolves the gap % against it), no rounding → exact geometry.
const isSinglePage = (settings.paginatedLayout ?? "double") === "single";
const rendererWidth = Number(renderer.size || 0);
const singlePageInlineSize =
rendererWidth > 0 ? Math.round(Math.max(980, Math.min(rendererWidth * 0.94, 1600))) : 1280;
renderer.setAttribute("max-inline-size", isSinglePage ? `${singlePageInlineSize}px` : "760px");
const windowWidth =
renderer.clientWidth || window.innerWidth || document.documentElement?.clientWidth || 1280;
const margin = settings.pageMargin ?? 40;
const paper = windowWidth * 0.99;
if (isSinglePage) {
// single: outer = col1 (0.5%H + M/2) + side padding (gap_px/2 = M/2)
// = M + 0.5%H. max-inline = col3 + g·H with col3 = paper − M.
const g = margin / paper;
const inline = paper - margin + g * windowWidth;
renderer.setAttribute("max-inline-size", `${inline}px`);
renderer.setAttribute("gap", `${g * 100}%`);
} else {
// double: gutter = M and col3 = paper − M → g = 2M/(0.99H + M),
// max-inline-size = (paper − M + g·H)/2.
const g = (2 * margin) / (paper + margin);
const doublePageColumnSize = (paper - margin + g * windowWidth) / 2;
renderer.setAttribute("max-inline-size", `${doublePageColumnSize}px`);
renderer.setAttribute("gap", `${g * 100}%`);
}
renderer.setAttribute("max-block-size", "1440px");
renderer.setAttribute("gap", isSinglePage ? "1.2%" : "4.5%");
applyReflowLayoutSettings(view, settings);
}

Expand All @@ -3316,6 +3347,14 @@ function applyReflowLayoutSettings(view: FoliateView, settings: ViewSettings) {
(settings.paginatedLayout ?? "double") === "single" ? "1" : "2",
);

// Vertical page margins (the horizontal ones come from the gap + max-inline
// sizing in applyRendererSettings).
const margin = settings.pageMargin ?? 40;
renderer.setAttribute("margin-top", `${margin}px`);
renderer.setAttribute("margin-bottom", `${margin}px`);
renderer.setAttribute("margin-left", "0px");
renderer.setAttribute("margin-right", "0px");

if (settings.viewMode === "scroll") {
renderer.setAttribute("flow", "scrolled");
} else {
Expand Down
7 changes: 7 additions & 0 deletions packages/foliate-js/paginator.js
Original file line number Diff line number Diff line change
Expand Up @@ -1648,6 +1648,13 @@ export class Paginator extends HTMLElement {
this.render()
break
case 'gap':
// percentage (e.g. "7%"); sets --_gap which drives the column
// gap, the per-page side padding and the outer spacing. (This
// was previously conflated with `margin`, so --_gap never got
// updated from the attribute and stayed at its 7% default.)
this.#top.style.setProperty('--_gap', value)
this.render()
break
case 'margin':
this.#top.style.setProperty('--_margin-top', value)
this.#top.style.setProperty('--_margin-right', value)
Expand Down