Remove up front calculations and improve performance of section rendering

This commit is contained in:
HighImKevin committed 2026-09-26 08:16:23 -04:00
1 parent c0c5c456f4
commit 992851fc76
1 file changed
+210 -75
+210 -75
View File
@@ -610,7 +610,11 @@
if (!leftButton || !rightButton) return;
const currentPosition = Math.abs(new DOMMatrixReadOnly(window.getComputedStyle(scroller).transform)?.m41 || 0);
const maxPosition = scroller.scrollWidth - scroller.clientWidth;
const maxPosition = getMaxPositionFromPadding(
scroller,
readScrollerPadding(scroller),
readSampleCardPadding(scroller)
);
leftButton.disabled = currentPosition === 0;
rightButton.disabled = currentPosition >= maxPosition;
}
@@ -2333,7 +2337,7 @@
const renderProgressiveSectionsEndTime = performance.now();
const renderProgressiveSectionsDuration = renderProgressiveSectionsEndTime - renderProgressiveSectionsStartTime;
console.log(`Render progressive sections initialization time: ${renderProgressiveSectionsDuration.toFixed(2)}ms`);
},
},
/**
* Unified function to render progressive sections (Cards or Spotlight)
@@ -4035,12 +4039,9 @@
}));
}
// Set backdrop width based on client width either 1920 or 1280, 640 is the minimum
const clientWidth = window.innerWidth;
let backdropWidth = 1920;
if (clientWidth < 1280) {
backdropWidth = 1280;
} else if (clientWidth < 640) {
// Set backdrop width based on layout-mobile presence
let backdropWidth = 0;
if (document.querySelector('.layout-mobile')) {
backdropWidth = 640;
}
@@ -5858,7 +5859,9 @@
function inferSkeletonItemType(sectionConfig) {
if (!sectionConfig) return null;
const query = sectionConfig.queries?.[0];
const queries = sectionConfig.queries || [];
const activeIndex = resolveActiveQueryIndex(sectionConfig, { initialize: false });
const query = queries[activeIndex] || queries[0];
const path = typeof query?.path === 'string' ? query.path : '';
if (path.includes('/Studios')) return 'Studio';
if (path.includes('/Genres')) return 'Genre';
@@ -5883,6 +5886,83 @@
return 2;
}
function readScrollerPadding(scroller) {
if (!scroller) return { left: 0, right: 0 };
// Return --scroller-padding if it exists, otherwise set it and return it
if (scroller.style.getPropertyValue('--scroller-padding-left') && scroller.style.getPropertyValue('--scroller-padding-right')) {
return {
left: parseFloat(scroller.style.getPropertyValue('--scroller-padding-left')),
right: parseFloat(scroller.style.getPropertyValue('--scroller-padding-right'))
};
}
const style = window.getComputedStyle(scroller);
const paddingLeft = parseFloat(style.paddingLeft) || 0;
const paddingRight = parseFloat(style.paddingRight) || 0;
scroller.style.setProperty('--scroller-padding-left', paddingLeft);
scroller.style.setProperty('--scroller-padding-right', paddingRight);
return {
left: paddingLeft,
right: paddingRight
};
}
function readCardPadding(card) {
if (!card) return { left: 0, right: 0 };
// Return --card-padding if it exists, otherwise set it and return it
if (card.style.getPropertyValue('--card-padding-left') && card.style.getPropertyValue('--card-padding-right')) {
return {
left: parseFloat(card.style.getPropertyValue('--card-padding-left')),
right: parseFloat(card.style.getPropertyValue('--card-padding-right'))
};
}
const style = window.getComputedStyle(card);
const paddingLeft = parseFloat(style.paddingLeft) || 0;
const paddingRight = parseFloat(style.paddingRight) || 0;
card.style.setProperty('--card-padding-left', paddingLeft);
card.style.setProperty('--card-padding-right', paddingRight);
return {
left: paddingLeft,
right: paddingRight
};
}
function readSampleCardPadding(scroller) {
const sampleCard = scroller?.querySelector?.(
'.itemsContainer > .card:not(.card-layout-dummy):not(.skeleton-card)'
);
return readCardPadding(sampleCard);
}
/**
* Max transform/scroll offset so the trailing content edge matches the leading
* gutter when the skin only pads the left (e.g. Fin --sidePadding), plus the
* sample card's horizontal padding so the last card's right padding clears the clip.
*/
function getMaxPositionFromPadding(scroller, pad, cardPad) {
if (!scroller) return 0;
// Return --max-scroll if it exists, otherwise set it and return it
if (scroller.style.getPropertyValue('--max-scroll')) {
return parseFloat(scroller.style.getPropertyValue('--max-scroll'));
}
const { left, right } = pad || { left: 0, right: 0 };
const cp = cardPad || { left: 0, right: 0 };
const endInset = Math.max(right, left);
const base = Math.max(0, (scroller.scrollWidth - scroller.clientWidth) + endInset);
// Last-card right scroll: include card L+R padding in the end stop
const maxScroll = base + (cp.left || 0) + (cp.right || 0);
scroller.style.setProperty('--max-scroll', String(maxScroll));
return maxScroll;
}
function createScrollerElement() {
const scroller = document.createElement('div');
const isMobile = isMobileLayout();
@@ -5941,14 +6021,38 @@
return Math.abs(translateX);
}
// Gesture-local padding: captured once per drag/wheel burst, cleared on end/idle
let gesturePad = null;
let gestureCardPad = null;
let wheelIdleTimer = null;
function ensureGestureInsets() {
if (!gesturePad) gesturePad = readScrollerPadding(scroller);
if (!gestureCardPad) gestureCardPad = readSampleCardPadding(scroller);
}
function clearGestureInsets() {
gesturePad = null;
gestureCardPad = null;
}
function getMaxPosition() {
return scroller.scrollWidth - scroller.clientWidth;
ensureGestureInsets();
return getMaxPositionFromPadding(scroller, gesturePad, gestureCardPad);
}
function getMaxPositionFresh() {
return getMaxPositionFromPadding(
scroller,
readScrollerPadding(scroller),
readSampleCardPadding(scroller)
);
}
function setPosition(newPosition, options) {
const opts = options || {};
const maxPosition = getMaxPosition() + 300;
const clampedPosition = Math.min(Math.max(newPosition, 0), Math.max(maxPosition, 0));
const maxPosition = Math.max(0, opts.freshMax ? getMaxPositionFresh() : getMaxPosition());
const clampedPosition = Math.min(Math.max(newPosition, 0), maxPosition);
if (opts.animate) {
scroller.style.transition = 'transform 270ms ease-out';
@@ -5979,42 +6083,42 @@
return Array.from(itemsContainer.querySelectorAll(':scope > .card:not(.card-layout-dummy):not(.skeleton-card)'));
}
const scrollerPadding = (() => {
const style = window.getComputedStyle(scroller);
return {
left: parseFloat(style.paddingLeft) || 0,
right: parseFloat(style.paddingRight) || 0
};
})();
function getCardScrollLeft(card) {
// Scroller + cards share the same translateX, so this delta is layout offset
// within the border box. Subtract paddingLeft so snap aligns to the padded
// content gutter (same inset as card 0 at scroll 0), not the page edge.
// Align card content edge (border-box left + card paddingLeft) to the
// scroller's padded content gutter.
const pad = readScrollerPadding(scroller);
const cardPad = readSampleCardPadding(scroller);
const scrollerRect = scroller.getBoundingClientRect();
const cardRect = card.getBoundingClientRect();
return cardRect.left - scrollerRect.left - scrollerPadding.left;
return cardRect.left - scrollerRect.left - pad.left + cardPad.left;
}
/**
* Cards fully inside the padded content clip (not the browser viewport / border box).
* Undo translateX, then inset by scroller padding so visibility matches snap targets.
* Undo translateX, then inset by scroller + card padding so visibility matches snap targets.
* When the skin only pads the left, treat trailing inset as max(left, right).
*/
function getFullyVisibleCardIndices(cards) {
const fullyVisible = [];
const epsilon = 1;
const pad = readScrollerPadding(scroller);
const cardPad = readSampleCardPadding(scroller);
const endInset = Math.max(pad.right, pad.left);
const scrollerRect = scroller.getBoundingClientRect();
const currentPosition = getCurrentPosition();
const contentWidth = Math.max(
0,
scroller.clientWidth - scrollerPadding.left - scrollerPadding.right
scroller.clientWidth - pad.left - endInset
);
const visibleLeft = scrollerRect.left + currentPosition + scrollerPadding.left;
const visibleLeft = scrollerRect.left + currentPosition + pad.left;
const visibleRight = visibleLeft + contentWidth;
cards.forEach((card, index) => {
const cardRect = card.getBoundingClientRect();
if (cardRect.width <= 0) return;
if (cardRect.left >= visibleLeft - epsilon && cardRect.right <= visibleRight + epsilon) {
// Use content box (inside card padding) for visibility
const cardContentLeft = cardRect.left + cardPad.left;
const cardContentRight = cardRect.right - cardPad.right;
if (cardContentLeft >= visibleLeft - epsilon && cardContentRight <= visibleRight + epsilon) {
fullyVisible.push(index);
}
});
@@ -6024,8 +6128,13 @@
function snapScrollToCardIndex(cards, targetIndex) {
if (!cards.length) return;
const clampedIndex = Math.max(0, Math.min(targetIndex, cards.length - 1));
const targetLeft = clampedIndex === 0 ? 0 : getCardScrollLeft(cards[clampedIndex]);
setPosition(targetLeft, { animate: true });
// Last card: use max (scroller end inset + card L+R padding), not left-align snap
if (clampedIndex === cards.length - 1) {
setPosition(getMaxPositionFresh(), { animate: true, freshMax: true });
return;
}
const targetLeft = getCardScrollLeft(cards[clampedIndex]);
setPosition(targetLeft, { animate: true, freshMax: true });
}
leftButton.addEventListener('click', () => {
@@ -6056,8 +6165,9 @@
const fullyVisible = getFullyVisibleCardIndices(cards);
const lastVisible = fullyVisible.length ? fullyVisible[fullyVisible.length - 1] : -1;
const nextIndex = lastVisible + 1;
if (nextIndex >= cards.length) {
setPosition(getMaxPosition(), { animate: true });
const lastWillBeVisible = nextIndex + fullyVisible.length >= cards.length;
if (nextIndex >= cards.length || lastWillBeVisible) {
setPosition(getMaxPositionFresh(), { animate: true, freshMax: true });
return;
}
snapScrollToCardIndex(cards, nextIndex);
@@ -6104,6 +6214,7 @@
const dy = p.clientY - dragState.startY;
if (Math.abs(dx) < DRAG_THRESHOLD_PX && Math.abs(dy) < DRAG_THRESHOLD_PX) return;
ensureGestureInsets();
dragState.isDragging = true;
dragState.startPosition = getCurrentPosition();
dragState.currentPosition = dragState.startPosition;
@@ -6117,6 +6228,7 @@
if (e.touches) {
document.addEventListener('touchmove', onDragMove, { passive: false });
document.addEventListener('touchend', onDragEnd, { passive: true });
document.addEventListener('touchcancel', onDragEnd, { passive: true });
} else {
document.addEventListener('mousemove', onDragMove);
document.addEventListener('mouseup', onDragEnd);
@@ -6182,9 +6294,10 @@
function onDragEnd(e) {
if (!dragState.isDragging) return;
if (e.type === 'touchend') {
if (e.type === 'touchend' || e.type === 'touchcancel') {
document.removeEventListener('touchmove', onDragMove);
document.removeEventListener('touchend', onDragEnd);
document.removeEventListener('touchcancel', onDragEnd);
} else {
document.removeEventListener('mousemove', onDragMove);
document.removeEventListener('mouseup', onDragEnd);
@@ -6222,6 +6335,7 @@
if (currentPosition === 0 || currentPosition === maxPosition) {
dragState.velocity = 0;
dragState.momentumId = null;
clearGestureInsets();
return;
}
@@ -6230,6 +6344,7 @@
} else {
dragState.velocity = 0;
dragState.momentumId = null;
clearGestureInsets();
}
};
@@ -6237,10 +6352,46 @@
} else {
dragState.velocity = 0;
dragState.momentumId = null;
clearGestureInsets();
}
}
scroller.addEventListener('mousedown', onPointerDown);
// Trackpad / mouse wheel → custom transform scroll (native overflow scrollers handle this themselves)
const WHEEL_DELTA_MIN = 1;
const WHEEL_IDLE_MS = 150;
scroller.addEventListener('wheel', (e) => {
if (scroller.classList.contains('scrollX')) return;
let dx = e.deltaX;
let dy = e.deltaY;
if (e.deltaMode === 1) {
dx *= 16;
dy *= 16;
} else if (e.deltaMode === 2) {
dx *= scroller.clientWidth;
dy *= scroller.clientHeight;
}
const shiftHorizontal = e.shiftKey && Math.abs(dy) >= WHEEL_DELTA_MIN && Math.abs(dx) < WHEEL_DELTA_MIN;
if (shiftHorizontal) {
dx = dy;
dy = 0;
}
if (Math.abs(dx) < WHEEL_DELTA_MIN) return;
if (Math.abs(dx) < Math.abs(dy)) return;
e.preventDefault();
ensureGestureInsets();
if (wheelIdleTimer) clearTimeout(wheelIdleTimer);
wheelIdleTimer = setTimeout(() => {
wheelIdleTimer = null;
clearGestureInsets();
}, WHEEL_IDLE_MS);
setPosition(getCurrentPosition() + dx, { animate: false });
}, { passive: false });
}
return scrollButtons;
@@ -6420,7 +6571,6 @@
verticalSection.appendChild(scrollButtons);
verticalSection.appendChild(scroller);
registerScrollSectionScrollButtons(verticalSection);
return verticalSection;
}
@@ -6661,7 +6811,6 @@
verticalSection.appendChild(scrollButtons);
verticalSection.appendChild(scroller);
registerScrollSectionScrollButtons(verticalSection);
return verticalSection;
}
@@ -6999,7 +7148,19 @@
const rightButton = scrollButtons && scrollButtons.querySelector('button[data-direction="right"]');
if (!scroller || !scrollButtons || !leftButton || !rightButton) return;
const scrollX = parseFloat(scroller.style.getPropertyValue('--scroll-x')) || 0;
const maxScroll = parseFloat(scroller.style.getPropertyValue('--max-scroll')) || 0;
let maxScroll = 0;
// Check if max-scroll is already set, if not then set it
if (scroller.style.getPropertyValue('--max-scroll')) {
maxScroll = parseFloat(scroller.style.getPropertyValue('--max-scroll')) || 0;
} else {
maxScroll = getMaxPositionFromPadding(
scroller,
readScrollerPadding(scroller),
readSampleCardPadding(scroller)
);
}
leftButton.disabled = scrollX <= 0;
rightButton.disabled = scrollX >= maxScroll - 1;
}
@@ -7010,45 +7171,15 @@
function updateScrollButtonStateForSection(verticalSection) {
const scroller = verticalSection.querySelector('.emby-scroller');
if (!scroller) return;
const maxScroll = Math.max(0, (scroller.scrollWidth - scroller.clientWidth) || 0);
const maxScroll = getMaxPositionFromPadding(
scroller,
readScrollerPadding(scroller),
readSampleCardPadding(scroller)
);
scroller.style.setProperty('--max-scroll', String(maxScroll));
applyScrollButtonState(verticalSection);
}
/**
* Ensure scroll section visibility observer exists. When a section becomes visible, runs updateScrollButtonStateForSection once then unobserves.
*/
function getScrollSectionVisibilityObserver() {
if (scrollSectionVisibilityObserver) return scrollSectionVisibilityObserver;
scrollSectionVisibilityObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (!entry.isIntersecting) return;
const verticalSection = entry.target;
scrollSectionVisibilityObserver.unobserve(verticalSection);
updateScrollButtonStateForSection(verticalSection);
});
}, { root: null, threshold: 0.01 });
return scrollSectionVisibilityObserver;
}
/**
* Register a scrollable section: when it becomes visible, set --max-scroll once and apply initial button state.
*/
function registerScrollSectionScrollButtons(verticalSection) {
const itemsContainer = verticalSection.querySelector('.itemsContainer');
const scroller = verticalSection.querySelector('.emby-scroller');
const first = itemsContainer && itemsContainer.firstElementChild;
const last = itemsContainer && itemsContainer.lastElementChild;
if (!first || !last) {
if (scroller) {
scroller.style.setProperty('--max-scroll', '0');
applyScrollButtonState(verticalSection);
}
return;
}
getScrollSectionVisibilityObserver().observe(verticalSection);
}
/**
* Initialize the smart lazy loading system
* Scans existing elements and starts both observers
@@ -7354,8 +7485,12 @@
function setScrollerPosition(scroller, position, animate) {
if (!scroller) return;
const maxPosition = Math.max(0, (scroller.scrollWidth - scroller.clientWidth) || 0);
const clamped = Math.min(Math.max(position, 0), maxPosition + 300);
const maxPosition = getMaxPositionFromPadding(
scroller,
readScrollerPadding(scroller),
readSampleCardPadding(scroller)
);
const clamped = Math.min(Math.max(position, 0), maxPosition);
if (scroller.classList.contains('scrollX')) {
scroller.scrollTo({ left: clamped, behavior: animate ? 'smooth' : 'auto' });
return;
@@ -7537,7 +7672,7 @@
invalidateLastRowPadding(itemsContainer);
patchCardsUserData(sectionElement, items);
ensureCardBorders(sectionElement);
updateScrollButtonStateForSection(sectionElement);
//updateScrollButtonStateForSection(sectionElement);
if (typeof onComplete === 'function') {
onComplete();
}
@@ -7585,7 +7720,7 @@
applyItemsLayoutState(sectionElement, layout, gapless);
ensureCardBorders(sectionElement);
attachSectionControlButtons(sectionConfig, sectionElement, items);
requestAnimationFrame(() => updateScrollButtonStateForSection(sectionElement));
//requestAnimationFrame(() => updateScrollButtonStateForSection(sectionElement));
return sectionElement;
}