mirror of
https://github.com/ranaldsgift/KefinTweaks.git
synced 2026-11-05 15:52:47 +01:00
327 lines
11 KiB
JavaScript
327 lines
11 KiB
JavaScript
// Jellyfin Home Screen Script
|
|
// Adds custom scrollable sections to the home screen
|
|
// Requires: cardBuilder.js module to be loaded before this script
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
// Common logging function
|
|
const LOG = (...args) => console.log('[HomeScreen]', ...args);
|
|
const WARN = (...args) => console.warn('[HomeScreen]', ...args);
|
|
const ERR = (...args) => console.error('[HomeScreen]', ...args);
|
|
|
|
LOG('Script loaded - JS Injector mode');
|
|
|
|
// Custom home sections configuration
|
|
const customHomeSections = [
|
|
// Add home screen sections here with the following format:
|
|
/* {
|
|
name: 'Section Name',
|
|
id: 'playlistId', <- Make sure the playlist is Public
|
|
maxItems: 16, <- Maximum number of items to display on the home screen
|
|
order: 3 <- Order of the section on the home screen
|
|
}, */
|
|
];
|
|
|
|
// Processing flag to prevent parallel execution
|
|
let isProcessing = false;
|
|
|
|
/************ Helpers ************/
|
|
|
|
/**
|
|
* Fetches playlist data from Jellyfin API
|
|
* @param {string} playlistId - The playlist ID to fetch
|
|
* @returns {Promise<Object>} - Playlist data with ItemIds array
|
|
*/
|
|
async function fetchPlaylistData(playlistId) {
|
|
const apiClient = window.ApiClient;
|
|
const serverUrl = apiClient.serverAddress();
|
|
const token = apiClient.accessToken();
|
|
|
|
const url = `${serverUrl}/Playlists/${playlistId}`;
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
headers: {
|
|
"Authorization": `MediaBrowser Token="${token}"`
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
return data;
|
|
} catch (err) {
|
|
ERR(`Failed to fetch playlist data for ${playlistId}:`, err);
|
|
return { ItemIds: [] };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates the view more URL for a playlist
|
|
* @param {string} playlistId - The playlist ID
|
|
* @returns {string} - The complete URL for the playlist page
|
|
*/
|
|
function generateViewMoreUrl(playlistId) {
|
|
const apiClient = window.ApiClient;
|
|
const serverId = apiClient.serverId();
|
|
return `/web/#/list.html?parentId=${playlistId}&serverId=${serverId}`;
|
|
}
|
|
|
|
/**
|
|
* Renders a custom home section
|
|
* @param {Object} section - Section configuration object
|
|
* @param {HTMLElement} container - Container to append the section to
|
|
* @returns {Promise<boolean>} - Success status
|
|
*/
|
|
async function renderCustomSection(section, container) {
|
|
try {
|
|
// Fetch playlist data
|
|
const playlistData = await fetchPlaylistData(section.id);
|
|
const itemIds = playlistData.ItemIds || [];
|
|
|
|
// Limit to maxItems
|
|
const limitedItemIds = itemIds.slice(0, section.maxItems);
|
|
|
|
if (limitedItemIds.length === 0) {
|
|
return false;
|
|
}
|
|
|
|
// Generate view more URL
|
|
const viewMoreUrl = generateViewMoreUrl(section.id);
|
|
|
|
// Check if cardBuilder is available
|
|
if (typeof window.cardBuilder === 'undefined' || !window.cardBuilder.renderCardsFromIds) {
|
|
WARN("cardBuilder not available, skipping section:", section.name);
|
|
return false;
|
|
}
|
|
|
|
// Render the scrollable container
|
|
const scrollableContainer = await window.cardBuilder.renderCardsFromIds(
|
|
limitedItemIds,
|
|
section.name,
|
|
viewMoreUrl
|
|
);
|
|
|
|
// Add data attribute to track rendered sections
|
|
scrollableContainer.setAttribute('data-custom-section-id', section.id);
|
|
scrollableContainer.setAttribute('data-custom-section-name', section.name);
|
|
|
|
if (section.order) {
|
|
scrollableContainer.style.order = section.order;
|
|
}
|
|
|
|
// Append to container
|
|
container.appendChild(scrollableContainer);
|
|
|
|
return true;
|
|
|
|
} catch (err) {
|
|
ERR(`Error rendering section ${section.name}:`, err);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Renders all custom home sections
|
|
* @param {HTMLElement} container - Container to append sections to
|
|
*/
|
|
async function renderAllCustomSections(container) {
|
|
const results = await Promise.all(
|
|
customHomeSections.map(section => renderCustomSection(section, container))
|
|
);
|
|
|
|
const successCount = results.filter(Boolean).length;
|
|
LOG(`Rendered ${successCount}/${customHomeSections.length} custom sections`);
|
|
}
|
|
|
|
/************ Home Screen Observer ************/
|
|
|
|
/**
|
|
* Checks if custom sections are already rendered and renders them if not
|
|
*/
|
|
async function checkAndRenderCustomSections() {
|
|
// Check if already processing to prevent parallel execution
|
|
if (isProcessing) {
|
|
return;
|
|
}
|
|
|
|
// Try multiple selectors to find the container
|
|
const selectors = [
|
|
'.itemDetailPage:not(.hide) .homeSectionsContainer',
|
|
'.homeSectionsContainer',
|
|
'.homeSectionsContainer:not(.hide)',
|
|
'[class*="homeSections"]'
|
|
];
|
|
|
|
let homeSectionsContainer = null;
|
|
for (const selector of selectors) {
|
|
homeSectionsContainer = document.querySelector(selector);
|
|
if (homeSectionsContainer) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!homeSectionsContainer) {
|
|
return;
|
|
}
|
|
|
|
// Check if sections are already rendered
|
|
const hasRenderedSections = customHomeSections.some(section =>
|
|
homeSectionsContainer.querySelector(`[data-custom-section-id="${section.id}"]`)
|
|
);
|
|
|
|
if (hasRenderedSections) {
|
|
return;
|
|
}
|
|
|
|
// Set processing flag to prevent parallel execution
|
|
isProcessing = true;
|
|
|
|
try {
|
|
await renderAllCustomSections(homeSectionsContainer);
|
|
LOG('Custom sections rendered successfully');
|
|
} catch (error) {
|
|
ERR('Error rendering custom sections:', error);
|
|
} finally {
|
|
// Always reset the processing flag
|
|
isProcessing = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets up observer to watch for home sections container
|
|
*/
|
|
function setupHomeScreenObserver() {
|
|
// Check immediately in case the element already exists
|
|
checkAndRenderCustomSections();
|
|
|
|
// Set up MutationObserver to watch for home sections container
|
|
const observer = new MutationObserver((mutations) => {
|
|
let shouldCheck = false;
|
|
|
|
mutations.forEach((mutation) => {
|
|
if (mutation.type === 'childList') {
|
|
mutation.addedNodes.forEach((node) => {
|
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
// Check if the added node contains home sections container
|
|
if (node.querySelector && node.querySelector('.homeSectionsContainer')) {
|
|
shouldCheck = true;
|
|
}
|
|
|
|
// Also check if the added node is the home sections container itself
|
|
if (node.classList && node.classList.contains('homeSectionsContainer')) {
|
|
shouldCheck = true;
|
|
}
|
|
|
|
// Check for any class containing "home" or "section"
|
|
if (node.classList) {
|
|
for (const className of node.classList) {
|
|
if (className.toLowerCase().includes('home') || className.toLowerCase().includes('section')) {
|
|
shouldCheck = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Also check on attribute changes (like class changes)
|
|
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
|
|
const target = mutation.target;
|
|
if (target.classList && (target.classList.contains('homeSectionsContainer') ||
|
|
Array.from(target.classList).some(cls => cls.toLowerCase().includes('home') || cls.toLowerCase().includes('section')))) {
|
|
shouldCheck = true;
|
|
}
|
|
}
|
|
});
|
|
|
|
if (shouldCheck) {
|
|
setTimeout(checkAndRenderCustomSections, 100); // Small delay to ensure DOM is ready
|
|
}
|
|
});
|
|
|
|
// Start observing
|
|
observer.observe(document.body, {
|
|
childList: true,
|
|
subtree: true,
|
|
attributes: true,
|
|
attributeFilter: ['class']
|
|
});
|
|
|
|
LOG('Home screen observer initialized');
|
|
}
|
|
|
|
// Initialize the home screen observer
|
|
setupHomeScreenObserver();
|
|
|
|
// Fallback: Check periodically in case mutation observer misses something
|
|
let checkInterval = null;
|
|
const startPeriodicCheck = () => {
|
|
if (checkInterval) return;
|
|
|
|
checkInterval = setInterval(() => {
|
|
// Skip if already processing
|
|
if (isProcessing) {
|
|
return;
|
|
}
|
|
|
|
const homeSectionsContainer = document.querySelector('.homeSectionsContainer');
|
|
if (homeSectionsContainer && !homeSectionsContainer.querySelector('[data-custom-section-id]')) {
|
|
checkAndRenderCustomSections();
|
|
}
|
|
}, 2000); // Check every 2 seconds
|
|
};
|
|
|
|
// Start periodic check after a short delay
|
|
setTimeout(startPeriodicCheck, 1000);
|
|
|
|
// Debug functions for troubleshooting (available in console)
|
|
window.debugHomeScreen = function() {
|
|
LOG('Manual debug trigger called');
|
|
checkAndRenderCustomSections();
|
|
};
|
|
|
|
window.debugCustomSections = function() {
|
|
LOG('Custom sections configuration:', customHomeSections);
|
|
LOG('Processing flag status:', isProcessing);
|
|
|
|
// Check multiple selectors
|
|
const selectors = [
|
|
'.itemDetailPage:not(.hide) .homeSectionsContainer',
|
|
'.homeSectionsContainer',
|
|
'.homeSectionsContainer:not(.hide)',
|
|
'[class*="homeSections"]'
|
|
];
|
|
|
|
selectors.forEach(selector => {
|
|
const element = document.querySelector(selector);
|
|
LOG(`Selector "${selector}":`, element ? 'FOUND' : 'NOT FOUND', element);
|
|
});
|
|
|
|
// Log all elements with "home" or "section" in class name
|
|
const allElements = document.querySelectorAll('*');
|
|
const relevantElements = Array.from(allElements).filter(el =>
|
|
el.className && typeof el.className === 'string' &&
|
|
(el.className.toLowerCase().includes('home') || el.className.toLowerCase().includes('section'))
|
|
);
|
|
|
|
LOG('Elements with "home" or "section" in class:', relevantElements.map(el => ({
|
|
tagName: el.tagName,
|
|
className: el.className,
|
|
id: el.id
|
|
})));
|
|
|
|
// Check if cardBuilder is available
|
|
LOG('cardBuilder available:', typeof window.cardBuilder !== 'undefined');
|
|
LOG('renderCardsFromIds available:', typeof window.cardBuilder?.renderCardsFromIds === 'function');
|
|
};
|
|
|
|
LOG('Home screen functionality initialized');
|
|
LOG('Debug functions available: window.debugHomeScreen(), window.debugCustomSections()');
|
|
|
|
})();
|