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:
Huang Xin
2026-06-22 01:03:24 +08:00
committed by GitHub
parent 9735f497db
commit c781aeddaa
13 changed files with 402 additions and 7 deletions
@@ -75,6 +75,9 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
const [showRemainingTime, setShowRemainingTime] = useState(viewSettings.showRemainingTime);
const [showRemainingPages, setShowRemainingPages] = useState(viewSettings.showRemainingPages);
const [showProgressInfo, setShowProgressInfo] = useState(viewSettings.showProgressInfo);
const [showStickyProgressBar, setShowStickyProgressBar] = useState(
viewSettings.showStickyProgressBar,
);
const [showCurrentTime, setShowCurrentTime] = useState(viewSettings.showCurrentTime);
const [use24HourClock, setUse24HourClock] = useState(viewSettings.use24HourClock);
const [showCurrentBatteryStatus, setShowCurrentBatteryStatus] = useState(
@@ -120,6 +123,7 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
showRemainingTime: setShowRemainingTime,
showRemainingPages: setShowRemainingPages,
showProgressInfo: setShowProgressInfo,
showStickyProgressBar: setShowStickyProgressBar,
showCurrentTime: setShowCurrentTime,
use24HourClock: setUse24HourClock,
showCurrentBatteryStatus: setShowCurrentBatteryStatus,
@@ -352,6 +356,18 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [showProgressInfo]);
useEffect(() => {
saveViewSettings(
envConfig,
bookKey,
'showStickyProgressBar',
showStickyProgressBar,
false,
false,
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [showStickyProgressBar]);
useEffect(() => {
saveViewSettings(envConfig, bookKey, 'showCurrentTime', showCurrentTime, false, false);
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -740,6 +756,13 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
data-setting-id='settings.layout.referencePageCount'
/>
)}
<SettingsSwitchRow
label={_('Show Progress Bar')}
checked={showStickyProgressBar}
disabled={!showFooter}
onChange={() => setShowStickyProgressBar(!showStickyProgressBar)}
data-setting-id='settings.layout.showStickyProgressBar'
/>
<SettingsSwitchRow
label={_('Show Current Time')}
checked={showCurrentTime}