fix(reader): keep footer compact on mobile and clear text under chrome
Release / build-and-push (push) Successful in 2m34s
Release / build-and-push (push) Successful in 2m34s
On phones the footer's progress cell rendered the full chapter title (e.g. 'Long Chapter Name · 5 / 12') in a div with no max-width or nowrap, so the text wrapped to multiple lines and ballooned the bottom bar. Combined with viewport offsets that were computed from assumed pixel heights with ~0px margin, the book text slipped underneath the bars.
Chapter label in footer: split #progress-display into two spans (progressLabel hidden on mobile via 'hidden sm:inline', progressMain always shown) and cap it with 'truncate whitespace-nowrap max-w-[5rem] sm:max-w-none' so it can never wrap or grow the bar. Phones now show just '5 / 12'; larger screens keep 'Chapter · 5 / 12'.
Viewport offset: replace the fragile hardcoded calc() with runtime measurement. Gave the chrome bars ids (reader-topbar/reader-bottombar) and added updateViewportInsets(), which sets #reader-viewport top/bottom from each bar's real offsetHeight (which already includes env(safe-area-inset-*) padding) plus a 6px margin. It runs on init and refreshes on resize, orientationchange, and via a ResizeObserver, so the content area tracks the actual chrome height on any DPI, notch, home-indicator, or zoom level instead of guessing.
Refactored formatProgress into formatProgressParts (returns {label, main}; only chapter mode sets a label) with a setProgress() helper wiring progressLabel/progressMain/progressText across the relocate, cycleProgressMode, and applyProgressMode call sites. Rebuilt reader_templ.go and style.css.
This commit is contained in:
+93
-51
@@ -372,6 +372,8 @@ document.addEventListener("alpine:init", () => {
|
||||
interactionMode: "select" as string,
|
||||
magnifierEnabled: false,
|
||||
progressText: "",
|
||||
progressLabel: "",
|
||||
progressMain: "",
|
||||
sliderValue: 0,
|
||||
settings: null as ReaderSettings | null,
|
||||
justify: true,
|
||||
@@ -489,21 +491,20 @@ document.addEventListener("alpine:init", () => {
|
||||
tocItem,
|
||||
section,
|
||||
};
|
||||
const progressStr = this.formatProgress(
|
||||
const progressParts = this.formatProgressParts(
|
||||
fraction,
|
||||
location,
|
||||
pageItem,
|
||||
tocItem,
|
||||
section,
|
||||
);
|
||||
this.progressText = progressStr;
|
||||
this.setProgress(progressParts);
|
||||
this.sliderValue = fraction;
|
||||
const slider = document.getElementById(
|
||||
"progress-slider",
|
||||
) as HTMLInputElement;
|
||||
if (slider) {
|
||||
slider.value = fraction;
|
||||
slider.title = progressStr;
|
||||
}
|
||||
const range = e.detail.range as Range | undefined;
|
||||
if (range) {
|
||||
@@ -547,6 +548,34 @@ document.addEventListener("alpine:init", () => {
|
||||
}
|
||||
this.initTime = Date.now();
|
||||
this.fetchReadingSpeed();
|
||||
this.setupViewportInsets();
|
||||
},
|
||||
updateViewportInsets() {
|
||||
const top = document.getElementById("reader-topbar");
|
||||
const bot = document.getElementById("reader-bottombar");
|
||||
const vp = document.getElementById("reader-viewport");
|
||||
if (!top || !bot || !vp) return;
|
||||
const margin = 6;
|
||||
vp.style.setProperty("top", `${top.offsetHeight + margin}px`);
|
||||
vp.style.setProperty("bottom", `${bot.offsetHeight + margin}px`);
|
||||
},
|
||||
setupViewportInsets() {
|
||||
const top = document.getElementById("reader-topbar");
|
||||
const bot = document.getElementById("reader-bottombar");
|
||||
this.updateViewportInsets();
|
||||
window.addEventListener("resize", () => this.updateViewportInsets());
|
||||
window.addEventListener("orientationchange", () =>
|
||||
setTimeout(() => this.updateViewportInsets(), 250),
|
||||
);
|
||||
if (
|
||||
typeof ResizeObserver !== "undefined" &&
|
||||
top &&
|
||||
bot
|
||||
) {
|
||||
const ro = new ResizeObserver(() => this.updateViewportInsets());
|
||||
ro.observe(top);
|
||||
ro.observe(bot);
|
||||
}
|
||||
},
|
||||
debouncedSaveProgress(fraction: number, location: any, cfi: string) {
|
||||
if (Date.now() - this.initTime < 5000) return;
|
||||
@@ -774,19 +803,20 @@ document.addEventListener("alpine:init", () => {
|
||||
/* ignore bookmark errors for now */
|
||||
}
|
||||
},
|
||||
formatProgress(
|
||||
formatProgressParts(
|
||||
fraction: number,
|
||||
location: { current: number; next: number; total: number },
|
||||
pageItem: { id: number; label: string; href: string },
|
||||
tocItem: FoliateTocItem | null,
|
||||
section: { current: number; total: number },
|
||||
): string {
|
||||
): { label: string; main: string } {
|
||||
const percent = new Intl.NumberFormat("en", { style: "percent" }).format(
|
||||
fraction,
|
||||
);
|
||||
const chapterLabel = tocItem?.label ? `${tocItem.label} · ` : "";
|
||||
switch (this.progressMode) {
|
||||
case "percentage":
|
||||
return percent;
|
||||
return { label: "", main: percent };
|
||||
case "chapter": {
|
||||
if (this.isFixedLayout && tocItem && this.chapterBoundaries.length > 0) {
|
||||
const totalSections = this.book?.sections?.filter(
|
||||
@@ -809,10 +839,10 @@ document.addEventListener("alpine:init", () => {
|
||||
1,
|
||||
Math.min(currentPage - chapterStart + 1, chapterPages),
|
||||
);
|
||||
const label = tocItem.label
|
||||
? `${tocItem.label} · `
|
||||
: "";
|
||||
return `${label}${currentInChapter} / ${chapterPages}`;
|
||||
return {
|
||||
label: chapterLabel,
|
||||
main: `${currentInChapter} / ${chapterPages}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (tocItem && this.chapterBoundaries.length > 0) {
|
||||
@@ -836,10 +866,10 @@ document.addEventListener("alpine:init", () => {
|
||||
1,
|
||||
Math.min(currentPage - chapterStart + 1, chapterPages),
|
||||
);
|
||||
const label = tocItem.label
|
||||
? `${tocItem.label} · `
|
||||
: "";
|
||||
return `${label}${currentInChapter} / ${chapterPages}`;
|
||||
return {
|
||||
label: chapterLabel,
|
||||
main: `${currentInChapter} / ${chapterPages}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (section && this.sectionFractionsArr.length > 1) {
|
||||
@@ -859,16 +889,19 @@ document.addEventListener("alpine:init", () => {
|
||||
Math.round((fraction - startFrac) * pageBase),
|
||||
);
|
||||
const clamped = Math.min(currentInSec, totalInSec);
|
||||
const label = tocItem?.label
|
||||
? `${tocItem.label} · `
|
||||
: "";
|
||||
return `${label}${clamped} / ${totalInSec}`;
|
||||
return {
|
||||
label: chapterLabel,
|
||||
main: `${clamped} / ${totalInSec}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (location.total > 0) {
|
||||
return `${percent} · ${location.current} / ${location.total}`;
|
||||
return {
|
||||
label: "",
|
||||
main: `${percent} · ${location.current} / ${location.total}`,
|
||||
};
|
||||
}
|
||||
return percent;
|
||||
return { label: "", main: percent };
|
||||
}
|
||||
case "time-left": {
|
||||
if (this.readingSpeedPpm > 0 && location.total > 0) {
|
||||
@@ -877,30 +910,47 @@ document.addEventListener("alpine:init", () => {
|
||||
if (mins >= 60) {
|
||||
const hrs = Math.floor(mins / 60);
|
||||
const m = mins % 60;
|
||||
return `${percent} · ~${hrs}h ${m}m left`;
|
||||
return { label: "", main: `${percent} · ~${hrs}h ${m}m left` };
|
||||
}
|
||||
return `${percent} · ~${mins} min left`;
|
||||
return { label: "", main: `${percent} · ~${mins} min left` };
|
||||
}
|
||||
return `${percent} · ~-- min left`;
|
||||
return { label: "", main: `${percent} · ~-- min left` };
|
||||
}
|
||||
default: {
|
||||
if (this.isFixedLayout) {
|
||||
const pageInfo = this.getRenderedPageInfo();
|
||||
if (pageInfo) {
|
||||
return `${pageInfo.current} / ${pageInfo.total}`;
|
||||
return { label: "", main: `${pageInfo.current} / ${pageInfo.total}` };
|
||||
}
|
||||
}
|
||||
if (pageItem) {
|
||||
return `${percent} · Page ${pageItem.label}`;
|
||||
return { label: "", main: `${percent} · Page ${pageItem.label}` };
|
||||
}
|
||||
const pageInfoReflow = this.getRenderedPageInfo();
|
||||
if (pageInfoReflow) {
|
||||
return `${percent} · ${pageInfoReflow.current + 1} / ${pageInfoReflow.total}`;
|
||||
return {
|
||||
label: "",
|
||||
main: `${percent} · ${pageInfoReflow.current + 1} / ${pageInfoReflow.total}`,
|
||||
};
|
||||
}
|
||||
return `${percent} · ${location.current} / ${location.total}`;
|
||||
return {
|
||||
label: "",
|
||||
main: `${percent} · ${location.current} / ${location.total}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
setProgress(parts: { label: string; main: string }, sliderTitle?: string) {
|
||||
this.progressLabel = parts.label;
|
||||
this.progressMain = parts.main;
|
||||
this.progressText = parts.label + parts.main;
|
||||
const slider = document.getElementById(
|
||||
"progress-slider",
|
||||
) as HTMLInputElement;
|
||||
if (slider) {
|
||||
slider.title = sliderTitle ?? this.progressText;
|
||||
}
|
||||
},
|
||||
cycleProgressMode() {
|
||||
const modes = ["pages", "chapter", "percentage", "time-left"];
|
||||
const idx = modes.indexOf(this.progressMode);
|
||||
@@ -909,19 +959,15 @@ document.addEventListener("alpine:init", () => {
|
||||
if (this.lastRelocateDetail) {
|
||||
const { fraction, location, pageItem, tocItem, section } =
|
||||
this.lastRelocateDetail;
|
||||
this.progressText = this.formatProgress(
|
||||
fraction,
|
||||
location,
|
||||
pageItem,
|
||||
tocItem,
|
||||
section,
|
||||
this.setProgress(
|
||||
this.formatProgressParts(
|
||||
fraction,
|
||||
location,
|
||||
pageItem,
|
||||
tocItem,
|
||||
section,
|
||||
),
|
||||
);
|
||||
const slider = document.getElementById(
|
||||
"progress-slider",
|
||||
) as HTMLInputElement;
|
||||
if (slider) {
|
||||
slider.title = this.progressText;
|
||||
}
|
||||
}
|
||||
},
|
||||
async fetchReadingSpeed() {
|
||||
@@ -944,19 +990,15 @@ document.addEventListener("alpine:init", () => {
|
||||
if (this.lastRelocateDetail) {
|
||||
const { fraction, location, pageItem, tocItem, section } =
|
||||
this.lastRelocateDetail;
|
||||
this.progressText = this.formatProgress(
|
||||
fraction,
|
||||
location,
|
||||
pageItem,
|
||||
tocItem,
|
||||
section,
|
||||
this.setProgress(
|
||||
this.formatProgressParts(
|
||||
fraction,
|
||||
location,
|
||||
pageItem,
|
||||
tocItem,
|
||||
section,
|
||||
),
|
||||
);
|
||||
const slider = document.getElementById(
|
||||
"progress-slider",
|
||||
) as HTMLInputElement;
|
||||
if (slider) {
|
||||
slider.title = this.progressText;
|
||||
}
|
||||
}
|
||||
},
|
||||
computeFixedLayoutChapterBoundaries() {
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user