mirror of
https://github.com/ranaldsgift/KefinTweaks.git
synced 2026-11-05 15:52:47 +01:00
Improve Infinite Scroll functionality- handle List View and other view options
This commit is contained in:
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -43,7 +43,8 @@ window.KefinTweaksDefaultConfig = {
|
||||
},
|
||||
"infiniteScroll": {
|
||||
"hidePageControls": false,
|
||||
"onlyShowTotalItemCount": false
|
||||
"onlyShowTotalItemCount": false,
|
||||
"showFollowingAlphaLetters": false
|
||||
},
|
||||
"thumbnailScrubber": {
|
||||
"PreviewOnHover": false,
|
||||
|
||||
+99
-1
@@ -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
@@ -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
@@ -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
@@ -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';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user