diff --git a/scripts/cardBuilder.js b/scripts/cardBuilder.js index 48617c5..82863a3 100644 --- a/scripts/cardBuilder.js +++ b/scripts/cardBuilder.js @@ -784,21 +784,23 @@ function flattenSeriesEpisodes(items) { if (!items || items.length === 0) return items; - // Keep only the first item in the array for any given series - const seriesMap = new Map(); - items.forEach((item) => { - if (item.Type !== 'Episode') { - return; + const seenSeries = new Set(); + const result = []; + + for (const item of items) { + if (item.Type === 'Episode') { + // Only keep the first episode seen for a given SeriesId + if (item.SeriesId && !seenSeries.has(item.SeriesId)) { + seenSeries.add(item.SeriesId); + result.push(item); + } + } else { + // Keep non-Episode items as-is, in order + result.push(item); } + } - if (seriesMap.has(item.SeriesId)) { - return; - } - - seriesMap.set(item.SeriesId, item); - }); - - return Array.from(seriesMap.values()); + return result; } function formatAirDate(premiereDate) { @@ -1665,26 +1667,35 @@ } let activePopover = null; + let activePopoverCloseHandler = null; const closePopover = () => { + if (activePopoverCloseHandler) { + document.removeEventListener('click', activePopoverCloseHandler); + activePopoverCloseHandler = null; + } if (activePopover) { activePopover.remove(); activePopover = null; } }; - if (!document.getElementById('kefinTweaks-multiQueryPopover-style')) { - const styleElement = document.createElement('style'); - styleElement.id = 'kefinTweaks-multiQueryPopover-style'; - styleElement.textContent = ` + const multiQueryPopoverCss = ` .multiQueryPopover { background-color: rgba(0, 0, 0, 0.95); position: absolute !important; z-index: 1001; display: block; + overflow-y: auto; + box-sizing: border-box; } `; - document.head.appendChild(styleElement); + let multiQueryStyleEl = document.getElementById('kefinTweaks-multiQueryPopover-style'); + if (!multiQueryStyleEl) { + multiQueryStyleEl = document.createElement('style'); + multiQueryStyleEl.id = 'kefinTweaks-multiQueryPopover-style'; + document.head.appendChild(multiQueryStyleEl); } + multiQueryStyleEl.textContent = multiQueryPopoverCss; selectButton.addEventListener('click', (e) => { e.stopPropagation(); @@ -1763,15 +1774,37 @@ popover.appendChild(itemElement); }); - titleContainer.style.position = 'relative'; - selectButton.parentNode.insertBefore(popover, selectButton.nextSibling); + // Mount on body with absolute + document coords so the menu scrolls with the button + document.body.appendChild(popover); + const margin = 8; + const edgeMargin = 16; + const gap = 8; + const scrollX = window.scrollX || window.pageXOffset || 0; + const scrollY = window.scrollY || window.pageYOffset || 0; const buttonRect = selectButton.getBoundingClientRect(); - const containerRect = titleContainer.getBoundingClientRect(); - popover.style.left = `${Math.max(0, buttonRect.left - containerRect.left)}px`; - popover.style.top = '100%'; + const popoverWidth = popover.offsetWidth || popover.getBoundingClientRect().width || 200; + const spaceBelow = window.innerHeight - buttonRect.bottom - margin; + const spaceAbove = buttonRect.top - margin; + const placeBelow = spaceBelow >= Math.min(spaceAbove, 160) || spaceBelow >= spaceAbove; + const available = placeBelow ? spaceBelow : spaceAbove; + const maxHeight = Math.max(120, Math.min(window.innerHeight * 0.6, available - gap)); + + let leftViewport = buttonRect.left; + leftViewport = Math.min( + Math.max(edgeMargin, leftViewport), + Math.max(edgeMargin, window.innerWidth - popoverWidth - edgeMargin), + ); + + popover.style.position = 'absolute'; + popover.style.left = `${leftViewport + scrollX}px`; + popover.style.maxHeight = `${maxHeight}px`; popover.style.bottom = 'auto'; - popover.style.marginTop = '8px'; - popover.style.marginBottom = '0'; + if (placeBelow) { + popover.style.top = `${buttonRect.bottom + gap + scrollY}px`; + } else { + const height = Math.min(popover.offsetHeight || maxHeight, maxHeight); + popover.style.top = `${Math.max(scrollY + margin, buttonRect.top - gap - height + scrollY)}px`; + } activePopover = popover; if (selectedItemElement) { @@ -1784,14 +1817,13 @@ }, 10); } - const closeHandler = (ev) => { + activePopoverCloseHandler = (ev) => { if (!popover.contains(ev.target) && !selectButton.contains(ev.target)) { closePopover(); - document.removeEventListener('click', closeHandler); } }; setTimeout(() => { - document.addEventListener('click', closeHandler); + document.addEventListener('click', activePopoverCloseHandler); }, 100); }); } @@ -3669,12 +3701,8 @@ imageUrl = `${serverAddress}/Items/${itemId}/Images/Logo?${imageParams}&quality=96&tag=${logoTag}`; } else if (cardFormat === 'clear art' && item.ImageTags?.Art) { imageUrl = `${serverAddress}/Items/${item.Id}/Images/Art?${imageParams}&quality=96&tag=${item.ImageTags.Art}`; - } else if (cardFormat === 'banner') { - if (item.ImageTags?.Banner) { - imageUrl = `${serverAddress}/Items/${item.Id}/Images/Banner?${imageParams}&quality=96&tag=${item.ImageTags.Banner}`; - } else { - imageUrl = `${serverAddress}/Items/${item.SeriesId || item.ParentId}/Images/Banner?${imageParams}&quality=96`; - } + } else if (cardFormat === 'banner' && item.ImageTags?.Banner) { + imageUrl = `${serverAddress}/Items/${item.Id}/Images/Banner?${imageParams}&quality=96&tag=${item.ImageTags.Banner}`; } else if (cardFormat === 'disc' && item.ImageTags?.Disc) { imageUrl = `${serverAddress}/Items/${item.Id}/Images/Disc?${imageParams}&quality=96&tag=${item.ImageTags.Disc}`; } else if (item.ImageTags?.Primary) { diff --git a/scripts/homeScreenSectionConfigure.js b/scripts/homeScreenSectionConfigure.js index 2333046..f135ac3 100644 --- a/scripts/homeScreenSectionConfigure.js +++ b/scripts/homeScreenSectionConfigure.js @@ -1008,7 +1008,7 @@ if (activePopover.popover.parentElement !== mountElement) { mountElement.appendChild(activePopover.popover); } - requestAnimationFrame(() => repositionActivePopover()); + requestAnimationFrame(() => repositionActivePopover({ force: true })); } } @@ -1868,6 +1868,7 @@ flipItems.forEach(({ el }) => clearSectionFlipStyles(el)); clearOverflowAnchorNone(anchorEls || []); if (activePopover) activePopover.isAnimating = false; + repositionActivePopover({ force: true }); resolve(); }; @@ -1880,6 +1881,7 @@ el.style.transform = `translateY(${invertY * (1 - eased)}px)`; }); setScrollY(scrollRoot, startScroll + scrollDelta * eased); + repositionActivePopover({ force: true }); if (t < 1) requestAnimationFrame(frame); else finish(); @@ -1921,6 +1923,7 @@ setScrollY(scrollRoot, getScrollY(scrollRoot) - delta); void currentEl.offsetHeight; clearOverflowAnchorNone(anchorEls); + repositionActivePopover({ force: true }); return; } @@ -1981,6 +1984,7 @@ setScrollY(scrollRoot, getScrollY(scrollRoot) - delta); void currentEl.offsetHeight; clearOverflowAnchorNone(anchorEls); + repositionActivePopover({ force: true }); return; } @@ -2103,21 +2107,22 @@ sectionElement?.classList.remove('kefin-section-configure-popover-open'); } - function repositionActivePopover() { - if (!activePopover || activePopover.isAnimating) return; - // Once placed, never move — scrolling/resize/reorder must not shift targets mid-click. - if (activePopover.placementLocked) return; + function repositionActivePopover({ force = false } = {}) { + if (!activePopover || (activePopover.isAnimating && !force)) return; + // Once placed, never move on scroll/resize — reorder/remount can force a re-anchor. + if (activePopover.placementLocked && !force) return; const anchor = activePopover.anchorButton || getConfigureAnchorButton(activePopover.entry); const mount = activePopover.mountElement || resolvePopoverMount(anchor, activePopover.entry?.element); if (!anchor || !mount || !document.body.contains(anchor) || !document.body.contains(mount)) return; - activePopover.anchorButton = anchor; + activePopover.anchorButton = getConfigureAnchorButton(activePopover.entry) || anchor; activePopover.mountElement = mount; if (activePopover.entry?.element) { - activePopover.entry.element = anchor.closest('[data-section-id]') || activePopover.entry.element; + activePopover.entry.element = activePopover.anchorButton?.closest('[data-section-id]') + || activePopover.entry.element; activePopover.sectionElement = activePopover.entry.element; } - positionConfigurePopover(activePopover.popover, anchor, mount); + positionConfigurePopover(activePopover.popover, activePopover.anchorButton, mount); activePopover.placementLocked = true; if (activePopover.formatMenu && activePopover.formatMenuAnchor) { positionFormatMenu(activePopover.formatMenu, activePopover.formatMenuAnchor); @@ -2273,18 +2278,19 @@ const buttonRect = anchorButton.getBoundingClientRect(); const popoverWidth = popover.offsetWidth || popover.getBoundingClientRect().width || 280; const margin = 8; + const edgeMargin = 16; const gap = 8; let leftViewport = buttonRect.left + (buttonRect.width / 2) - (popoverWidth / 2); - const minLeft = Math.max(sectionRect.left + margin, margin); - const maxLeft = Math.min(sectionRect.right - popoverWidth - margin, window.innerWidth - popoverWidth - margin); + const minLeft = Math.max(sectionRect.left + margin, edgeMargin); + const maxLeft = Math.min(sectionRect.right - popoverWidth - margin, window.innerWidth - popoverWidth - edgeMargin); if (maxLeft >= minLeft) { leftViewport = Math.min(Math.max(leftViewport, minLeft), maxLeft); } else { leftViewport = Math.min( - Math.max(sectionRect.left + margin, margin), - Math.max(margin, window.innerWidth - popoverWidth - margin) + Math.max(sectionRect.left + margin, edgeMargin), + Math.max(edgeMargin, window.innerWidth - popoverWidth - edgeMargin) ); } @@ -2300,16 +2306,18 @@ if (activePopover) activePopover.anchorMode = placeBelow ? 'below' : 'above'; } + const scrollX = window.scrollX || window.pageXOffset || 0; + const scrollY = window.scrollY || window.pageYOffset || 0; + popover.classList.toggle('is-below', placeBelow); - popover.style.position = 'fixed'; - popover.style.left = `${leftViewport}px`; + popover.style.position = 'absolute'; + popover.style.left = `${leftViewport + scrollX}px`; + popover.style.bottom = 'auto'; if (placeBelow) { - popover.style.top = `${buttonRect.bottom + gap}px`; - popover.style.bottom = 'auto'; + popover.style.top = `${buttonRect.bottom + gap + scrollY}px`; } else { const height = getPopoverPlacementHeight(popover, popover.classList.contains('is-expanded')); - popover.style.top = `${Math.max(margin, buttonRect.top - gap - height)}px`; - popover.style.bottom = 'auto'; + popover.style.top = `${Math.max(scrollY + margin, buttonRect.top - gap - height + scrollY)}px`; } } @@ -2869,7 +2877,7 @@ overflow: visible; } .kefin-section-configure-popover { - position: fixed; + position: absolute; z-index: 12000; box-sizing: border-box; display: flex;