Improve Infinite Scroll functionality- handle List View and other view options

This commit is contained in:
HighImKevin
2026-10-25 17:08:21 -04:00
parent 7d010c81e4
commit 0cd35f5b16
7 changed files with 528 additions and 42 deletions
+15 -3
View File
@@ -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'
)}
</div>
`;
}
@@ -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
};
}
+9 -5
View File
@@ -458,6 +458,14 @@
<button type="button" class="emby-button raised" id="um-tag-cancel">Cancel</button>
<button type="button" class="emby-button raised button-submit" id="um-tag-ok">Add</button>`;
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)
});
});
}
+2 -1
View File
@@ -43,7 +43,8 @@ window.KefinTweaksDefaultConfig = {
},
"infiniteScroll": {
"hidePageControls": false,
"onlyShowTotalItemCount": false
"onlyShowTotalItemCount": false,
"showFollowingAlphaLetters": false
},
"thumbnailScrubber": {
"PreviewOnHover": false,
+99 -1
View File
@@ -4798,4 +4798,102 @@ html.layout-tv .spotlight-section .spotlight-tagline {
bottom: 0;
}
/* --- End Variant Row Layouts --- */
/* 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);
}
+290 -4
View File
@@ -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 <html>).
* 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);
}
+85 -22
View File
@@ -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);
}
+28 -6
View File
@@ -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';
}