Files
KefinTweaks/headerTabs.js
T

501 lines
20 KiB
JavaScript

// Header Tabs Script
// Enhances header tab functionality by updating URL parameters when tabs are clicked
// Maintains tab state in the URL for better navigation and bookmarking
(function() {
'use strict';
// Common logging function
const LOG = (...args) => console.log('[KefinTweaks HeaderTabs]', ...args);
const WARN = (...args) => console.warn('[KefinTweaks HeaderTabs]', ...args);
const ERR = (...args) => console.error('[KefinTweaks HeaderTabs]', ...args);
LOG('Initializing...');
const SUPPORTED_PAGES = ['home', 'home.html', 'tv', 'tv.html', 'movies', 'movies.html', 'music', 'music.html', 'livetv', 'livetv.html'];
// Get current page from URL hash
function getCurrentPage() {
const hash = window.location.hash;
const hashMatches = hash.match(/#\/([^?]+)/g);
if (!hashMatches || hashMatches.length === 0) return null;
for (const match of hashMatches) {
const pageFromHash = match.replace('#/', '');
if (SUPPORTED_PAGES.includes(pageFromHash)) {
return pageFromHash;
}
}
return null;
}
// Get parent ID from URL (topParentId or parentId)
function getParentIdFromUrl() {
const hash = window.location.hash;
if (!hash.includes('?')) return null;
const [hashPath, hashSearch] = hash.split('?');
const params = new URLSearchParams(hashSearch);
// Prefer topParentId, fall back to parentId
return params.get('topParentId') || params.get('parentId') || null;
}
// Get current tab index from URL parameter
// Returns null if no tab parameter is present (so we can fetch from preferences)
function getCurrentTabFromUrl() {
const hash = window.location.hash;
if (!hash.includes('?')) return null;
const [hashPath, hashSearch] = hash.split('?');
const params = new URLSearchParams(hashSearch);
const tabParam = params.get('tab');
if (tabParam) {
const tabIndex = parseInt(tabParam, 10);
if (!isNaN(tabIndex) && tabIndex >= 0) {
LOG('Current tab from URL:', tabIndex);
return tabIndex;
}
}
return null;
}
let _userDisplayPreferences = null;
// Fetch display preferences and find matching tab based on landing preference
// Returns the tab index if found, or null if no match/preference found
// landingKey can be either a parentId (e.g., "f137a2dd21bbc1b99aa5c0f6bf02a805") or a page name (e.g., "livetv")
async function getTabFromDisplayPreferences(landingKey) {
if (!landingKey || !window.ApiClient) {
LOG('No landingKey or ApiClient not available');
return null;
}
try {
const userId = window.ApiClient.getCurrentUserId();
if (!userId) {
LOG('No user ID available');
return null;
}
if (!_userDisplayPreferences) {
const serverAddress = window.ApiClient._serverAddress;
const accessToken = window.ApiClient.accessToken();
if (!serverAddress || !accessToken) {
LOG('Server address or access token not available');
return null;
}
const response = await fetch(
`${serverAddress}/DisplayPreferences/usersettings?userId=${userId}&client=emby`,
{
headers: {
'X-Emby-Token': accessToken
}
}
);
if (!response.ok) {
WARN('Failed to fetch display preferences:', response.status);
return null;
}
_userDisplayPreferences = await response.json();
LOG('Fetched display preferences for landing tab preference');
}
const customPrefs = _userDisplayPreferences.CustomPrefs || {};
// Look for landing-{landingKey} in CustomPrefs
const fullLandingKey = `landing-${landingKey}`;
const landingValue = customPrefs[fullLandingKey];
if (!landingValue) {
LOG(`No landing preference found for ${fullLandingKey}`);
return null;
}
LOG(`Found landing preference for ${fullLandingKey}:`, landingValue);
// Find the tab button whose inner div text matches the landing value
const headerTabs = document.querySelector('.headerTabs');
if (!headerTabs) {
LOG('Header tabs not found');
return null;
}
const buttons = headerTabs.querySelectorAll('.emby-tab-button');
for (const button of buttons) {
const div = button.querySelector('div');
if (div && div.innerText && div.innerText.replace(/\s+/g, '').trim().toLowerCase() === landingValue.replace(/\s+/g, '').trim().toLowerCase()) {
const tabIndex = parseInt(button.getAttribute('data-index') || '0', 10);
LOG(`Matched landing preference "${landingValue}" to tab index:`, tabIndex);
return tabIndex;
}
}
LOG(`No tab button found matching landing preference "${landingValue}"`);
return null;
} catch (error) {
ERR('Error fetching display preferences:', error);
return null;
}
}
// Update URL with tab parameter
function updateUrlWithTab(tabIndex) {
const currentHash = window.location.hash;
const currentPage = getCurrentPage();
if (!currentPage) return;
// Check if there's a parentId - if not, don't add tab=0
const parentId = getParentIdFromUrl();
const shouldIncludeTab = tabIndex !== 0 || parentId !== null;
let newHash = currentHash;
if (currentHash.includes('?')) {
// Replace existing tab parameter and clear watchlist-specific params
const [hashPath, hashSearch] = currentHash.split('?');
const params = new URLSearchParams(hashSearch);
// Clear watchlist-specific parameters when switching header tabs
params.delete('pageTab');
params.delete('page');
params.delete('sort');
params.delete('sortDirection');
params.delete('movieSort');
params.delete('movieSortDirection');
if (shouldIncludeTab) {
params.set('tab', tabIndex.toString());
} else {
// Remove tab parameter if it's tab 0 and no parentId
params.delete('tab');
}
const paramString = params.toString();
newHash = paramString ? `${hashPath}?${paramString}` : hashPath;
} else {
// Add tab parameter only if we should include it
if (shouldIncludeTab) {
newHash = `${currentHash}?tab=${tabIndex}`;
} else {
// Don't add tab parameter for tab 0 when no parentId
newHash = currentHash;
}
}
// Manually notify handlers for Header Tab navigation because it doesn't trigger normal Jellyfin navigation
window.KefinTweaksUtils.notifyHandlers(currentPage, document, newHash);
// Replace history entry instead of adding new one
const newUrl = window.location.origin + window.location.pathname + '#' + newHash.substring(1);
window.history.replaceState(null, '', newUrl);
LOG('Updated URL with tab:', tabIndex, newUrl);
}
// Handle tab button clicks
function handleTabClick(event) {
const clickedButton = event.currentTarget;
const tabIndex = parseInt(clickedButton.getAttribute('data-index') || '0', 10);
const currentPage = getCurrentPage();
if (currentPage) {
updateUrlWithTab(tabIndex);
// Clean up tab content - ensure only the clicked tab has is-active
const libraryPage = document.querySelector('.libraryPage:not(.hide)');
if (libraryPage) {
const tabContents = libraryPage.querySelectorAll('.pageTabContent');
// Remove is-active class from all tab contents
tabContents.forEach(content => {
content.classList.remove('is-active');
});
// Add is-active class to the clicked tab content
const targetContent = libraryPage.querySelector(`.pageTabContent[data-index="${tabIndex}"]`);
if (targetContent) {
targetContent.classList.add('is-active');
LOG('Set active tab content:', tabIndex);
} else {
WARN('Target tab content not found for index:', tabIndex);
}
}
window.scrollTo(0, 0);
LOG('Tab clicked:', currentPage, tabIndex);
}
}
// Synchronize active tab state with URL parameter
async function syncActiveTabState() {
const headerTabs = document.querySelector('.headerTabs');
if (!headerTabs) return;
let currentTabFromUrl = getCurrentTabFromUrl();
// If no tab parameter in URL, try to fetch from display preferences
if (currentTabFromUrl === null) {
const parentId = getParentIdFromUrl();
let matchedTab = null;
if (parentId) {
// Try to get landing preference using parentId
LOG('No tab parameter found, fetching from display preferences for parentId:', parentId);
matchedTab = await getTabFromDisplayPreferences(parentId);
} else {
// No parentId, try to get landing preference using page name (e.g., "livetv", "home")
const currentPage = getCurrentPage();
if (currentPage) {
// Remove .html extension if present for the landing key
const pageName = currentPage.replace('.html', '');
LOG('No tab parameter and no parentId, fetching from display preferences for page:', pageName);
matchedTab = await getTabFromDisplayPreferences(pageName);
}
}
if (matchedTab !== null) {
// Only update URL if we successfully matched a landing preference
currentTabFromUrl = matchedTab;
updateUrlWithTab(matchedTab);
} else {
// No match found, use tab 0 for active state but don't update URL
LOG('No landing preference match found, using tab 0 without updating URL');
currentTabFromUrl = 0;
}
}
// Patch for media bar because it usually reacts slowly and hides later
// Hide media bar slideshow if we aren't specifically on the home page first tab
const currentPage = getCurrentPage();
if (currentPage !== 'home' && currentPage !== 'home.html' || currentTabFromUrl !== 0) {
const mediaBarSlideshow = document.getElementById('slides-container');
if (mediaBarSlideshow) {
mediaBarSlideshow.style.display = 'none';
}
}
const buttons = headerTabs.querySelectorAll('.emby-tab-button');
const activeButton = headerTabs.querySelector('.emby-tab-button-active');
const activeTabContent = document.querySelector('.libraryPage:not(.hide) .pageTabContent.is-active');
const activeTabContentIndex = activeTabContent ? activeTabContent.getAttribute('data-index') : null;
// Remove is-active class from the active tab content if it's not the current tab
if (activeTabContentIndex && activeTabContentIndex !== currentTabFromUrl.toString()) {
activeTabContent.classList.remove('is-active');
}
// Add is-active class to the current tab content if it's not already active
if (!activeTabContentIndex || activeTabContentIndex !== currentTabFromUrl.toString()) {
const targetContent = document.querySelector(`.libraryPage:not(.hide) .pageTabContent[data-index="${currentTabFromUrl}"]`);
if (targetContent) {
targetContent.classList.add('is-active');
LOG('Set active tab content:', currentTabFromUrl);
}
}
if (activeButton && activeButton.getAttribute('data-index') === currentTabFromUrl.toString()) {
return;
}
// Remove active class from all buttons
buttons.forEach(button => {
button.classList.remove('emby-tab-button-active');
});
// Find the button with the correct data-index
const correctButton = Array.from(buttons).find(button => {
const buttonIndex = parseInt(button.getAttribute('data-index') || '0', 10);
return buttonIndex === currentTabFromUrl;
});
if (correctButton) {
correctButton.classList.add('emby-tab-button-active');
LOG('Set active tab button to index:', currentTabFromUrl);
}
}
// Add click listeners to tab buttons
function addClickListeners() {
const headerTabs = document.querySelector('.headerTabs');
if (headerTabs) {
const buttons = headerTabs.querySelectorAll('.emby-tab-button');
buttons.forEach(button => {
button.removeEventListener('click', handleTabClick);
button.addEventListener('click', handleTabClick);
});
LOG('Added click listeners to', buttons.length, 'buttons');
// Synchronize active tab state with URL parameter
syncActiveTabState().catch(err => {
ERR('Error in syncActiveTabState:', err);
});
}
}
// TODO - Replace the MutationObservers with a less taxing alternative
// Setup observer to watch for emby-tab-button additions
function setupHeaderTabsObserver() {
const headerTabs = document.querySelector('.headerTabs');
if (!headerTabs) {
LOG('No .headerTabs found, will retry...');
setTimeout(setupHeaderTabsObserver, 500);
return;
}
LOG('Setting up headerTabs observer');
const observer = new MutationObserver(function(mutations) {
let shouldReinit = false;
mutations.forEach(mutation => {
if (mutation.type === 'childList') {
const addedNodes = Array.from(mutation.addedNodes);
const removedNodes = Array.from(mutation.removedNodes);
const hasTabButtons = addedNodes.some(node =>
node.nodeType === Node.ELEMENT_NODE &&
(node.classList.contains('emby-tab-button') ||
node.querySelector && node.querySelector('.emby-tab-button'))
) || removedNodes.some(node =>
node.nodeType === Node.ELEMENT_NODE &&
(node.classList.contains('emby-tab-button') ||
node.querySelector && node.querySelector('.emby-tab-button'))
);
if (hasTabButtons) {
shouldReinit = true;
}
}
});
if (shouldReinit) {
LOG('Tab buttons changed, reinitializing...');
setTimeout(addClickListeners, 100);
}
});
observer.observe(headerTabs, {
childList: true,
subtree: true
});
// Initial setup
addClickListeners();
}
// Setup observer to watch for pageTabContent changes
function setupObserver() {
// Find the library page container
const libraryPage = document.querySelector('.libraryPage:not(.hide)');
if (!libraryPage) {
LOG('No library page found, will retry...');
setTimeout(setupObserver, 500);
return;
}
LOG('Setting up observer on library page');
const observer = new MutationObserver(function(mutations) {
let shouldReinit = false;
mutations.forEach(mutation => {
// Check if pageTabContent elements were added/removed
if (mutation.type === 'childList') {
const addedNodes = Array.from(mutation.addedNodes);
const removedNodes = Array.from(mutation.removedNodes);
const hasPageTabContent = addedNodes.some(node =>
node.nodeType === Node.ELEMENT_NODE &&
(node.classList.contains('pageTabContent') ||
node.querySelector && node.querySelector('.pageTabContent'))
) || removedNodes.some(node =>
node.nodeType === Node.ELEMENT_NODE &&
(node.classList.contains('pageTabContent') ||
node.querySelector && node.querySelector('.pageTabContent'))
);
if (hasPageTabContent) {
shouldReinit = true;
}
}
});
if (shouldReinit) {
LOG('pageTabContent changed, reinitializing...');
setTimeout(addClickListeners, 100);
}
});
observer.observe(libraryPage, {
childList: true,
subtree: true
});
}
// Monitor for page changes
function setupPageMonitor() {
let lastPage = null;
let lastUrl = window.location.href;
function checkPageChange() {
const newPage = getCurrentPage();
const currentUrl = window.location.href;
// Check if page changed OR if URL changed (including query params)
if (newPage !== lastPage || currentUrl !== lastUrl) {
lastPage = newPage;
lastUrl = currentUrl;
if (newPage) {
LOG('Page change detected:', newPage);
// Setup observers when page changes
setupHeaderTabsObserver();
setupObserver();
// Sync active tab state after page change
setTimeout(() => {
syncActiveTabState().catch(err => {
ERR('Error in syncActiveTabState:', err);
});
}, 100);
}
}
}
// Check immediately
checkPageChange();
// Check on hash changes
window.addEventListener('hashchange', checkPageChange);
}
// Register onViewPage handler to sync active tab state
if (window.KefinTweaksUtils && window.KefinTweaksUtils.onViewPage) {
window.KefinTweaksUtils.onViewPage((view, element) => {
LOG('onViewPage handler triggered for view:', view);
// Sync active tab state when page view changes
syncActiveTabState().catch(err => {
ERR('Error in syncActiveTabState:', err);
});
}, {
pages: SUPPORTED_PAGES // Only trigger for supported pages
});
LOG('Registered onViewPage handler for syncActiveTabState');
} else {
WARN('KefinTweaksUtils.onViewPage not available');
}
// Initialize
setupPageMonitor();
setupHeaderTabsObserver();
LOG('Header tabs functionality initialized');
})();