feat(reader): add sticky progress bar with chapter ticks (#4707)
Add an always-visible, opt-in progress bar with chapter tick marks in the persistent footer, so reading progress no longer disappears like the hover footer slider does. - New StickyProgressBar: a 1px rounded-border capsule with a fill and chapter tick marks; display-only, e-ink aware, and RTL safe. Ticks render inside the clipped track so the rounded ends crop them and they never exceed the border. - Chapter ticks come from the TOC, mapped to spine-section start fractions (getChapterTickFractions); the first and last ticks are trimmed so they do not crowd the rounded ends. - Thread the overall size-domain reading fraction through setProgress so the bar fill aligns with the tick domain. - Footer layout: when enabled the bar grows on the left and the info widgets group to the right with even spacing; otherwise the existing layout is unchanged. - Horizontal writing mode only; vertical keeps the current footer. - Add the showStickyProgressBar view setting, a LayoutPanel toggle, and i18n. Closes #1616. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,45 @@
|
||||
import { localizeNumber } from './number';
|
||||
import type { TOCItem } from '@/libs/document';
|
||||
|
||||
interface ChapterTickSource {
|
||||
getSectionFractions: () => number[];
|
||||
resolveNavigation: (href: string) => { index?: number } | null | undefined;
|
||||
}
|
||||
|
||||
const flattenTOCHrefs = (items: TOCItem[]): string[] =>
|
||||
items.flatMap((item) => [
|
||||
...(item.href ? [item.href] : []),
|
||||
...(item.subitems?.length ? flattenTOCHrefs(item.subitems) : []),
|
||||
]);
|
||||
|
||||
/**
|
||||
* Reading-fraction positions (0..1) of chapter boundaries for the sticky
|
||||
* progress bar's tick marks. Each TOC entry's href is resolved to its spine
|
||||
* section index, then mapped to that section's start fraction (the same
|
||||
* size-domain as the bar fill). Ticks at the book start/end are dropped and
|
||||
* duplicates collapsed, so multiple TOC entries inside one spine file yield a
|
||||
* single tick. The first and last remaining ticks are also dropped so they
|
||||
* never crowd the bar's rounded ends.
|
||||
*/
|
||||
export function getChapterTickFractions(
|
||||
view: ChapterTickSource | null | undefined,
|
||||
toc: TOCItem[] | null | undefined,
|
||||
): number[] {
|
||||
if (!view || !toc?.length) return [];
|
||||
const sectionFractions = view.getSectionFractions();
|
||||
if (!sectionFractions?.length) return [];
|
||||
// sectionFractions = [0, ...interior boundaries..., 1]; section `i` starts at
|
||||
// sectionFractions[i]. Keep interior starts only (1 .. length-2).
|
||||
const lastIndex = sectionFractions.length - 1;
|
||||
const ticks = new Set<number>();
|
||||
for (const href of flattenTOCHrefs(toc)) {
|
||||
const index = view.resolveNavigation(href)?.index;
|
||||
if (typeof index === 'number' && index >= 1 && index < lastIndex) {
|
||||
ticks.add(sectionFractions[index]!);
|
||||
}
|
||||
}
|
||||
return [...ticks].sort((a, b) => a - b).slice(1, -1);
|
||||
}
|
||||
|
||||
export function formatProgress(
|
||||
current: number | undefined,
|
||||
|
||||
Reference in New Issue
Block a user