mirror of
https://github.com/ranaldsgift/KefinTweaks.git
synced 2026-11-05 15:52:47 +01:00
Fix and improve Infinite Scroll functionality on the modern Display Mode (default in Jellyfin v12)
This commit is contained in:
1 parent
a4ca0d5e16
commit
2ad3886b3a
6 files changed
+703
-176
No files matched your search
+428
-154
@@ -14,7 +14,7 @@
|
||||
LOG('Initializing...');
|
||||
|
||||
// Supported pages (same as headerTabs)
|
||||
const SUPPORTED_PAGES = ['tv.html', 'movies.html', 'music.html', 'tv', 'movies', 'music'];
|
||||
const SUPPORTED_PAGES = ['tv.html', 'movies.html', 'music.html', 'tv', 'movies', 'music', 'mixed'];
|
||||
|
||||
// Media type configurations - Read from centralized config or use defaults
|
||||
const MEDIA_CONFIGS = {
|
||||
@@ -47,8 +47,20 @@
|
||||
includeItemTypes: 'Music',
|
||||
context: 'music',
|
||||
sortBy: 'SortName,ProductionYear'
|
||||
},
|
||||
'mixed': {
|
||||
includeItemTypes: null,
|
||||
context: 'mixed',
|
||||
sortBy: 'SortName,ProductionYear'
|
||||
}
|
||||
};
|
||||
|
||||
const MODERN_LIBRARY_PAGE_IDS = {
|
||||
movies: 'moviesPage',
|
||||
tv: 'tvshowsPage',
|
||||
music: 'musicPage',
|
||||
mixed: 'mixed'
|
||||
};
|
||||
|
||||
let userId = null;
|
||||
const limit = 100;
|
||||
@@ -66,6 +78,10 @@
|
||||
let isRequestInProgress = false; // Blocking flag to prevent multiple simultaneous requests
|
||||
let hasInitialized = false; // Prevent multiple initializations
|
||||
let isInitializing = false; // Prevent concurrent initializations
|
||||
// # is not a normal sort prefix. Track when that bucket is finished so the next
|
||||
// page can continue at A without reusing the # offset.
|
||||
let alphaRangeKey = '';
|
||||
let hashBucketCount = null;
|
||||
|
||||
// Create loading indicator element
|
||||
function createLoadingIndicator() {
|
||||
@@ -121,27 +137,70 @@
|
||||
}
|
||||
}
|
||||
|
||||
const COUNT_GROUPED_NUMBER = String.raw`\d{1,3}(?:[,\s\u00A0\u202F]\d{3})*|\d+`;
|
||||
const COUNT_RANGE_PATTERN = new RegExp(
|
||||
`^(${COUNT_GROUPED_NUMBER})\\s*-\\s*(${COUNT_GROUPED_NUMBER})\\s+of\\s+(${COUNT_GROUPED_NUMBER})\\s*$`
|
||||
);
|
||||
|
||||
function formatItemCount(value) {
|
||||
if (value == null || value === '?') return value == null ? '' : '?';
|
||||
const digits = String(value).replace(/[,\s\u00A0\u202F]/g, '');
|
||||
const count = Number(digits);
|
||||
if (!Number.isFinite(count)) return String(value);
|
||||
return count.toLocaleString();
|
||||
}
|
||||
const MODERN_LIBRARY_PAGE_SELECTOR = '#moviesPage, #tvshowsPage, #musicPage, #mixed';
|
||||
let pagingObserver = null;
|
||||
let pagingApplyScheduled = false;
|
||||
|
||||
function isInfiniteScrollEnabled() {
|
||||
return window.KefinTweaksConfig?.scripts?.infiniteScroll !== false;
|
||||
}
|
||||
|
||||
function getInfiniteScrollOptions() {
|
||||
const options = window.KefinTweaksConfig?.infiniteScroll;
|
||||
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
||||
return { hidePageControls: false, onlyShowTotalItemCount: false };
|
||||
}
|
||||
return {
|
||||
hidePageControls: options.hidePageControls === true,
|
||||
onlyShowTotalItemCount: options.onlyShowTotalItemCount === true
|
||||
};
|
||||
}
|
||||
|
||||
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"]`)
|
||||
.join(',\n');
|
||||
return `
|
||||
.libraryPage:not(.hide) .paging .listPaging .btnPreviousPage,
|
||||
.libraryPage:not(.hide) .paging .listPaging .btnNextPage,
|
||||
${modernButtons} {
|
||||
display: none !important;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
// Add CSS to hide paging controls when infinite scroll is active
|
||||
function addInfiniteScrollCSS() {
|
||||
// Check if infinite scroll is enabled in configuration
|
||||
const isEnabled = window.KefinTweaksConfig?.scripts?.infiniteScroll !== false;
|
||||
if (!isEnabled) {
|
||||
if (!isInfiniteScrollEnabled()) {
|
||||
LOG('Infinite scroll disabled in configuration, skipping CSS addition');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const styleId = 'infinite-scroll-paging-hide';
|
||||
|
||||
// Check if style already exists
|
||||
if (document.getElementById(styleId)) {
|
||||
return;
|
||||
let style = document.getElementById(styleId);
|
||||
if (!style) {
|
||||
style = document.createElement('style');
|
||||
style.id = styleId;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.id = styleId;
|
||||
|
||||
style.textContent = `
|
||||
|
||||
|
||||
${pageControlHideCss()}
|
||||
|
||||
/* Infinite scroll loading indicator */
|
||||
.infinite-scroll-loading {
|
||||
display: flex;
|
||||
@@ -178,10 +237,100 @@
|
||||
}
|
||||
`;
|
||||
|
||||
document.head.appendChild(style);
|
||||
LOG('Added CSS to hide paging controls for infinite scroll');
|
||||
}
|
||||
|
||||
function countLabelScopes() {
|
||||
const scopes = [];
|
||||
const view = getLibraryViewRoot();
|
||||
if (view) scopes.push(view);
|
||||
const libraryPage = document.querySelector('.libraryPage:not(.hide)');
|
||||
if (libraryPage && !scopes.includes(libraryPage)) scopes.push(libraryPage);
|
||||
return scopes;
|
||||
}
|
||||
|
||||
function isPagingCountLabel(el) {
|
||||
if (!el) return false;
|
||||
if (el.matches('.listPaging > span')) return true;
|
||||
if (!el.classList.contains('MuiChip-label')) return false;
|
||||
return COUNT_RANGE_PATTERN.test(String(el.textContent || '').trim())
|
||||
|| el.dataset.kefinTotalCount === 'true';
|
||||
}
|
||||
|
||||
function forEachCountLabel(callback) {
|
||||
const seen = new Set();
|
||||
countLabelScopes().forEach((scope) => {
|
||||
scope.querySelectorAll('.listPaging > span, .MuiChip-label').forEach((el) => {
|
||||
if (seen.has(el) || !isPagingCountLabel(el)) return;
|
||||
seen.add(el);
|
||||
callback(el);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function totalFromLabel(el) {
|
||||
const match = String(el?.textContent || '').trim().match(COUNT_RANGE_PATTERN);
|
||||
if (match) return match[3];
|
||||
if (el?.dataset?.kefinTotalCount === 'true' && totalRecordCount != null) {
|
||||
return String(totalRecordCount);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function renderCountLabel(el, shownCount, total) {
|
||||
if (!el) return;
|
||||
if (getInfiniteScrollOptions().onlyShowTotalItemCount) {
|
||||
const raw = total == null || total === '?' ? totalFromLabel(el) : total;
|
||||
const next = raw == null || raw === '?' ? raw : formatItemCount(raw);
|
||||
if (!next) return;
|
||||
el.dataset.kefinTotalCount = 'true';
|
||||
if (el.textContent !== next) el.textContent = next;
|
||||
return;
|
||||
}
|
||||
if (shownCount == null || total == null) return;
|
||||
delete el.dataset.kefinTotalCount;
|
||||
el.textContent = `1-${formatItemCount(shownCount)} of ${formatItemCount(total)}`;
|
||||
}
|
||||
|
||||
function applyTotalCountLabels() {
|
||||
if (!isInfiniteScrollEnabled() || !getInfiniteScrollOptions().onlyShowTotalItemCount) return;
|
||||
if (!getLibraryViewRoot()) return;
|
||||
forEachCountLabel((el) => {
|
||||
const total = totalFromLabel(el);
|
||||
if (!total) return;
|
||||
renderCountLabel(el, null, total);
|
||||
});
|
||||
}
|
||||
|
||||
function scheduleApplyTotalCountLabels() {
|
||||
if (pagingApplyScheduled) return;
|
||||
pagingApplyScheduled = true;
|
||||
requestAnimationFrame(() => {
|
||||
pagingApplyScheduled = false;
|
||||
applyTotalCountLabels();
|
||||
});
|
||||
}
|
||||
|
||||
function disconnectPagingObserver() {
|
||||
if (pagingObserver) {
|
||||
pagingObserver.disconnect();
|
||||
pagingObserver = null;
|
||||
}
|
||||
}
|
||||
|
||||
function syncPagingPresentation() {
|
||||
addInfiniteScrollCSS();
|
||||
disconnectPagingObserver();
|
||||
if (!isInfiniteScrollEnabled() || !getInfiniteScrollOptions().onlyShowTotalItemCount) return;
|
||||
const root = getLibraryViewRoot() || document.querySelector('.libraryPage:not(.hide)');
|
||||
if (!root) return;
|
||||
applyTotalCountLabels();
|
||||
pagingObserver = new MutationObserver(() => {
|
||||
scheduleApplyTotalCountLabels();
|
||||
});
|
||||
pagingObserver.observe(root, { subtree: true, childList: true, characterData: true });
|
||||
}
|
||||
|
||||
// Wait for ApiClient to be available
|
||||
function waitForApiClient() {
|
||||
return new Promise((resolve) => {
|
||||
@@ -265,7 +414,9 @@
|
||||
// Map page to media type
|
||||
const mediaTypeMap = {
|
||||
'movies.html': 'movies',
|
||||
'tv.html': 'series'
|
||||
'movies': 'movies',
|
||||
'tv.html': 'series',
|
||||
'tv': 'series'
|
||||
};
|
||||
|
||||
const mediaType = mediaTypeMap[page];
|
||||
@@ -295,7 +446,7 @@
|
||||
years: filterData.Years || '',
|
||||
genres: filterData.Genres || '',
|
||||
tags: filterData.Tags || '',
|
||||
// Add other filter properties as needed
|
||||
nameStartsWith: sortData.NameStartsWith || ''
|
||||
};
|
||||
} catch (error) {
|
||||
ERR('Error parsing sort settings from localStorage:', error);
|
||||
@@ -303,30 +454,138 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Get current alpha picker selection
|
||||
function getCurrentAlphaPicker() {
|
||||
function isModernLibraryUi() {
|
||||
return window.KefinTweaksUtils?.isModernUI?.() === true;
|
||||
}
|
||||
|
||||
function getLibraryViewRoot() {
|
||||
const page = getCurrentPage();
|
||||
if (!page) return null;
|
||||
|
||||
let activeTab = null;
|
||||
if (page === 'tv.html') {
|
||||
activeTab = document.querySelector("#seriesTab.is-active");
|
||||
} else if (page === 'movies.html') {
|
||||
activeTab = document.querySelector("#moviesTab.is-active");
|
||||
|
||||
if (isModernLibraryUi()) {
|
||||
if (getCurrentTab() !== 0) return null;
|
||||
const pageId = MODERN_LIBRARY_PAGE_IDS[page];
|
||||
if (!pageId) return null;
|
||||
return document.getElementById(pageId);
|
||||
}
|
||||
|
||||
if (!activeTab) return null;
|
||||
|
||||
const alphaPickerRow = activeTab.querySelector(".alphaPickerRow");
|
||||
if (!alphaPickerRow) return null;
|
||||
|
||||
const selectedButton = alphaPickerRow.querySelector(".alphaPickerButton-selected");
|
||||
|
||||
const libraryPage = document.querySelector(".libraryPage:not(.hide)");
|
||||
if (!libraryPage) return null;
|
||||
|
||||
if (page === 'tv.html' || page === 'tv') {
|
||||
return libraryPage.querySelector("#seriesTab.is-active");
|
||||
}
|
||||
if (page === 'movies.html' || page === 'movies') {
|
||||
return libraryPage.querySelector("#moviesTab.is-active");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function readSelectedAlphaPickerValue(root) {
|
||||
if (!root) return null;
|
||||
const selectedButton = root.querySelector(".alphaPickerButton-selected, .alphaPicker-fixed-right [aria-pressed='true']");
|
||||
if (!selectedButton) return null;
|
||||
|
||||
const dataValue = selectedButton.getAttribute('data-value');
|
||||
LOG('Found alpha picker selection:', dataValue);
|
||||
|
||||
return dataValue;
|
||||
const dataValue = selectedButton.getAttribute('data-value') || selectedButton.value;
|
||||
return dataValue ? dataValue : null;
|
||||
}
|
||||
|
||||
// Selected letter from the side alpha picker. Jellyfin stores it as NameStartsWith.
|
||||
function getCurrentAlphaPicker() {
|
||||
const view = getLibraryViewRoot();
|
||||
const fromView = readSelectedAlphaPickerValue(view);
|
||||
if (fromView) {
|
||||
LOG('Found alpha picker selection:', fromView);
|
||||
return { value: fromView, sawPicker: true };
|
||||
}
|
||||
|
||||
const libraryPage = document.querySelector(".libraryPage:not(.hide)");
|
||||
const fromPage = readSelectedAlphaPickerValue(libraryPage);
|
||||
if (fromPage) {
|
||||
LOG('Found alpha picker selection:', fromPage);
|
||||
return { value: fromPage, sawPicker: true };
|
||||
}
|
||||
|
||||
const sawPicker = [view, libraryPage].some((scope) => scope?.querySelector(".alphaPickerButton, .alphaPickerRow"));
|
||||
return { value: null, sawPicker };
|
||||
}
|
||||
|
||||
function primarySortField(sortBy) {
|
||||
return String(sortBy || '').split(',')[0].trim().toLowerCase();
|
||||
}
|
||||
|
||||
function isSortNameSort(sortBy) {
|
||||
const field = primarySortField(sortBy);
|
||||
return field === 'sortname' || field === 'name';
|
||||
}
|
||||
|
||||
function isDescendingSort(sortOrder) {
|
||||
return String(sortOrder || '').toLowerCase() === 'descending';
|
||||
}
|
||||
|
||||
function nextAlphaLetter(letter) {
|
||||
const value = String(letter || '').trim().toUpperCase();
|
||||
if (value.length !== 1 || value < 'A' || value > 'Y') return null;
|
||||
return String.fromCharCode(value.charCodeAt(0) + 1);
|
||||
}
|
||||
|
||||
function normalizeAlphaLetter(letter) {
|
||||
const value = String(letter || '').trim();
|
||||
if (value === '#') return '#';
|
||||
if (/^[a-zA-Z]$/.test(value)) return value.toUpperCase();
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Widen a selected alpha-picker letter across the rest of a SortName list.
|
||||
* Ascending uses NameStartsWithOrGreater. Descending uses NameLessThan on the
|
||||
* next letter, and Z drops the letter filter because the descending library
|
||||
* already starts there. # stays an exact bucket until it is exhausted.
|
||||
*/
|
||||
function resolveAlphaQuery(nameStartsWith, sortBy, sortOrder, displayedCount) {
|
||||
const letter = normalizeAlphaLetter(nameStartsWith);
|
||||
const identity = [letter, sortBy, sortOrder, getParentId(), getCurrentPage(), getCurrentTab()].join('|');
|
||||
if (identity !== alphaRangeKey) {
|
||||
alphaRangeKey = identity;
|
||||
hashBucketCount = null;
|
||||
}
|
||||
|
||||
if (!letter || !isSortNameSort(sortBy)) {
|
||||
return letter
|
||||
? { mode: 'exact', startIndex: displayedCount, nameStartsWith: letter }
|
||||
: { mode: 'none', startIndex: displayedCount };
|
||||
}
|
||||
|
||||
const descending = isDescendingSort(sortOrder);
|
||||
if (letter === '#') {
|
||||
if (!descending && hashBucketCount != null) {
|
||||
return { mode: 'after-hash', startIndex: Math.max(0, displayedCount - hashBucketCount), nameStartsWithOrGreater: 'A' };
|
||||
}
|
||||
return { mode: descending ? 'exact' : 'hash', startIndex: displayedCount, nameStartsWith: '#' };
|
||||
}
|
||||
|
||||
if (!/^[A-Z]$/.test(letter)) {
|
||||
return { mode: 'exact', startIndex: displayedCount, nameStartsWith: letter };
|
||||
}
|
||||
|
||||
if (!descending) {
|
||||
return { mode: 'or-greater', startIndex: displayedCount, nameStartsWithOrGreater: letter };
|
||||
}
|
||||
|
||||
const next = nextAlphaLetter(letter);
|
||||
if (!next) {
|
||||
return { mode: 'unbounded', startIndex: displayedCount };
|
||||
}
|
||||
return { mode: 'less-than', startIndex: displayedCount, nameLessThan: next };
|
||||
}
|
||||
|
||||
function applyAlphaQuery(apiParams, query) {
|
||||
delete apiParams.NameStartsWith;
|
||||
delete apiParams.NameStartsWithOrGreater;
|
||||
delete apiParams.NameLessThan;
|
||||
if (query.nameStartsWith) apiParams.NameStartsWith = query.nameStartsWith;
|
||||
if (query.nameStartsWithOrGreater) apiParams.NameStartsWithOrGreater = query.nameStartsWithOrGreater;
|
||||
if (query.nameLessThan) apiParams.NameLessThan = query.nameLessThan;
|
||||
apiParams.StartIndex = query.startIndex;
|
||||
}
|
||||
|
||||
// Get current start index based on existing items count
|
||||
@@ -341,50 +600,34 @@
|
||||
}
|
||||
|
||||
// Update pagination display to show current range
|
||||
function updatePaginationDisplay() {
|
||||
const listPagings = document.querySelectorAll(".page:not(.hide) .pageTabContent.is-active .listPaging");
|
||||
if (!listPagings.length) return;
|
||||
|
||||
listPagings.forEach(listPaging => {
|
||||
const span = listPaging.querySelector("span");
|
||||
if (!span) return;
|
||||
|
||||
const container = getContainer();
|
||||
if (!container) return;
|
||||
|
||||
const currentItemsCount = container.children.length;
|
||||
const totalCount = totalRecordCount || '?';
|
||||
|
||||
// Update the display to show current range
|
||||
span.textContent = `1-${currentItemsCount} of ${totalCount}`;
|
||||
LOG('Updated pagination display:', { currentItemsCount, totalCount });
|
||||
function updatePaginationDisplay() {
|
||||
const container = getContainer();
|
||||
if (!container) return;
|
||||
|
||||
const currentItemsCount = container.children.length;
|
||||
const totalCount = totalRecordCount || '?';
|
||||
const showTotalOnly = getInfiniteScrollOptions().onlyShowTotalItemCount;
|
||||
|
||||
forEachCountLabel((el) => {
|
||||
if (showTotalOnly) {
|
||||
const total = totalCount !== '?' ? totalCount : totalFromLabel(el);
|
||||
renderCountLabel(el, currentItemsCount, total);
|
||||
return;
|
||||
}
|
||||
if (el.matches('.listPaging > span')) {
|
||||
renderCountLabel(el, currentItemsCount, totalCount);
|
||||
}
|
||||
});
|
||||
LOG('Updated pagination display:', { currentItemsCount, totalCount, showTotalOnly });
|
||||
}
|
||||
|
||||
function getContainer() {
|
||||
const libraryPage = document.querySelector(".libraryPage:not(.hide)");
|
||||
if (!libraryPage) return null;
|
||||
|
||||
// Check if we're on the correct active tab
|
||||
const page = getCurrentPage();
|
||||
if (!page) return null;
|
||||
|
||||
let activeTab = null;
|
||||
if (page === 'tv.html' || page === 'tv') {
|
||||
activeTab = libraryPage.querySelector("#seriesTab.is-active") || document.querySelector("#tvshowsPage:not(.hide)");
|
||||
} else if (page === 'movies.html' || page === 'movies') {
|
||||
activeTab = libraryPage.querySelector("#moviesTab.is-active") || document.querySelector("#moviesPage:not(.hide)");
|
||||
}
|
||||
|
||||
if (!activeTab) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const activeTab = getLibraryViewRoot();
|
||||
if (!activeTab) return null;
|
||||
|
||||
const container = activeTab.querySelector(".itemsContainer");
|
||||
if (!container) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!container) return null;
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
@@ -421,9 +664,12 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't load more if there are no items in the container (page still loading)
|
||||
const container = getContainer();
|
||||
if (container && container.children.length === 0) {
|
||||
if (!container) {
|
||||
LOG('LoadMore blocked: no eligible items container');
|
||||
return;
|
||||
}
|
||||
if (container.children.length === 0) {
|
||||
LOG('LoadMore blocked: No items in container yet (page still loading)');
|
||||
return;
|
||||
}
|
||||
@@ -490,7 +736,6 @@
|
||||
let apiParams = {
|
||||
SortBy: mediaConfig.sortBy, // fallback
|
||||
SortOrder: "Ascending", // fallback
|
||||
IncludeItemTypes: mediaConfig.includeItemTypes,
|
||||
Recursive: true,
|
||||
Fields: "PrimaryImageAspectRatio,MediaSourceCount",
|
||||
ImageTypeLimit: 1,
|
||||
@@ -499,6 +744,9 @@
|
||||
StartIndex: startIndex,
|
||||
Limit: limit
|
||||
};
|
||||
if (mediaConfig.includeItemTypes) {
|
||||
apiParams.IncludeItemTypes = mediaConfig.includeItemTypes;
|
||||
}
|
||||
|
||||
// Apply detected sort settings if available
|
||||
if (sortSettings) {
|
||||
@@ -523,95 +771,113 @@
|
||||
LOG('Using fallback sort settings');
|
||||
}
|
||||
|
||||
// Add alpha picker support
|
||||
// Widen the selected letter across the rest of a SortName list.
|
||||
const alphaPicker = getCurrentAlphaPicker();
|
||||
if (alphaPicker) {
|
||||
apiParams.NameStartsWith = alphaPicker;
|
||||
LOG('Added alpha picker filter:', alphaPicker);
|
||||
}
|
||||
|
||||
const url = ApiClient.getUrl("Users/" + ApiClient.getCurrentUserId() + "/Items", apiParams);
|
||||
|
||||
LOG('Loading more items:', { page, tab, startIndex, limit, url });
|
||||
const nameStartsWith = alphaPicker.value
|
||||
|| (!alphaPicker.sawPicker && sortSettings?.nameStartsWith)
|
||||
|| '';
|
||||
let alphaQuery = resolveAlphaQuery(nameStartsWith, apiParams.SortBy, apiParams.SortOrder, startIndex);
|
||||
applyAlphaQuery(apiParams, alphaQuery);
|
||||
LOG('Alpha query:', alphaQuery);
|
||||
|
||||
try {
|
||||
const result = await ApiClient.getJSON(url);
|
||||
let fetchAnotherRange = true;
|
||||
while (fetchAnotherRange) {
|
||||
fetchAnotherRange = false;
|
||||
const url = ApiClient.getUrl("Users/" + ApiClient.getCurrentUserId() + "/Items", apiParams);
|
||||
LOG('Loading more items:', { page, tab, startIndex: alphaQuery.startIndex, limit, url, mode: alphaQuery.mode });
|
||||
|
||||
// Update total record count from API response
|
||||
if (result.TotalRecordCount !== undefined) {
|
||||
totalRecordCount = result.TotalRecordCount;
|
||||
LOG('Updated total record count from API:', totalRecordCount);
|
||||
}
|
||||
const result = await ApiClient.getJSON(url);
|
||||
const items = result.Items || [];
|
||||
const queryOffset = alphaQuery.startIndex + items.length;
|
||||
|
||||
if (result.Items && result.Items.length > 0) {
|
||||
const container = getContainer();
|
||||
if (container) {
|
||||
// Check if cardBuilder is available
|
||||
if (typeof window.cardBuilder === 'undefined' || !window.cardBuilder.buildCard) {
|
||||
ERR('cardBuilder not available or buildCard method missing');
|
||||
loading = false;
|
||||
isRequestInProgress = false;
|
||||
hideLoadingIndicator();
|
||||
return;
|
||||
if (items.length > 0) {
|
||||
const container = getContainer();
|
||||
if (container) {
|
||||
if (typeof window.cardBuilder === 'undefined' || !window.cardBuilder.buildCard) {
|
||||
ERR('cardBuilder not available or buildCard method missing');
|
||||
loading = false;
|
||||
isRequestInProgress = false;
|
||||
hideLoadingIndicator();
|
||||
return;
|
||||
}
|
||||
|
||||
LOG('Building cards:', {
|
||||
itemCount: items.length,
|
||||
context: mediaConfig.context,
|
||||
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);
|
||||
});
|
||||
if (typeof window.cardBuilder.registerLazyImagesIn === 'function') {
|
||||
window.cardBuilder.registerLazyImagesIn(container);
|
||||
}
|
||||
|
||||
LOG('Cards built successfully');
|
||||
}
|
||||
|
||||
LOG('Building cards:', {
|
||||
itemCount: result.Items.length,
|
||||
context: mediaConfig.context,
|
||||
container: container
|
||||
});
|
||||
|
||||
// Build each card individually and append to container
|
||||
const cardFormat = inferCardFormatFromContainer(container);
|
||||
LOG('Inferred card format from container:', cardFormat);
|
||||
result.Items.forEach((item, index) => {
|
||||
const card = window.cardBuilder.buildCard(item, false, cardFormat);
|
||||
card.setAttribute('data-index', startIndex + index);
|
||||
container.appendChild(card);
|
||||
});
|
||||
if (typeof window.cardBuilder.registerLazyImagesIn === 'function') {
|
||||
window.cardBuilder.registerLazyImagesIn(container);
|
||||
}
|
||||
|
||||
LOG('Cards built successfully');
|
||||
}
|
||||
startIndex += result.Items.length;
|
||||
|
||||
// Update pagination display
|
||||
updatePaginationDisplay();
|
||||
|
||||
// Update hasMore based on total record count
|
||||
if (totalRecordCount !== null) {
|
||||
hasMore = startIndex < totalRecordCount;
|
||||
LOG('hasMore updated based on total count:', {
|
||||
startIndex,
|
||||
totalRecordCount,
|
||||
hasMore
|
||||
|
||||
// # is its own bucket. Once it is exhausted, continue at A in this same load.
|
||||
if (alphaQuery.mode === 'hash') {
|
||||
const hashDone = items.length === 0
|
||||
|| (result.TotalRecordCount != null && queryOffset >= result.TotalRecordCount);
|
||||
if (hashDone) {
|
||||
hashBucketCount = queryOffset;
|
||||
alphaQuery = { mode: 'after-hash', startIndex: 0, nameStartsWithOrGreater: 'A' };
|
||||
applyAlphaQuery(apiParams, alphaQuery);
|
||||
fetchAnotherRange = true;
|
||||
LOG('Hash bucket exhausted, continuing from A');
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (result.TotalRecordCount !== undefined) {
|
||||
if (alphaQuery.mode === 'after-hash' && hashBucketCount != null) {
|
||||
totalRecordCount = hashBucketCount + result.TotalRecordCount;
|
||||
hasMore = queryOffset < result.TotalRecordCount;
|
||||
} else {
|
||||
totalRecordCount = result.TotalRecordCount;
|
||||
hasMore = queryOffset < totalRecordCount;
|
||||
}
|
||||
LOG('hasMore updated based on total count:', {
|
||||
queryOffset,
|
||||
totalRecordCount,
|
||||
hasMore,
|
||||
mode: alphaQuery.mode
|
||||
});
|
||||
} else {
|
||||
// Fallback to old logic if no total count available
|
||||
hasMore = result.Items.length >= limit;
|
||||
LOG('hasMore updated using fallback logic:', {
|
||||
itemsLength: result.Items.length,
|
||||
limit,
|
||||
hasMore
|
||||
totalRecordCount = null;
|
||||
hasMore = items.length >= limit;
|
||||
LOG('hasMore updated using fallback logic:', {
|
||||
itemsLength: items.length,
|
||||
limit,
|
||||
hasMore
|
||||
});
|
||||
}
|
||||
|
||||
// Scroll state persistence removed - not needed
|
||||
|
||||
LOG('Loaded items successfully:', {
|
||||
loaded: result.Items.length,
|
||||
startIndex,
|
||||
|
||||
if (!items.length) {
|
||||
hasMore = false;
|
||||
LOG('No more items to load (empty result)');
|
||||
}
|
||||
|
||||
startIndex = queryOffset;
|
||||
updatePaginationDisplay();
|
||||
LOG('Loaded items successfully:', {
|
||||
loaded: items.length,
|
||||
startIndex,
|
||||
totalRecordCount,
|
||||
hasMore
|
||||
hasMore,
|
||||
mode: alphaQuery.mode
|
||||
});
|
||||
} else {
|
||||
hasMore = false;
|
||||
LOG('No more items to load (empty result)');
|
||||
}
|
||||
|
||||
// Reset retry count on success
|
||||
|
||||
retryCount = 0;
|
||||
|
||||
} catch (err) {
|
||||
@@ -710,6 +976,8 @@
|
||||
// startIndex will be calculated dynamically based on existing items
|
||||
hasMore = true;
|
||||
totalRecordCount = null;
|
||||
alphaRangeKey = '';
|
||||
hashBucketCount = null;
|
||||
loading = false;
|
||||
retryCount = 0;
|
||||
isScrollTriggered = false;
|
||||
@@ -751,6 +1019,9 @@
|
||||
if (page === 'tv.html' || page === 'tv' || page === 'movies.html' || page === 'movies') {
|
||||
return true;
|
||||
}
|
||||
if (isModernLibraryUi() && (page === 'music' || page === 'mixed')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -781,6 +1052,7 @@
|
||||
reset();
|
||||
//loadMore(); // load first page
|
||||
setupScrollWatcher();
|
||||
syncPagingPresentation();
|
||||
} finally {
|
||||
isInitializing = false;
|
||||
}
|
||||
@@ -797,6 +1069,7 @@
|
||||
}
|
||||
|
||||
function armLibraryPageInit() {
|
||||
if (isInfiniteScrollEnabled()) syncPagingPresentation();
|
||||
if (tryInitializeFromView()) return;
|
||||
const utils = window.KefinTweaksUtils;
|
||||
if (utils?.pollUntil) {
|
||||
@@ -831,6 +1104,7 @@
|
||||
// Cleanup function
|
||||
function cleanup() {
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
disconnectPagingObserver();
|
||||
if (scrollTimeout) {
|
||||
clearTimeout(scrollTimeout);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user