From f7b756aad0173337e599761b6c72b0e1860e0f18 Mon Sep 17 00:00:00 2001 From: HighImKevin Date: Sat, 10 Oct 2026 10:15:31 -0400 Subject: [PATCH] Switch grid layout to use a proper grid instead of a flex box with dummy cards --- scripts/cardBuilder.css | 197 +++++++++++++--- scripts/cardBuilder.js | 317 ++++---------------------- scripts/homeScreenPin.js | 17 +- scripts/homeScreenSectionConfigure.js | 1 - scripts/seriesEpisodes.js | 2 +- 5 files changed, 209 insertions(+), 325 deletions(-) diff --git a/scripts/cardBuilder.css b/scripts/cardBuilder.css index a474698..8944ed6 100644 --- a/scripts/cardBuilder.css +++ b/scripts/cardBuilder.css @@ -1358,50 +1358,175 @@ max-width: unset !important; } -/* Grid (non-scrolling) item layouts */ +/* Grid (non-scrolling) item layouts — CSS Grid with JF card.scss column breakpoints. + Shape inferred from data-card-format (see resolveGridShape); default = portrait. + Columns use @container against the scroller width (not viewport), so inset / + max-width sections (e.g. 67vw series episodes) get fewer, correctly sized cards. */ +.emby-scroller-container > .emby-scroller { + container-type: inline-size; + container-name: kefin-items; +} + .itemsContainer[data-layout="grid"] { - display: flex !important; - flex-wrap: wrap !important; + display: grid !important; + flex-wrap: unset; white-space: normal !important; transform: none !important; transition: none !important; + grid-template-columns: repeat(3, 1fr); } -.itemsContainer[data-layout="grid"] > .card { - flex: auto; - box-sizing: border-box; +/* Cards fill tracks; override JF portraitCard/backdropCard % widths and Banner 33vw */ +.itemsContainer[data-layout="grid"] > .card, +.itemsContainer[data-layout="grid"] > .homeLibraryButton, +.emby-scroller-container[data-card-format="Banner"] .itemsContainer[data-layout="grid"] > .card { + width: unset !important; + max-width: 100% !important; min-width: 0; -} - -/* Natural widths while counting items-per-row for last-row dummy padding */ -.itemsContainer[data-measuring="true"] > .card { - flex: 0 0 auto !important; -} - -/* Grid dummies keep aspect via padder but allow .cardBorder to paint */ -.itemsContainer[data-layout="grid"] > .card.card-layout-dummy { - visibility: hidden; - position: relative; - pointer-events: none; -} - -[data-border-style="wood"] .itemsContainer[data-layout="grid"] > .card.card-layout-dummy, -[data-border="wood"] .itemsContainer[data-layout="grid"] > .card.card-layout-dummy { - visibility: visible; -} - -.itemsContainer[data-layout="grid"] > .card.card-layout-dummy .cardPadder { - visibility: hidden; -} - -.itemsContainer[data-layout="grid"] > .card.card-layout-dummy .cardBorder { - position: absolute; - inset: 0; - z-index: 2; - pointer-events: none; box-sizing: border-box; - --card-border-cream: var(--kefin-card-border-color, #e8dab2); - --card-border-fancy: var(--kefin-card-border-color, rgb(231 255 123 / 35%)); + align-self: start; +} + +.itemsContainer[data-layout="grid"] > .card > .cardBox, +.itemsContainer[data-layout="grid"] > .card .cardScalable { + width: 100% !important; + max-width: none !important; + height: auto; +} + +/* Portrait / Square column table (JF .portraitCard / .squareCard) — default on grid */ +@container kefin-items (min-width: 31.25em) { + .itemsContainer[data-layout="grid"] { + grid-template-columns: repeat(3, 1fr); + } +} +@container kefin-items (min-width: 43.75em) { + .itemsContainer[data-layout="grid"] { + grid-template-columns: repeat(4, 1fr); + } +} +@container kefin-items (min-width: 50em) { + .itemsContainer[data-layout="grid"] { + grid-template-columns: repeat(5, 1fr); + } +} +@container kefin-items (min-width: 75em) { + .itemsContainer[data-layout="grid"] { + grid-template-columns: repeat(6, 1fr); + } +} +@container kefin-items (min-width: 87.5em) { + .itemsContainer[data-layout="grid"] { + grid-template-columns: repeat(7, 1fr); + } +} +@container kefin-items (min-width: 100em) { + .itemsContainer[data-layout="grid"] { + grid-template-columns: repeat(8, 1fr); + } +} +@container kefin-items (min-width: 120em) { + .itemsContainer[data-layout="grid"] { + grid-template-columns: repeat(9, 1fr); + } +} +@container kefin-items (min-width: 131.25em) { + .itemsContainer[data-layout="grid"] { + grid-template-columns: repeat(10, 1fr); + } +} + +/* Backdrop shape: Backdrop, Thumb, Series Thumb, Logo, Clear Art, Button */ +.emby-scroller-container[data-card-format="Backdrop"] .itemsContainer[data-layout="grid"], +.emby-scroller-container[data-card-format="Thumb"] .itemsContainer[data-layout="grid"], +.emby-scroller-container[data-card-format="Series Thumb"] .itemsContainer[data-layout="grid"], +.emby-scroller-container[data-card-format="Logo"] .itemsContainer[data-layout="grid"], +.emby-scroller-container[data-card-format="Clear Art"] .itemsContainer[data-layout="grid"], +.emby-scroller-container[data-card-format="Button"] .itemsContainer[data-layout="grid"] { + grid-template-columns: repeat(1, 1fr); +} +@container kefin-items (min-width: 25em) { + .emby-scroller-container[data-card-format="Backdrop"] .itemsContainer[data-layout="grid"], + .emby-scroller-container[data-card-format="Thumb"] .itemsContainer[data-layout="grid"], + .emby-scroller-container[data-card-format="Series Thumb"] .itemsContainer[data-layout="grid"], + .emby-scroller-container[data-card-format="Logo"] .itemsContainer[data-layout="grid"], + .emby-scroller-container[data-card-format="Clear Art"] .itemsContainer[data-layout="grid"], + .emby-scroller-container[data-card-format="Button"] .itemsContainer[data-layout="grid"] { + grid-template-columns: repeat(2, 1fr); + } +} +@container kefin-items (min-width: 48.125em) { + .emby-scroller-container[data-card-format="Backdrop"] .itemsContainer[data-layout="grid"], + .emby-scroller-container[data-card-format="Thumb"] .itemsContainer[data-layout="grid"], + .emby-scroller-container[data-card-format="Series Thumb"] .itemsContainer[data-layout="grid"], + .emby-scroller-container[data-card-format="Logo"] .itemsContainer[data-layout="grid"], + .emby-scroller-container[data-card-format="Clear Art"] .itemsContainer[data-layout="grid"], + .emby-scroller-container[data-card-format="Button"] .itemsContainer[data-layout="grid"] { + grid-template-columns: repeat(3, 1fr); + } +} +@container kefin-items (min-width: 75em) { + .emby-scroller-container[data-card-format="Backdrop"] .itemsContainer[data-layout="grid"], + .emby-scroller-container[data-card-format="Thumb"] .itemsContainer[data-layout="grid"], + .emby-scroller-container[data-card-format="Series Thumb"] .itemsContainer[data-layout="grid"], + .emby-scroller-container[data-card-format="Logo"] .itemsContainer[data-layout="grid"], + .emby-scroller-container[data-card-format="Clear Art"] .itemsContainer[data-layout="grid"], + .emby-scroller-container[data-card-format="Button"] .itemsContainer[data-layout="grid"] { + grid-template-columns: repeat(4, 1fr); + } +} +@container kefin-items (min-width: 100em) { + .emby-scroller-container[data-card-format="Backdrop"] .itemsContainer[data-layout="grid"], + .emby-scroller-container[data-card-format="Thumb"] .itemsContainer[data-layout="grid"], + .emby-scroller-container[data-card-format="Series Thumb"] .itemsContainer[data-layout="grid"], + .emby-scroller-container[data-card-format="Logo"] .itemsContainer[data-layout="grid"], + .emby-scroller-container[data-card-format="Clear Art"] .itemsContainer[data-layout="grid"], + .emby-scroller-container[data-card-format="Button"] .itemsContainer[data-layout="grid"] { + grid-template-columns: repeat(5, 1fr); + } +} +@container kefin-items (min-width: 156.25em) { + .emby-scroller-container[data-card-format="Backdrop"] .itemsContainer[data-layout="grid"], + .emby-scroller-container[data-card-format="Thumb"] .itemsContainer[data-layout="grid"], + .emby-scroller-container[data-card-format="Series Thumb"] .itemsContainer[data-layout="grid"], + .emby-scroller-container[data-card-format="Logo"] .itemsContainer[data-layout="grid"], + .emby-scroller-container[data-card-format="Clear Art"] .itemsContainer[data-layout="grid"], + .emby-scroller-container[data-card-format="Button"] .itemsContainer[data-layout="grid"] { + grid-template-columns: repeat(6, 1fr); + } +} + +/* Banner shape */ +.emby-scroller-container[data-card-format="Banner"] .itemsContainer[data-layout="grid"] { + grid-template-columns: repeat(1, 1fr); +} +@container kefin-items (min-width: 50em) { + .emby-scroller-container[data-card-format="Banner"] .itemsContainer[data-layout="grid"] { + grid-template-columns: repeat(2, 1fr); + } +} +@container kefin-items (min-width: 75em) { + .emby-scroller-container[data-card-format="Banner"] .itemsContainer[data-layout="grid"] { + grid-template-columns: repeat(3, 1fr); + } +} +@container kefin-items (min-width: 131.25em) { + .emby-scroller-container[data-card-format="Banner"] .itemsContainer[data-layout="grid"] { + grid-template-columns: repeat(4, 1fr); + } +} + +/* TV: portrait/square → 6, backdrop → 4 (fixed, not container-width) */ +html.layout-tv .itemsContainer[data-layout="grid"] { + grid-template-columns: repeat(6, 1fr); +} +html.layout-tv .emby-scroller-container[data-card-format="Backdrop"] .itemsContainer[data-layout="grid"], +html.layout-tv .emby-scroller-container[data-card-format="Thumb"] .itemsContainer[data-layout="grid"], +html.layout-tv .emby-scroller-container[data-card-format="Series Thumb"] .itemsContainer[data-layout="grid"], +html.layout-tv .emby-scroller-container[data-card-format="Logo"] .itemsContainer[data-layout="grid"], +html.layout-tv .emby-scroller-container[data-card-format="Clear Art"] .itemsContainer[data-layout="grid"], +html.layout-tv .emby-scroller-container[data-card-format="Button"] .itemsContainer[data-layout="grid"] { + grid-template-columns: repeat(4, 1fr); } .itemsContainer[data-gapless="true"], diff --git a/scripts/cardBuilder.js b/scripts/cardBuilder.js index 265ffff..4b6a560 100644 --- a/scripts/cardBuilder.js +++ b/scripts/cardBuilder.js @@ -435,7 +435,7 @@ } else { sectionEl.style.removeProperty('--wood-vert-img'); } - const cards = sectionEl.querySelectorAll('.card:not(.card-layout-dummy) .cardScalable'); + const cards = sectionEl.querySelectorAll('.card .cardScalable'); cards.forEach((scalable) => { let host = scalable.querySelector(':scope > .cardBorder'); if (!style) { @@ -479,9 +479,7 @@ function getCardRadius() { if (cachedCardRadius != null) return cachedCardRadius; - const candidates = document.querySelectorAll( - '.card:not(.card-layout-dummy) .cardScalable, .card:not(.card-layout-dummy) .cardContent, .card:not(.card-layout-dummy)', - ); + const candidates = document.querySelectorAll('.card .cardScalable, .card .cardContent, .card'); let sample = null; for (const el of candidates) { if (!sectionHasSquareCardBorder(el)) { @@ -784,6 +782,32 @@ return formats[Math.floor(Math.random() * formats.length)]; } + /** + * Map section cardFormat / data-card-format to JF grid column shape. + * Used for CSS grid column tables (no data-grid-shape attribute). + * @param {string|null|undefined} cardFormat + * @returns {'portrait'|'square'|'backdrop'|'banner'} + */ + function resolveGridShape(cardFormat) { + const f = String(cardFormat || '') + .trim() + .toLowerCase(); + if (f === 'banner') return 'banner'; + if (f === 'square' || f === 'sqaure' || f === 'disc') return 'square'; + if ( + f === 'backdrop' || + f === 'thumb' || + f === 'series thumb' || + f === 'logo' || + f === 'clear art' || + f === 'button' + ) { + return 'backdrop'; + } + // Poster, Series Poster, portrait, Random-resolved concrete formats, default + return 'portrait'; + } + function sectionHasRandomQuerySort(sectionConfig) { // Treat as random if any query has an undefined/null/non-existent SortBy without a path if (String(sectionConfig?.sortBy || '').toLowerCase() === 'random') { @@ -2022,7 +2046,6 @@ content.setAttribute(attr.name, attr.value); }); - invalidateLastRowPadding(content.querySelector('.itemsContainer')); invalidateScrollerMetrics(sectionElement.querySelector('.emby-scroller')); stripCopiedSkeletonSectionClasses(content); unobserveSkeletonShimmer(sectionElement); @@ -2202,9 +2225,9 @@ ensureCardBorders: ensureCardBorders, applyItemsLayoutState: applyItemsLayoutState, resolveItemsLayout: resolveItemsLayout, + resolveGridShape: resolveGridShape, resolveSpotlightPresentation: resolveSpotlightPresentation, resolveUseGaplessCards: resolveUseGaplessCards, - invalidateLastRowPadding: invalidateLastRowPadding, invalidateCardRadiusCache: invalidateCardRadiusCache, ensureCardRadiusCssVar: ensureCardRadiusCssVar, getScrollerPosition: getScrollerPosition, @@ -6457,241 +6480,12 @@ if (isButtonCardFormat(cardFormat)) { return Array.from(itemsContainer.querySelectorAll(':scope > .homeLibraryButton')); } - return Array.from(itemsContainer.querySelectorAll(':scope > .card:not(.card-layout-dummy)')); + return Array.from(itemsContainer.querySelectorAll(':scope > .card')); } function getRealLayoutCards(itemsContainer) { if (!itemsContainer) return []; - return Array.from(itemsContainer.querySelectorAll(':scope > .card:not(.card-layout-dummy)')); - } - - function removeLayoutDummies(itemsContainer) { - if (!itemsContainer) return; - itemsContainer.querySelectorAll(':scope > .card-layout-dummy').forEach((el) => el.remove()); - } - - function invalidateLastRowPadding(itemsContainer) { - if (!itemsContainer) return; - removeLayoutDummies(itemsContainer); - itemsContainer.removeAttribute('data-last-row-padding'); - itemsContainer.removeAttribute('data-last-row-padding-layout'); - itemsContainer.removeAttribute('data-last-row-padding-width'); - itemsContainer.removeAttribute('data-measuring'); - } - - function canMeasureLastRowPadding(itemsContainer) { - return !!(itemsContainer?.isConnected && itemsContainer.clientWidth >= 1); - } - - function getLastRowPaddingCache(itemsContainer, layout) { - if (!itemsContainer || layout !== 'grid') return null; - const cachedPad = itemsContainer.getAttribute('data-last-row-padding'); - const cachedLayout = itemsContainer.getAttribute('data-last-row-padding-layout'); - const cachedWidth = itemsContainer.getAttribute('data-last-row-padding-width'); - if (cachedPad == null || cachedPad === '' || cachedLayout !== layout || !/^\d+$/.test(cachedPad)) { - return null; - } - if (cachedWidth == null || !/^\d+$/.test(cachedWidth)) return null; - return { - padding: parseInt(cachedPad, 10), - width: parseInt(cachedWidth, 10), - }; - } - - function setLastRowPaddingCache(itemsContainer, layout, padding, width) { - itemsContainer.setAttribute('data-last-row-padding', String(padding)); - itemsContainer.setAttribute('data-last-row-padding-layout', layout); - itemsContainer.setAttribute('data-last-row-padding-width', String(width)); - } - - function countLayoutDummies(itemsContainer) { - if (!itemsContainer) return 0; - return itemsContainer.querySelectorAll(':scope > .card-layout-dummy').length; - } - - /** True when cache matches current container width and dummy count already equals padding. */ - function lastRowPaddingIsCurrent(itemsContainer, layout) { - const cache = getLastRowPaddingCache(itemsContainer, layout); - if (!cache || !canMeasureLastRowPadding(itemsContainer)) return false; - if (cache.width !== itemsContainer.clientWidth) return false; - return countLayoutDummies(itemsContainer) === cache.padding; - } - - function createLayoutDummyCard(templateCard, layout) { - const dummy = document.createElement('div'); - dummy.className = templateCard?.className || 'card'; - dummy.classList.add('card', 'card-layout-dummy'); - dummy.classList.remove('card-hoverable'); - dummy.setAttribute('aria-hidden', 'true'); - dummy.setAttribute('tabindex', '-1'); - - // cardBox - const cardBox = document.createElement('div'); - cardBox.className = 'cardBox'; - dummy.appendChild(cardBox); - - // cardScalable - const cardScalable = document.createElement('div'); - cardScalable.className = 'cardScalable'; - cardBox.appendChild(cardScalable); - - if (layout === 'grid') { - const templatePadder = templateCard?.querySelector('.cardPadder'); - const padder = document.createElement('div'); - padder.className = templatePadder?.className || 'cardPadder cardPadder-portrait lazy-hidden-children'; - const cardIcon = document.createElement('span'); - cardIcon.className = 'cardImageIcon material-icons'; - cardIcon.setAttribute('aria-hidden', 'true'); - cardIcon.textContent = 'folder'; - padder.appendChild(cardIcon); - cardScalable.appendChild(padder); - - const border = createCardBorderHost(); - const section = templateCard?.closest('[data-border],[data-border-style]'); - const raw = - section?.dataset?.border || - section?.dataset?.borderStyle || - templateCard?.querySelector('.cardBorder')?.dataset?.style || - ''; - const style = normalizeCardBorderStyle(raw); - if (style) fillCardBorder(border, style); - cardScalable.appendChild(border); - } - - return dummy; - } - - /** - * Count how many real cards fit on the first row at natural (non-growing) width. - * @param {HTMLElement} itemsContainer - * @returns {number} - */ - function countItemsPerRow(itemsContainer) { - if (!canMeasureLastRowPadding(itemsContainer)) return 0; - const cards = getRealLayoutCards(itemsContainer); - if (!cards.length) return 0; - itemsContainer.setAttribute('data-measuring', 'true'); - void itemsContainer.offsetWidth; - const firstTop = cards[0].offsetTop; - let count = 0; - for (const card of cards) { - if (card.offsetTop !== firstTop) break; - count++; - } - itemsContainer.removeAttribute('data-measuring'); - return count; - } - - /** - * Pad the last flex row with invisible dummy cards so grow distribution matches full rows. - * Reuses data-last-row-padding when cached for the same layout. - * @param {HTMLElement} itemsContainer - * @param {'grid'} layout - */ - function syncLastRowPadding(itemsContainer, layout) { - if (!itemsContainer || layout !== 'grid') return; - - // Already correct for this width — skip remove/recreate. - if (lastRowPaddingIsCurrent(itemsContainer, layout)) return; - - const cache = getLastRowPaddingCache(itemsContainer, layout); - const width = canMeasureLastRowPadding(itemsContainer) ? itemsContainer.clientWidth : 0; - const widthMatches = !!(cache && width >= 1 && cache.width === width); - - // Disconnected / zero-width: do not measure or cache (retry when laid out). - if (!widthMatches && !canMeasureLastRowPadding(itemsContainer)) return; - - let padding; - if (widthMatches) { - padding = cache.padding; - if (countLayoutDummies(itemsContainer) === padding) return; - removeLayoutDummies(itemsContainer); - } else { - // Dummies must be gone before measuring natural items-per-row. - removeLayoutDummies(itemsContainer); - const cards = getRealLayoutCards(itemsContainer); - if (!cards.length) { - invalidateLastRowPadding(itemsContainer); - return; - } - const perRow = countItemsPerRow(itemsContainer); - if (perRow <= 0) return; - const rem = cards.length % perRow; - padding = rem === 0 ? 0 : perRow - rem; - setLastRowPaddingCache(itemsContainer, layout, padding, itemsContainer.clientWidth); - } - - if (padding <= 0) return; - if (!itemsContainer.isConnected) return; - const template = getRealLayoutCards(itemsContainer)[0]; - if (!template) return; - const frag = document.createDocumentFragment(); - for (let i = 0; i < padding; i++) { - frag.appendChild(createLayoutDummyCard(template, layout)); - } - itemsContainer.appendChild(frag); - } - - let lastRowPaddingResizeTimer = null; - function handleLastRowPaddingWindowResize() { - clearTimeout(lastRowPaddingResizeTimer); - lastRowPaddingResizeTimer = setTimeout(() => { - document.querySelectorAll('.itemsContainer[data-layout="grid"]').forEach((container) => { - const layout = container.getAttribute('data-layout'); - if (lastRowPaddingIsCurrent(container, layout)) return; - invalidateLastRowPadding(container); - syncLastRowPadding(container, layout); - }); - }, 150); - } - if (!window.__kefinLastRowPaddingResizeBound) { - window.__kefinLastRowPaddingResizeBound = true; - window.addEventListener('resize', handleLastRowPaddingWindowResize); - } - - let lastRowPaddingRo = null; - let lastRowPaddingRoTimer = null; - const lastRowPaddingRoPending = new Set(); - - function ensureLastRowPaddingResizeObserver() { - if (lastRowPaddingRo || typeof ResizeObserver === 'undefined') return lastRowPaddingRo; - lastRowPaddingRo = new ResizeObserver((entries) => { - entries.forEach((entry) => { - if (entry?.target) lastRowPaddingRoPending.add(entry.target); - }); - clearTimeout(lastRowPaddingRoTimer); - lastRowPaddingRoTimer = setTimeout(() => { - const targets = [...lastRowPaddingRoPending]; - lastRowPaddingRoPending.clear(); - targets.forEach((el) => { - const layout = el.getAttribute?.('data-layout'); - if (layout !== 'grid') return; - if (lastRowPaddingIsCurrent(el, layout)) return; - invalidateLastRowPadding(el); - syncLastRowPadding(el, layout); - }); - }, 80); - }); - return lastRowPaddingRo; - } - - function observeLastRowPadding(itemsContainer) { - const ro = ensureLastRowPaddingResizeObserver(); - if (!ro || !itemsContainer) return; - try { - ro.observe(itemsContainer); - } catch (_) { - /* ignore */ - } - } - - function unobserveLastRowPadding(itemsContainer) { - if (!lastRowPaddingRo || !itemsContainer) return; - try { - lastRowPaddingRo.unobserve(itemsContainer); - } catch (_) { - /* ignore */ - } + return Array.from(itemsContainer.querySelectorAll(':scope > .card')); } /** @@ -6716,8 +6510,6 @@ itemsContainer.removeAttribute('data-gapless'); } if (resolved === 'row') { - unobserveLastRowPadding(itemsContainer); - removeLayoutDummies(itemsContainer); if (scrollButtons) scrollButtons.style.display = ''; } else if (scrollButtons) { scrollButtons.style.display = 'none'; @@ -6728,33 +6520,6 @@ showAllButton.setAttribute('aria-label', showAllButton.title); setShowAllIcon(showAllButton, nextLayout); } - if (resolved === 'grid') { - observeLastRowPadding(itemsContainer); - // Two frames so wrap layout is settled before measuring / padding - requestAnimationFrame(() => { - requestAnimationFrame(() => { - if (itemsContainer.getAttribute('data-layout') !== resolved) return; - syncLastRowPadding(itemsContainer, resolved); - }); - }); - } - } - - function syncAttachedGridTilesLayouts(root) { - if (!root?.querySelectorAll) return; - root.querySelectorAll('.itemsContainer[data-layout="grid"]').forEach((itemsContainer) => { - const layout = itemsContainer.getAttribute('data-layout'); - const gapless = itemsContainer.getAttribute('data-gapless') === 'true'; - const section = itemsContainer.closest('.verticalSection, .emby-scroller-container') || itemsContainer.parentElement; - if (section) applyItemsLayoutState(section, layout, gapless); - else { - observeLastRowPadding(itemsContainer); - if (!lastRowPaddingIsCurrent(itemsContainer, layout)) { - invalidateLastRowPadding(itemsContainer); - syncLastRowPadding(itemsContainer, layout); - } - } - }); } /** @deprecated Use applyItemsLayoutState */ @@ -7198,7 +6963,7 @@ const metrics = getScrollerMetrics(scroller); if (metrics.cardPad) return metrics.cardPad; - const sampleCard = scroller.querySelector?.('.itemsContainer > .card:not(.card-layout-dummy):not(.skeleton-card)'); + const sampleCard = scroller.querySelector?.('.itemsContainer > .card:not(.skeleton-card)'); const pad = readCardPadding(sampleCard); metrics.cardPad = pad; return pad; @@ -7286,7 +7051,7 @@ */ function toCenterCard(scroller, card, options = {}) { if (!scroller || !card || !scroller.contains(card)) return; - if (card.classList.contains('skeleton-card') || card.classList.contains('card-layout-dummy')) return; + if (card.classList.contains('skeleton-card')) return; const animate = options.animate !== false; const metrics = getScrollerMetrics(scroller); @@ -7316,7 +7081,7 @@ (e) => { const card = e.target?.closest?.('.card'); if (!card || !scroller.contains(card)) return; - if (card.classList.contains('skeleton-card') || card.classList.contains('card-layout-dummy')) return; + if (card.classList.contains('skeleton-card')) return; pendingCard = card; if (rafId) return; rafId = requestAnimationFrame(() => { @@ -7369,7 +7134,7 @@ return metrics.maxScroll; } - const cards = scroller.querySelectorAll('.itemsContainer > .card:not(.card-layout-dummy):not(.skeleton-card)'); + const cards = scroller.querySelectorAll('.itemsContainer > .card:not(.skeleton-card)'); const fullyVisible = getFullyVisibleCardCount(scroller, cards); if (fullyVisible === cards.length) { setMaxScrollPosition(scroller, 0); @@ -7503,7 +7268,7 @@ function getScrollCards() { const itemsContainer = scroller.querySelector('.itemsContainer'); if (!itemsContainer) return []; - return Array.from(itemsContainer.querySelectorAll(':scope > .card:not(.card-layout-dummy):not(.skeleton-card)')); + return Array.from(itemsContainer.querySelectorAll(':scope > .card:not(.skeleton-card)')); } function getCardScrollLeft(card) { @@ -9105,7 +8870,7 @@ } function getPaintedCardIds(sectionElement) { - const cards = sectionElement.querySelectorAll('.itemsContainer > .card[data-id]:not(.card-layout-dummy):not(.skeleton-card)'); + const cards = sectionElement.querySelectorAll('.itemsContainer > .card[data-id]:not(.skeleton-card)'); if (cards.length) { return Array.from(cards) .map((card) => card.getAttribute('data-id')) @@ -9282,7 +9047,7 @@ const animate = options.animate === true; const itemsContainer = scroller.querySelector('.itemsContainer'); if (!itemsContainer) return; - const cards = Array.from(itemsContainer.querySelectorAll(':scope > .card:not(.card-layout-dummy):not(.skeleton-card)')); + const cards = Array.from(itemsContainer.querySelectorAll(':scope > .card:not(.skeleton-card)')); if (!cards.length) return; const clampedIndex = Math.max(0, Math.min(index, cards.length - 1)); @@ -9415,7 +9180,7 @@ if (existing) { seenKept = true; nextNode = existing.nextElementSibling; - while (nextNode && (nextNode.classList.contains('card-layout-dummy') || nextNode.classList.contains('skeleton-card'))) { + while (nextNode && nextNode.classList.contains('skeleton-card')) { nextNode = nextNode.nextElementSibling; } return; @@ -9503,7 +9268,6 @@ if (slidee) slidee.style.transition = ''; } reindexSectionCards(itemsContainer); - invalidateLastRowPadding(itemsContainer); patchCardsUserData(sectionElement, items); ensureCardBorders(sectionElement); if (typeof onComplete === 'function') { @@ -9530,8 +9294,6 @@ : resolveItemsLayout(sectionConfig); const gapless = itemsContainer.getAttribute('data-gapless') === 'true' || resolveUseGaplessCards(sectionConfig); - invalidateLastRowPadding(itemsContainer); - const fragment = document.createDocumentFragment(); if (normalizedFormat === 'button' || isButtonCardFormat(cardFormat)) { items.forEach((item, index) => { @@ -9645,7 +9407,6 @@ const gapless = oldItemsContainer ? oldItemsContainer.getAttribute('data-gapless') === 'true' : resolveUseGaplessCards(sectionConfig); - invalidateLastRowPadding(content.querySelector('.itemsContainer')); attachSectionControlButtons(sectionConfig, content, items); invalidateScrollerMetrics(sectionElement.querySelector('.emby-scroller')); stripCopiedSkeletonSectionClasses(content); diff --git a/scripts/homeScreenPin.js b/scripts/homeScreenPin.js index 716ba1e..6306934 100644 --- a/scripts/homeScreenPin.js +++ b/scripts/homeScreenPin.js @@ -496,7 +496,6 @@ const card = window.cardBuilder?.buildCard?.(item, true, cardFormat); if (card) { itemsContainer.appendChild(card); - window.cardBuilder?.invalidateLastRowPadding?.(itemsContainer); addUnpinButton(card); addReorderButtons(card); updateAllReorderChevrons(itemsContainer); @@ -830,7 +829,8 @@ const pinButton = document.createElement('button'); pinButton.setAttribute('is', 'emby-button'); pinButton.type = 'button'; - pinButton.className = 'listItem listItem-button actionSheetMenuItem emby-button'; + const isMobile = window.innerWidth < 900; + pinButton.className = `listItem listItem-button actionSheetMenuItem emby-button${isMobile ? ' actionsheet-xlargeFont' : ''}`; pinButton.setAttribute('data-kefin-pin-action', 'true'); pinButton.innerHTML = ` @@ -886,7 +886,7 @@ function getRealPinnedCards(itemsContainer) { if (!itemsContainer) return []; - return [...itemsContainer.querySelectorAll(':scope > .card[data-id]:not(.card-layout-dummy)')]; + return [...itemsContainer.querySelectorAll(':scope > .card[data-id]')]; } function updateReorderChevronState(card) { @@ -937,7 +937,6 @@ const ok = await saveHomeScreen(hs, { refreshHome: false }); if (ok) { updateRuntimeAfterListPin(sectionId, null, orderedIds); - window.cardBuilder?.invalidateLastRowPadding?.(itemsContainer); } return ok; } @@ -973,7 +972,7 @@ const card = cardOrOverlay?.classList?.contains('card') ? cardOrOverlay : cardOrOverlay?.closest?.('.card'); - if (!card || card.classList.contains('card-layout-dummy')) return; + if (!card) return; if (card.querySelector(':scope > .pin-reorder-left')) return; const sectionEl = card.closest('[data-section-id]'); @@ -1016,7 +1015,7 @@ const card = cardOrOverlay?.classList?.contains('card') ? cardOrOverlay : cardOrOverlay?.closest?.('.card'); - if (!card || card.classList.contains('card-layout-dummy')) return; + if (!card) return; if (card.querySelector(':scope > .unpin-item-button')) return; const sectionEl = card.closest('[data-section-id]'); @@ -1049,7 +1048,7 @@ let hs = await loadHomeScreen(); const restoreHomeScreen = cloneHomeScreen(hs); const realCards = sectionEl - ? [...sectionEl.querySelectorAll('.itemsContainer > .card[data-id]:not(.card-layout-dummy)')] + ? [...sectionEl.querySelectorAll('.itemsContainer > .card[data-id]')] : []; const isLastInSection = realCards.length <= 1; let listName = null; @@ -1151,7 +1150,7 @@ padding: 0 !important; pointer-events: auto !important; } - [data-section-id^="pinned-list-"] .card[data-id]:not(.card-layout-dummy) { + [data-section-id^="pinned-list-"] .card[data-id] { position: relative !important; } .pin-reorder-left { @@ -1244,7 +1243,7 @@ }; document.querySelectorAll('.cardOverlayContainer').forEach(processOverlay); - document.querySelectorAll('[data-section-id^="pinned-list-"] .card[data-id]:not(.card-layout-dummy)').forEach(processCard); + document.querySelectorAll('[data-section-id^="pinned-list-"] .card[data-id]').forEach(processCard); const observer = new MutationObserver((mutations) => { mutations.forEach(mutation => { diff --git a/scripts/homeScreenSectionConfigure.js b/scripts/homeScreenSectionConfigure.js index 33d2eb5..bdb5a3d 100644 --- a/scripts/homeScreenSectionConfigure.js +++ b/scripts/homeScreenSectionConfigure.js @@ -996,7 +996,6 @@ } if (!isSpotlight && window.cardBuilder?.applyItemsLayoutState) { - window.cardBuilder.invalidateLastRowPadding?.(newContent.querySelector('.itemsContainer')); window.cardBuilder.applyItemsLayoutState(newContent, preservedLayout || 'row', !!preservedGapless); } diff --git a/scripts/seriesEpisodes.js b/scripts/seriesEpisodes.js index e4ccbea..70cdd00 100644 --- a/scripts/seriesEpisodes.js +++ b/scripts/seriesEpisodes.js @@ -575,7 +575,7 @@ function performEpisodeScroll(scrollerContainer, targetCard, targetEpisodeNumber) { const itemsContainer = scrollerContainer.querySelector('.itemsContainer') || scrollerContainer; const cards = Array.from(itemsContainer.querySelectorAll( - ':scope > .card:not(.card-layout-dummy):not(.skeleton-card)' + ':scope > .card:not(.skeleton-card)' )); const index = cards.indexOf(targetCard); if (index < 0) {