Measure the bottom bars so the section rail is never hidden behind the mini player

This commit is contained in:
Jonathan Sykes
2026-09-22 14:09:03 +08:00
parent 530d07d920
commit 6c4571cb33
2 changed files with 51 additions and 4 deletions

View File

@@ -5440,7 +5440,7 @@ const SectionRail = (() => {
{ id: 'related', sel: '#relatedPanel', icon: '🔗', label: 'Related', fold: true },
{ id: 'list', sel: '#listPane', icon: '📋', label: 'List' },
];
const st = { active: '', btns: new Map(), labelTimer: null, raf: 0 };
const st = { active: '', btns: new Map(), labelTimer: null, raf: 0, bottom: -1 };
const rail = () => document.getElementById('sectionRail');
const folds = () => (data.settings.foldedCards ||= {});
const isFolded = (id) => !!folds()[id];
@@ -5524,6 +5524,7 @@ const SectionRail = (() => {
}
}
r.classList.remove('hidden');
placeRail();
paint();
}
@@ -5577,6 +5578,29 @@ const SectionRail = (() => {
return best || near;
}
// The mini player and the bottom nav are both position:fixed bottom:0, and
// either can be absent (nothing playing / landscape). Measure whatever is
// actually pinned down there and park the rail above it, rather than
// guessing an offset that is wrong on the next phone.
function placeRail() {
const r = rail();
if (!r || r.classList.contains('hidden')) return;
let occupied = 0;
for (const id of ['bottomNav', 'miniBar']) {
const el = document.getElementById(id);
if (!el || el.classList.contains('hidden')) continue;
const box = el.getBoundingClientRect();
if (!box.height) continue;
// Only count it when it really is pinned to the bottom edge.
if (box.bottom >= window.innerHeight - 4) occupied = Math.max(occupied, window.innerHeight - box.top);
}
const want = Math.round(occupied + 14);
if (st.bottom !== want) {
st.bottom = want;
r.style.setProperty('--rail-bottom', want + 'px');
}
}
function paint() {
for (const [id, btn] of st.btns) {
btn.classList.toggle('active', id === st.active);
@@ -5593,6 +5617,7 @@ const SectionRail = (() => {
// would leave the rail with nothing highlighted. build() is a no-op
// unless the set of visible cards actually changed.
build();
placeRail();
const id = currentId();
if (id && id !== st.active) { st.active = id; paint(); }
});
@@ -5607,7 +5632,22 @@ const SectionRail = (() => {
// keeps working when the scroller changes between browser and installed
// PWA (different elements scroll in each).
document.addEventListener('scroll', onScroll, { passive: true, capture: true });
window.addEventListener('resize', () => { build(); onScroll(); });
window.addEventListener('resize', () => { build(); placeRail(); onScroll(); });
// The mini player appears AFTER the scroll that reveals it, so a scroll
// handler alone measures the layout as it was a moment ago and parks the
// rail underneath it. Watch the two bottom bars for real.
const watch = ['bottomNav', 'miniBar'].map((id) => document.getElementById(id)).filter(Boolean);
if (watch.length && typeof MutationObserver !== 'undefined') {
// The mini bar slides in over 0.25s, so a measurement taken the instant
// it appears catches it mid-animation and lands the rail a few pixels
// too low. Measure now and again once it has settled.
const mo = new MutationObserver(() => { placeRail(); setTimeout(placeRail, 320); });
watch.forEach((el) => mo.observe(el, { attributes: true, attributeFilter: ['class', 'style'] }));
}
if (watch.length && typeof ResizeObserver !== 'undefined') {
const ro = new ResizeObserver(() => placeRail());
watch.forEach((el) => ro.observe(el));
}
build();
onScroll();
}
@@ -5616,6 +5656,7 @@ const SectionRail = (() => {
function refresh() {
SECTIONS.forEach(applyFold);
build();
placeRail();
onScroll();
}

View File

@@ -4230,6 +4230,12 @@ body.landscape-fs .player-stage { touch-action: none; } /* fullscreen (real or t
/* Phones only — on desktop the whole page is visible at once. */
@media (min-width: 861px) { .sec-rail { display: none !important; } }
@media (max-width: 860px) {
/* clear of the bottom nav / mini player */
.sec-rail { top: auto; bottom: 132px; transform: none; }
/* Sits above the bottom nav and the mini player. Both are position:fixed
bottom:0 and either can be absent, so SectionRail.placeRail() measures
them and overrides --rail-bottom; this is only the pre-measure default. */
.sec-rail {
top: auto;
bottom: var(--rail-bottom, 150px);
transform: none;
}
}