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';
}