some more features and fixes (title and show info)

This commit is contained in:
Simon
2026-09-07 11:50:45 +00:00
parent 0009574b77
commit e2632c962d
9 changed files with 363 additions and 109 deletions

View File

@@ -274,6 +274,61 @@ App.favorites = App.favorites || {};
const BAR_PAGE_AHEAD_PX = 800;
const barPage = { items: [], rendered: 0 };
// Bar titles are one line that scrolls when it doesn't fit, the same as the
// grid card's. Which one scrolls follows the grid too: with a real pointer
// it's the card under it, on touch the card nearest the middle of the strip.
// Animating every overflowing title at once turns the bar into a wall of
// moving text.
const barTitleEnv = {
useHoverFocus: window.matchMedia('(hover: hover) and (pointer: fine)').matches
};
const setBarTitleActive = function(card, active) {
const title = card && card.querySelector('.favorite-title');
if (!title) return;
card.classList.toggle('is-title-active', !!active && title.classList.contains('has-marquee'));
};
// Touch: the card nearest the centre of the visible strip is the one being
// read, so it is the one whose title scrolls.
const syncBarTitleActive = function(list) {
if (!list) return;
const listRect = list.getBoundingClientRect();
const centre = listRect.left + listRect.width / 2;
let best = null;
let bestDistance = Infinity;
const cards = list.querySelectorAll('.favorite-card');
cards.forEach((card) => {
const rect = card.getBoundingClientRect();
if (rect.right <= listRect.left || rect.left >= listRect.right) return;
const distance = Math.abs((rect.left + rect.width / 2) - centre);
if (distance < bestDistance) {
bestDistance = distance;
best = card;
}
});
cards.forEach((card) => setBarTitleActive(card, card === best));
};
// Measures every card in the strip. Cheap enough to redo wholesale: a card's
// width is fixed, so this only really runs when cards are added.
const measureBarTitles = function(list) {
const target = list || document.getElementById('favorites-list');
if (!target) return;
target.querySelectorAll('.favorite-card').forEach((card) => {
App.marquee.measure(card.querySelector('.favorite-title'),
card.querySelector('.favorite-title-text'));
});
if (!barTitleEnv.useHoverFocus) syncBarTitleActive(target);
};
// The display font arrives after the first render, and it changes how wide
// every title is -- so whatever was measured against the fallback font has
// to be measured again once the real one is in.
if (document.fonts && document.fonts.ready) {
document.fonts.ready.then(() => measureBarTitles()).catch(() => {});
}
App.favorites.renderBar = function() {
const bar = document.getElementById('favorites-bar');
const list = document.getElementById('favorites-list');
@@ -296,7 +351,16 @@ App.favorites = App.favorites || {};
// Assignment rather than addEventListener: renderBar runs on every
// favorite change, and this must not stack up handlers.
let scrollRaf = null;
list.onscroll = () => {
// Which card is centred changes as the strip moves, but only once
// per frame is worth measuring.
if (!barTitleEnv.useHoverFocus && !scrollRaf) {
scrollRaf = requestAnimationFrame(() => {
scrollRaf = null;
syncBarTitleActive(list);
});
}
if (barPage.rendered >= barPage.items.length) return;
const remaining = list.scrollWidth - (list.scrollLeft + list.clientWidth);
if (remaining <= BAR_PAGE_AHEAD_PX) appendBarPage(list);
@@ -336,7 +400,7 @@ App.favorites = App.favorites || {};
${durationText ? `<span class="video-duration">${durationText}</span>` : ''}
</div>
<div class="favorite-info">
<h4>${item.title}</h4>
<h4 class="favorite-title"><span class="favorite-title-text">${item.title}</span></h4>
</div>
`;
const thumb = card.querySelector('img');
@@ -372,9 +436,10 @@ App.favorites = App.favorites || {};
showInfoBtn.onclick = (event) => {
event.stopPropagation();
App.videos.closeAllMenus();
// Favorites deliberately store no resolved metadata, so pull
// it fresh before showing the full info dump.
App.videos.ensureFormats(item).then(() => App.ui.showInfo(item));
// Favorites deliberately store no resolved metadata; the
// panel opens on what the entry holds and resolves the rest
// itself.
App.ui.openInfo(item);
};
}
if (downloadBtn) {
@@ -394,7 +459,17 @@ App.favorites = App.favorites || {};
App.videos.handleSearch(uploader);
};
}
if (barTitleEnv.useHoverFocus) {
card.addEventListener('pointerenter', () => setBarTitleActive(card, true));
card.addEventListener('pointerleave', () => setBarTitleActive(card, false));
}
// Keyboard: tabbing to a card's heart should reveal its whole title
// too, on touch devices as much as on desktop.
card.addEventListener('focusin', () => setBarTitleActive(card, true));
card.addEventListener('focusout', () => setBarTitleActive(card, false));
list.appendChild(card);
});
// Widths only exist once the cards are laid out.
requestAnimationFrame(() => measureBarTitles(list));
}
})();