From 0cd35f5b16ac6703befebe791d6ffc276b3ca509 Mon Sep 17 00:00:00 2001 From: HighImKevin Date: Sun, 25 Oct 2026 17:08:21 -0400 Subject: [PATCH] Improve Infinite Scroll functionality- handle List View and other view options --- configuration/infiniteScroll-configuration.js | 18 +- configuration/userManager-configuration.js | 14 +- kefinTweaks-default-config.js | 3 +- scripts/cardBuilder.css | 100 +++++- scripts/cardBuilder.js | 294 +++++++++++++++++- scripts/infiniteScroll.js | 107 +++++-- scripts/utils.js | 34 +- 7 files changed, 528 insertions(+), 42 deletions(-) diff --git a/configuration/infiniteScroll-configuration.js b/configuration/infiniteScroll-configuration.js index 8bf9cb9..8a96077 100644 --- a/configuration/infiniteScroll-configuration.js +++ b/configuration/infiniteScroll-configuration.js @@ -11,7 +11,8 @@ function defaultOptions() { return { hidePageControls: false, - onlyShowTotalItemCount: false + onlyShowTotalItemCount: false, + showFollowingAlphaLetters: false }; } @@ -22,7 +23,8 @@ } return { hidePageControls: config.hidePageControls === true, - onlyShowTotalItemCount: config.onlyShowTotalItemCount === true + onlyShowTotalItemCount: config.onlyShowTotalItemCount === true, + showFollowingAlphaLetters: config.showFollowingAlphaLetters === true }; } @@ -65,6 +67,10 @@ infiniteScroll_onlyShowTotalItemCount: { on: 'The "1 of 100" text will be hidden in favor of just showing the total item count of the library', off: 'The paginated item count is unchanged.' + }, + infiniteScroll_showFollowingAlphaLetters: { + on: 'The Alpha Picker loads items from the Letter chosen, and all following letters.', + off: 'The Alpha Picker filters to only show items whose name starts with the chosen letter.' } }; @@ -113,6 +119,11 @@ config.onlyShowTotalItemCount === true, 'Only Show Total Item Count' )} + ${buildToggleCard( + 'infiniteScroll_showFollowingAlphaLetters', + config.showFollowingAlphaLetters === true, + 'Show items from Letters following the one selected in the Alpha Picker' + )} `; } @@ -120,7 +131,8 @@ function collectConfig() { return { hidePageControls: document.getElementById('infiniteScroll_hidePageControls')?.checked === true, - onlyShowTotalItemCount: document.getElementById('infiniteScroll_onlyShowTotalItemCount')?.checked === true + onlyShowTotalItemCount: document.getElementById('infiniteScroll_onlyShowTotalItemCount')?.checked === true, + showFollowingAlphaLetters: document.getElementById('infiniteScroll_showFollowingAlphaLetters')?.checked === true }; } diff --git a/configuration/userManager-configuration.js b/configuration/userManager-configuration.js index 782f7a3..b8c58d6 100644 --- a/configuration/userManager-configuration.js +++ b/configuration/userManager-configuration.js @@ -458,6 +458,14 @@ `; + let settled = false; + const finish = (val) => { + if (settled) return; + settled = true; + window.ModalSystem.close(TAG_MODAL_ID); + resolve(val); + }; + const modal = window.ModalSystem.create({ id: TAG_MODAL_ID, title, @@ -469,10 +477,6 @@ dialogStyle: { maxWidth: '420px', width: '90vw' }, onOpen: (m) => { const input = m.dialogContent.querySelector('#um-tag-input'); - const finish = (val) => { - window.ModalSystem.close(TAG_MODAL_ID); - resolve(val); - }; m.dialogFooter.querySelector('#um-tag-cancel')?.addEventListener('click', () => finish(null)); m.dialogFooter.querySelector('#um-tag-ok')?.addEventListener('click', () => { finish(input?.value?.trim() || null); @@ -485,7 +489,7 @@ }); setTimeout(() => input?.focus(), 50); }, - onClose: () => resolve(null) + onClose: () => finish(null) }); }); } diff --git a/kefinTweaks-default-config.js b/kefinTweaks-default-config.js index 806d222..87cc886 100644 --- a/kefinTweaks-default-config.js +++ b/kefinTweaks-default-config.js @@ -43,7 +43,8 @@ window.KefinTweaksDefaultConfig = { }, "infiniteScroll": { "hidePageControls": false, - "onlyShowTotalItemCount": false + "onlyShowTotalItemCount": false, + "showFollowingAlphaLetters": false }, "thumbnailScrubber": { "PreviewOnHover": false, diff --git a/scripts/cardBuilder.css b/scripts/cardBuilder.css index 4f0a6dd..6ee6846 100644 --- a/scripts/cardBuilder.css +++ b/scripts/cardBuilder.css @@ -4798,4 +4798,102 @@ html.layout-tv .spotlight-section .spotlight-tagline { bottom: 0; } - /* --- End Variant Row Layouts --- */ \ No newline at end of file +/* Ranked Lists */ + +.ranked-list .itemsContainer { + counter-reset: top-rank; + padding-left: 4rem; +} + +.ranked-list .itemsContainer > .card { + counter-increment: top-rank; + position: relative; + display: inline-block; + vertical-align: top; + padding-left: 5em; +} + +.ranked-list .itemsContainer > .card .cardScalable::before { + content: counter(top-rank); + position: absolute; + left: -4rem; + top: 50%; + font-size: 9rem; + font-weight: 900; + line-height: 1; + font-family: sans-serif; + color: #141414; + -webkit-text-stroke: 3px rgba(255, 255, 255, 0.4); + text-stroke: 3px rgba(255, 255, 255, 0.4); + z-index: 0; + pointer-events: none; + transform: translateY(-50%); +} + +.ranked-list .itemsContainer > .card .cardScalable { + position: relative; + z-index: 1; +} + +/* --- End Variant Row Layouts --- */ + +/* + * Library list action buttons and the community-rating star. Jellyfin puts these + * rules on generated classes (css-1vwv0k3 for the small IconButton, css-iguwhy + * for the medium SvgIcon, and the small star SvgIcon). Those names change + * between builds, so keep the copies here and update them when Jellyfin changes. + */ +.itemsContainer.vertical-list .listViewUserDataButtons .listItemButton { + display: inline-flex; + align-items: center; + justify-content: center; + position: relative; + box-sizing: border-box; + background-color: transparent; + outline: 0; + border: 0; + margin: 0; + cursor: pointer; + user-select: none; + vertical-align: middle; + appearance: none; + text-decoration: none; + text-align: center; + flex: 0 0 auto; + border-radius: 50%; + color: var(--jf-palette-action-active); + transition: background-color 150ms cubic-bezier(0.4, 0, 0.2, 1); + --IconButton-hoverBg: rgba(var(--jf-palette-action-activeChannel) / var(--jf-palette-action-hoverOpacity)); +} + +.itemsContainer.vertical-list .listViewUserDataButtons .listItemButton.MuiIconButton-sizeSmall { + padding: 5px; + font-size: 1.125rem; +} + +.itemsContainer.vertical-list .listViewUserDataButtons .listItemButton:hover { + background-color: var(--IconButton-hoverBg); +} + +.itemsContainer.vertical-list .listViewUserDataButtons .listItemButton .MuiSvgIcon-fontSizeMedium { + user-select: none; + width: 1em; + height: 1em; + display: inline-block; + flex-shrink: 0; + transition: fill 300ms cubic-bezier(0.4, 0, 0.2, 1); + fill: currentcolor; + font-size: 1.5rem; +} + +.itemsContainer.vertical-list .starRatingContainer .MuiSvgIcon-fontSizeSmall { + user-select: none; + width: 1em; + height: 1em; + display: inline-block; + flex-shrink: 0; + transition: fill 300ms cubic-bezier(0.4, 0, 0.2, 1); + fill: currentcolor; + font-size: 1.25rem; + color: var(--jf-palette-starIcon-main); +} \ No newline at end of file diff --git a/scripts/cardBuilder.js b/scripts/cardBuilder.js index 0260008..fda1f4f 100644 --- a/scripts/cardBuilder.js +++ b/scripts/cardBuilder.js @@ -3166,6 +3166,8 @@ invalidateScrollerMetrics: invalidateScrollerMetrics, getItemMaterialIcon: getItemMaterialIcon, createLibraryButtonElement: createLibraryButtonElement, + createListItemCard: createListItemCard, + createListItemContainer: createListItemContainer, /** * Main entry point function to build a Jellyfin card * @param {Object} item - The Jellyfin item object @@ -3183,8 +3185,9 @@ customFooterText = null, useParentCard = false, borderStyle = null, + cardDisplay = null, ) { - return createCardElement(item, overflowCard, cardFormat, customFooterText, useParentCard, borderStyle); + return createCardElement(item, overflowCard, cardFormat, customFooterText, useParentCard, borderStyle, null, cardDisplay); }, /** Push-register lazy images under an attached root (parallel native path). */ @@ -4443,7 +4446,8 @@ customFooterText = null, useParentCard = false, borderStyle = null, - cardImageParams = null + cardImageParams = null, + cardDisplay = null ) { cardFormat = cardFormat?.toLowerCase() || null; if (cardFormat === 'button') { @@ -4563,7 +4567,9 @@ // Card box container const cardBox = document.createElement('div'); - cardBox.className = `cardBox cardBox-bottompadded ${cardFormat === "button" ? "emby-button emby-button-foreground raised" : ""}`; + cardBox.className = cardDisplay && cardDisplay.visualCardBox === true + ? `cardBox visualCardBox ${cardFormat === "button" ? "emby-button emby-button-foreground raised" : ""}` + : `cardBox cardBox-bottompadded ${cardFormat === "button" ? "emby-button emby-button-foreground raised" : ""}`; // Card scalable container const cardScalable = document.createElement('div'); @@ -5193,6 +5199,44 @@ footerText.appendChild(footerBdi); cardTextFragment.appendChild(footerText); } + } else if (cardDisplay && (cardDisplay.showTitle === false || cardDisplay.showYear === false)) { + const showTitle = cardDisplay.showTitle !== false; + const showYear = cardDisplay.showYear !== false; + const yearText = listItemYearText(item); + if (showTitle) { + const titleLink = document.createElement('a'); + titleLink.href = cardUrl; + titleLink.className = 'itemAction textActionButton'; + if (!isCustomCard) titleLink.setAttribute('data-id', itemId); + titleLink.setAttribute('data-serverid', serverId); + titleLink.setAttribute('data-type', itemType); + titleLink.setAttribute('data-mediatype', 'undefined'); + titleLink.setAttribute('data-channelid', 'undefined'); + titleLink.setAttribute('data-isfolder', itemType === 'Series' || itemType === 'MusicAlbum' || itemType === 'Folder' || itemType === 'CollectionFolder' ? 'true' : 'false'); + titleLink.setAttribute('data-action', 'link'); + titleLink.title = item.Name || 'Unknown'; + titleLink.textContent = item.Name || 'Unknown'; + const titleBdi = document.createElement('bdi'); + titleBdi.appendChild(titleLink); + cardTextContainer.appendChild(titleBdi); + cardTextFragment.appendChild(cardTextContainer); + } + if (showYear && showTitle && yearText) { + const secondaryText = document.createElement('div'); + secondaryText.className = 'cardText cardTextCentered cardText-secondary'; + const secondaryBdi = document.createElement('bdi'); + secondaryBdi.textContent = yearText; + secondaryText.appendChild(secondaryBdi); + cardTextFragment.appendChild(secondaryText); + } else if (showYear && !showTitle && yearText) { + const imageFooter = document.createElement('div'); + imageFooter.className = 'innerCardFooter'; + const yearNode = document.createElement('div'); + yearNode.className = 'cardText cardTextCentered'; + yearNode.textContent = yearText; + imageFooter.appendChild(yearNode); + cardImageContainer.appendChild(imageFooter); + } } else { // Default: Item name as primary, year as secondary const titleLink = document.createElement('a'); @@ -5327,6 +5371,243 @@ return card; } + function isListModernUI() { + return window.KefinTweaksUtils?.isModernUI?.() === true; + } + + function listItemIsFolder(item) { + if (item?.IsFolder === true) return true; + const itemType = item?.Type || ''; + return itemType === 'Series' || itemType === 'Season' || itemType === 'MusicAlbum' + || itemType === 'CollectionFolder' || itemType === 'Folder' || itemType === 'BoxSet' + || itemType === 'Playlist' || itemType === 'Genre' || itemType === 'MusicArtist' || itemType === 'Person'; + } + + function listItemPrimaryImageUrl(item, modern) { + const id = item?.Id; + if (!id || typeof ApiClient === 'undefined' || !ApiClient.serverAddress) return ''; + const tag = item.ImageTags?.Primary; + const size = modern ? 'fillWidth=80&fillHeight=80' : 'fillWidth=80&fillHeight=80&quality=90'; + const params = tag ? `${size}&tag=${tag}` : size; + return `${ApiClient.serverAddress()}/Items/${id}/Images/Primary?${params}`; + } + + function listItemYearText(item) { + const itemType = item?.Type || ''; + if (itemType === 'Series' && item.Status === 'Continuing' && item.ProductionYear) { + return item.ProductionYear + ' - Present'; + } + if (itemType === 'Series' && item.EndDate && item.ProductionYear && item.EndDate.substring(0, 4) !== String(item.ProductionYear)) { + return item.ProductionYear + ' - ' + item.EndDate.substring(0, 4); + } + return item?.ProductionYear || item?.PremiereDate?.substring(0, 4) || ''; + } + + function appendListSvg(parent, pathD, testId) { + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.setAttribute('class', 'MuiSvgIcon-root MuiSvgIcon-fontSizeMedium'); + svg.setAttribute('focusable', 'false'); + svg.setAttribute('aria-hidden', 'true'); + svg.setAttribute('viewBox', '0 0 24 24'); + if (testId) svg.setAttribute('data-testid', testId); + const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + path.setAttribute('d', pathD); + svg.appendChild(path); + parent.appendChild(svg); + return svg; + } + + function createListItemContainer() { + const container = document.createElement('div'); + container.className = isListModernUI() + ? 'itemsContainer padded-left padded-right vertical-list MuiBox-root' + : 'itemsContainer padded-left padded-right vertical-list'; + if (!isListModernUI()) container.setAttribute('is', 'emby-itemscontainer'); + return container; + } + + function createListItemCard(item) { + const modern = isListModernUI(); + const serverId = (typeof ApiClient !== 'undefined' && ApiClient.serverId) ? ApiClient.serverId() : (item?.ServerId || ''); + const itemId = item?.Id || ''; + const itemType = item?.Type || 'Folder'; + const row = document.createElement('div'); + row.className = modern ? 'listItem listItem-border MuiBox-root' : 'listItem listItem-border'; + row.setAttribute('data-serverid', serverId); + row.setAttribute('data-id', itemId); + row.setAttribute('data-type', itemType); + row.setAttribute('data-mediatype', item?.MediaType || 'Unknown'); + row.setAttribute('data-isfolder', listItemIsFolder(item) ? 'true' : 'false'); + if (!modern) row.setAttribute('data-action', 'link'); + + const imageWrap = document.createElement('div'); + imageWrap.className = modern ? 'listItemImage itemAction MuiBox-root' : 'listItemImage itemAction lazy'; + imageWrap.setAttribute('data-action', 'link'); + const imageUrl = listItemPrimaryImageUrl(item, modern); + if (modern) { + const frame = document.createElement('div'); + if (imageUrl) { + const img = document.createElement('img'); + img.src = imageUrl; + img.alt = ''; + img.style.position = 'absolute'; + img.style.top = '0'; + img.style.right = '0'; + img.style.bottom = '0'; + img.style.left = '0'; + img.style.width = '100%'; + img.style.height = '100%'; + img.style.zIndex = '0'; + img.style.objectFit = 'cover'; + frame.appendChild(img); + } + imageWrap.appendChild(frame); + } else if (imageUrl) { + imageWrap.style.backgroundImage = `url("${imageUrl}")`; + } + + if (item?.UserData?.UnplayedItemCount > 0) { + const indicators = document.createElement('div'); + indicators.className = modern ? 'indicators listItemIndicators MuiBox-root' : 'indicators listItemIndicators'; + const count = document.createElement('div'); + count.className = modern ? 'countIndicator indicator unplayedItemCount MuiBox-root' : 'countIndicator indicator'; + count.textContent = String(item.UserData.UnplayedItemCount); + indicators.appendChild(count); + imageWrap.appendChild(indicators); + } + row.appendChild(imageWrap); + + const body = document.createElement('div'); + body.className = modern ? 'listItemBody itemAction MuiBox-root' : 'listItemBody itemAction'; + body.setAttribute('data-action', 'link'); + const name = item?.Name || 'Unknown'; + if (modern) { + const nameWrap = document.createElement('div'); + nameWrap.className = 'MuiBox-root'; + const heading = document.createElement('h3'); + heading.className = 'primary listItemBodyText'; + heading.style.margin = '0'; + heading.style.fontSize = 'inherit'; + heading.style.fontWeight = '400'; + heading.style.lineHeight = '1.2'; + const bdi = document.createElement('bdi'); + bdi.textContent = name; + heading.appendChild(bdi); + nameWrap.appendChild(heading); + body.appendChild(nameWrap); + } else { + const text = document.createElement('div'); + text.className = 'listItemBodyText'; + const bdi = document.createElement('bdi'); + bdi.textContent = name; + text.appendChild(bdi); + body.appendChild(text); + } + row.appendChild(body); + + const info = document.createElement('div'); + info.className = modern ? 'secondary listItemMediaInfo MuiBox-root' : 'secondary listItemMediaInfo'; + if (item?.OfficialRating) { + const rating = document.createElement('div'); + rating.className = modern + ? 'mediaInfoItem mediaInfoText mediaInfoOfficialRating MuiBox-root' + : 'mediaInfoItem mediaInfoText mediaInfoOfficialRating'; + rating.textContent = item.OfficialRating; + info.appendChild(rating); + } + if (item?.CommunityRating) { + const stars = document.createElement('div'); + stars.className = modern + ? 'mediaInfoItem starRatingContainer mediaInfoText MuiBox-root' + : 'mediaInfoItem starRatingContainer mediaInfoText'; + if (modern) { + const svg = appendListSvg(stars, 'M12 17.27 18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z', 'StarIcon'); + svg.setAttribute('class', 'MuiSvgIcon-root MuiSvgIcon-fontSizeSmall'); + } else { + const icon = document.createElement('span'); + icon.className = 'material-icons starIcon star'; + icon.setAttribute('aria-hidden', 'true'); + stars.appendChild(icon); + } + stars.appendChild(document.createTextNode(String(Math.round(Number(item.CommunityRating) * 10) / 10))); + info.appendChild(stars); + } + if (info.childNodes.length) row.appendChild(info); + + const buttons = document.createElement('div'); + buttons.className = modern ? 'listViewUserDataButtons MuiBox-root' : 'listViewUserDataButtons'; + const played = item?.UserData?.Played === true; + const favorite = item?.UserData?.IsFavorite === true; + if (modern) { + const playedBtn = document.createElement('button'); + playedBtn.type = 'button'; + playedBtn.tabIndex = 0; + playedBtn.className = 'MuiButtonBase-root MuiIconButton-root MuiIconButton-sizeSmall listItemButton'; + playedBtn.setAttribute('data-action', 'none'); + playedBtn.title = played ? 'Mark unplayed' : 'Mark played'; + appendListSvg(playedBtn, 'M9 16.17 4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z', 'CheckIcon'); + const favBtn = document.createElement('button'); + favBtn.type = 'button'; + favBtn.tabIndex = 0; + favBtn.className = 'MuiButtonBase-root MuiIconButton-root MuiIconButton-sizeSmall listItemButton'; + favBtn.setAttribute('data-action', 'none'); + favBtn.title = favorite ? 'Remove from favorites' : 'Add to favorites'; + appendListSvg(favBtn, 'm12 21.35-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54z', 'FavoriteIcon'); + const moreBtn = document.createElement('button'); + moreBtn.type = 'button'; + moreBtn.tabIndex = 0; + moreBtn.className = 'MuiButtonBase-root MuiIconButton-root MuiIconButton-sizeMedium paper-icon-button-light listItemButton itemAction'; + moreBtn.setAttribute('data-action', 'menu'); + moreBtn.title = 'More'; + appendListSvg(moreBtn, 'M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2m0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2m0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2', 'MoreVertIcon'); + buttons.appendChild(playedBtn); + buttons.appendChild(favBtn); + buttons.appendChild(moreBtn); + } else { + const playedBtn = document.createElement('button', { is: 'emby-playstatebutton' }); + playedBtn.type = 'button'; + playedBtn.className = 'listItemButton paper-icon-button-light emby-button'; + playedBtn.setAttribute('data-id', itemId); + playedBtn.setAttribute('data-serverid', serverId); + playedBtn.setAttribute('data-itemtype', itemType); + playedBtn.setAttribute('data-played', played ? 'true' : 'false'); + playedBtn.title = played ? 'Mark unplayed' : 'Mark played'; + const playedIcon = document.createElement('span'); + playedIcon.className = played ? 'material-icons check playstatebutton-icon-played' : 'material-icons check playstatebutton-icon-unplayed'; + playedIcon.setAttribute('aria-hidden', 'true'); + playedBtn.appendChild(playedIcon); + + const favBtn = document.createElement('button', { is: 'emby-ratingbutton' }); + favBtn.type = 'button'; + favBtn.className = 'listItemButton paper-icon-button-light emby-button'; + favBtn.setAttribute('data-id', itemId); + favBtn.setAttribute('data-serverid', serverId); + favBtn.setAttribute('data-itemtype', itemType); + favBtn.setAttribute('data-likes', ''); + favBtn.setAttribute('data-isfavorite', favorite ? 'true' : 'false'); + favBtn.title = favorite ? 'Remove from favorites' : 'Add to favorites'; + const favIcon = document.createElement('span'); + favIcon.className = 'material-icons favorite'; + favIcon.setAttribute('aria-hidden', 'true'); + favBtn.appendChild(favIcon); + + const moreBtn = document.createElement('button', { is: 'paper-icon-button-light' }); + moreBtn.className = 'listItemButton itemAction paper-icon-button-light'; + moreBtn.setAttribute('data-action', 'menu'); + moreBtn.title = 'More'; + const moreIcon = document.createElement('span'); + moreIcon.className = 'material-icons more_vert'; + moreIcon.setAttribute('aria-hidden', 'true'); + moreBtn.appendChild(moreIcon); + + buttons.appendChild(playedBtn); + buttons.appendChild(favBtn); + buttons.appendChild(moreBtn); + } + row.appendChild(buttons); + return row; + } + /** * TV layout detection (Jellyfin sets layout-tv on ). * Evaluated at call time so Display Mode changes are respected after re-render. @@ -5444,9 +5725,10 @@ useParentCard = false, borderStyle = null, cardImageParams = null, + cardDisplay = null, ) { if (isTvLayout()) { - return buildTvCardElement(item, overflowCard, cardFormat, customFooterText, useParentCard, borderStyle); + return buildTvCardElement(item, overflowCard, cardFormat, customFooterText, useParentCard, borderStyle, cardDisplay); } return createJellyfinCardElement( item, @@ -5456,6 +5738,7 @@ useParentCard, borderStyle, cardImageParams, + cardDisplay, ); } @@ -5553,6 +5836,7 @@ customFooterText = null, useParentCard = false, borderStyle = null, + cardDisplay = null, ) { const normalizedFormat = (cardFormat || '').toLowerCase() || null; if (normalizedFormat === 'button') { @@ -5571,6 +5855,8 @@ customFooterText, useParentCard, borderStyle, + null, + cardDisplay, ); return convertDesktopCardToTvButton(desktopCard, item); } diff --git a/scripts/infiniteScroll.js b/scripts/infiniteScroll.js index 64d5551..1cab488 100644 --- a/scripts/infiniteScroll.js +++ b/scripts/infiniteScroll.js @@ -149,7 +149,7 @@ if (!Number.isFinite(count)) return String(value); return count.toLocaleString(); } - const MODERN_LIBRARY_PAGE_SELECTOR = '#moviesPage, #tvshowsPage, #musicPage, #mixed'; + const MODERN_PAGING_PAGES = ['tv', 'movies', 'music', 'mixed']; let pagingObserver = null; let pagingApplyScheduled = false; @@ -160,19 +160,32 @@ function getInfiniteScrollOptions() { const options = window.KefinTweaksConfig?.infiniteScroll; if (!options || typeof options !== 'object' || Array.isArray(options)) { - return { hidePageControls: false, onlyShowTotalItemCount: false }; + return { hidePageControls: false, onlyShowTotalItemCount: false, showFollowingAlphaLetters: false }; } return { hidePageControls: options.hidePageControls === true, - onlyShowTotalItemCount: options.onlyShowTotalItemCount === true + onlyShowTotalItemCount: options.onlyShowTotalItemCount === true, + showFollowingAlphaLetters: options.showFollowingAlphaLetters === true }; } + function isMainLibraryPagingPage() { + const page = document.documentElement.dataset.activePage || ''; + return MODERN_PAGING_PAGES.includes(page); + } + + function modernPagingHeader() { + if (!isMainLibraryPagingPage()) return null; + return document.querySelector('header.MuiAppBar-root'); + } + function pageControlHideCss() { if (!getInfiniteScrollOptions().hidePageControls) return ''; - const modernButtons = MODERN_LIBRARY_PAGE_SELECTOR - .split(',') - .map((page) => `${page.trim()} button[title="Previous"], ${page.trim()} button[title="Next"]`) + const modernButtons = MODERN_PAGING_PAGES + .map((page) => [ + `html[data-active-page="${page}"] header.MuiAppBar-root .MuiToolbar-root button[title="Previous"]`, + `html[data-active-page="${page}"] header.MuiAppBar-root .MuiToolbar-root button[title="Next"]` + ].join(',\n')) .join(',\n'); return ` .libraryPage:not(.hide) .paging .listPaging .btnPreviousPage, @@ -246,6 +259,8 @@ if (view) scopes.push(view); const libraryPage = document.querySelector('.libraryPage:not(.hide)'); if (libraryPage && !scopes.includes(libraryPage)) scopes.push(libraryPage); + const header = modernPagingHeader(); + if (header && !scopes.includes(header)) scopes.push(header); return scopes; } @@ -294,7 +309,7 @@ function applyTotalCountLabels() { if (!isInfiniteScrollEnabled() || !getInfiniteScrollOptions().onlyShowTotalItemCount) return; - if (!getLibraryViewRoot()) return; + if (!getLibraryViewRoot() && !modernPagingHeader()) return; forEachCountLabel((el) => { const total = totalFromLabel(el); if (!total) return; @@ -323,12 +338,15 @@ disconnectPagingObserver(); if (!isInfiniteScrollEnabled() || !getInfiniteScrollOptions().onlyShowTotalItemCount) return; const root = getLibraryViewRoot() || document.querySelector('.libraryPage:not(.hide)'); - if (!root) return; + const header = modernPagingHeader(); + if (!root && !header) return; applyTotalCountLabels(); pagingObserver = new MutationObserver(() => { scheduleApplyTotalCountLabels(); }); - pagingObserver.observe(root, { subtree: true, childList: true, characterData: true }); + const observe = { subtree: true, childList: true, characterData: true }; + if (root) pagingObserver.observe(root, observe); + if (header && header !== root) pagingObserver.observe(header, observe); } // Wait for ApiClient to be available @@ -549,7 +567,7 @@ hashBucketCount = null; } - if (!letter || !isSortNameSort(sortBy)) { + if (!letter || !isSortNameSort(sortBy) || !getInfiniteScrollOptions().showFollowingAlphaLetters) { return letter ? { mode: 'exact', startIndex: displayedCount, nameStartsWith: letter } : { mode: 'none', startIndex: displayedCount }; @@ -588,14 +606,22 @@ apiParams.StartIndex = query.startIndex; } + // Grid cards are direct children. A modern list wraps every row, so count those rows. + function renderedItemCount(container) { + if (!container) return 0; + const listHost = listRowHost(container); + if (listHost) return listHost.querySelectorAll(':scope > .listItem').length; + return container.children.length; + } + // Get current start index based on existing items count function getCurrentStartIndex() { const container = getContainer(); if (!container) return 0; - - const existingItemsCount = container.children.length; + + const existingItemsCount = renderedItemCount(container); LOG('Current start index based on existing items:', existingItemsCount); - + return existingItemsCount; } @@ -604,7 +630,7 @@ const container = getContainer(); if (!container) return; - const currentItemsCount = container.children.length; + const currentItemsCount = renderedItemCount(container); const totalCount = totalRecordCount || '?'; const showTotalOnly = getInfiniteScrollOptions().onlyShowTotalItemCount; @@ -656,6 +682,29 @@ return null; } + function listRowHost(container) { + if (!container) return null; + if (container.querySelector(':scope > .listItem')) return container; + const nested = container.querySelector('.listItem'); + if (nested && nested.parentElement && container.contains(nested)) return nested.parentElement; + if (container.classList.contains('vertical-list')) return container; + return null; + } + + function inferCardDisplayFromContainer(container) { + const card = container && container.querySelector('.card'); + const cardBox = card && card.querySelector('.cardBox'); + if (!card || !cardBox) return null; + const showTitle = Array.from(card.querySelectorAll('.cardText-first')).some((el) => !el.closest('.innerCardFooter')); + const showYear = !!card.querySelector('.cardText-secondary') + || Array.from(card.querySelectorAll('.innerCardFooter .cardText')).some((el) => !el.querySelector('.itemProgressBar')); + return { + showTitle: showTitle, + showYear: showYear, + visualCardBox: cardBox.classList.contains('visualCardBox') + }; + } + async function loadMore() { LOG('LoadMore called:', { loading, hasMore, isRequestInProgress, isScrollTriggered }); @@ -808,14 +857,28 @@ container: container }); - const cardFormat = inferCardFormatFromContainer(container); - const indexBase = container.children.length; - LOG('Inferred card format from container:', cardFormat); - items.forEach((item, index) => { - const card = window.cardBuilder.buildCard(item, false, cardFormat); - card.setAttribute('data-index', indexBase + index); - container.appendChild(card); - }); + const listHost = listRowHost(container); + if (listHost && typeof window.cardBuilder.createListItemCard === 'function') { + const indexBase = listHost.querySelectorAll(':scope > .listItem').length; + LOG('Appending list rows:', { itemCount: items.length, indexBase: indexBase }); + items.forEach((item, index) => { + const row = window.cardBuilder.createListItemCard(item); + row.setAttribute('data-index', indexBase + index); + listHost.appendChild(row); + }); + } else { + const cardFormat = inferCardFormatFromContainer(container); + const cardDisplay = inferCardDisplayFromContainer(container); + const indexBase = container.children.length; + LOG('Inferred card format from container:', cardFormat, cardDisplay); + items.forEach((item, index) => { + const card = cardDisplay + ? window.cardBuilder.buildCard(item, false, cardFormat, null, false, null, cardDisplay) + : window.cardBuilder.buildCard(item, false, cardFormat); + card.setAttribute('data-index', indexBase + index); + container.appendChild(card); + }); + } if (typeof window.cardBuilder.registerLazyImagesIn === 'function') { window.cardBuilder.registerLazyImagesIn(container); } diff --git a/scripts/utils.js b/scripts/utils.js index a076b45..0f36557 100644 --- a/scripts/utils.js +++ b/scripts/utils.js @@ -3012,6 +3012,31 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`; }, FALLBACK_READY_MS); } + const LIBRARY_TAB_SLUGS = { + tv: ['suggestions', 'upcoming', 'genres', 'networks', 'episodes', 'collections', 'playlists'], + movies: ['suggestions', 'favorites', 'collections', 'genres'], + music: ['suggestions', 'albumartists', 'artists', 'playlists', 'songs', 'genres'], + homevideos: ['suggestions', 'favorites', 'collections'], + books: ['suggestions', 'collections', 'genres'], + mixed: [] + }; + + function libraryRoutePage(hash) { + const routes = ['homevideos', 'movies', 'music', 'books', 'mixed', 'tv']; + for (const route of routes) { + const pattern = new RegExp('#/' + route + '(?:\\.html)?(?:[?#]|$)'); + if (pattern.test(hash)) return route; + } + return ''; + } + + function libraryPageName(route, tabIndex) { + const index = tabIndex == null ? 0 : parseInt(tabIndex, 10); + if (!Number.isFinite(index) || index <= 0) return route; + const slug = LIBRARY_TAB_SLUGS[route]?.[index - 1]; + return slug ? route + '-' + slug : route + '-tab' + index; + } + function updateActivePage() { const hash = String(window.location.hash || ''); let watchlistUrl = activePageWatchlistUrl || window.KefinTweaksUtils?._watchlistUrl || null; @@ -3019,6 +3044,7 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`; let page = ''; const tabMatch = hash.match(/[?&]tab=(\d+)/); const tabIndex = tabMatch ? tabMatch[1] : null; + const libraryRoute = libraryRoutePage(hash); if (hash.includes('/home') && tabIndex === '1') { page = 'favorites'; @@ -3026,16 +3052,12 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`; page = 'watchlist'; } else if (hash === '#/' || hash === '#' || hash === '' || (hash.includes('#/home') && (tabIndex === '0' || tabIndex === null))) { page = 'home'; - } else if (hash.includes('#/tv')) { - page = 'tv'; - } else if (hash.includes('#/movies')) { - page = 'movies'; + } else if (libraryRoute) { + page = libraryPageName(libraryRoute, tabIndex); } else if (hash.includes('#/playlists')) { page = 'playlists'; } else if (hash.includes('#/collections')) { page = 'collections'; - } else if (hash.includes('#/music')) { - page = 'music'; } else if (hash.includes('#/games')) { page = 'games'; }