Fix breadcrumb styles and mount location

This commit is contained in:
HighImKevin committed 2026-10-01 07:36:15 -04:00
1 parent d1b2010e09
commit 8f42625d84
2 files changed
+142 -97

No files matched your search

+5 -6
View File
@@ -3,12 +3,6 @@
display: none;
}
.MuiToolbar-root:has(.kefinTweaks-breadcrumbs-wrapper) {
display: grid;
grid-template-columns: auto 1fr auto;
grid-template-rows: 1fr auto;
}
@media (min-width: 768px) {
.kefinTweaks-breadcrumbs-wrapper {
display: block;
@@ -146,3 +140,8 @@
max-width: initial !important;
white-space: nowrap;
}
.MuiToolbar-root:has(.kefinTweaks-breadcrumbs-wrapper) {
display: grid;
grid-template-columns: auto 1fr auto;
grid-template-rows: 1fr auto;
}
+137 -91
View File
@@ -3,7 +3,7 @@
// Supports: Movie, Series, Season, Episode, MusicArtist, MusicAlbum, Audio
// Requires: utils.js module to be loaded before this script
(function() {
(function () {
'use strict';
console.log('[KefinTweaks Breadcrumbs] Initializing...');
@@ -72,12 +72,12 @@
async function getTargetBreadcrumbStructure(item) {
const structure = {
elements: [],
item: item
item: item,
};
// Don't append .html for server versions 10.11 and above
const urlSuffix = ApiClient._appVersion.split('.')[1] > 10 ? '' : '.html'
const urlSuffix = ApiClient._appVersion.split('.')[1] > 10 ? '' : '.html';
let queryParams = '';
if (item.ParentId) {
// Get the top parent item from the ancestors with a Type = CollectionFolder
@@ -85,13 +85,13 @@
const ancestorsUrl = `${ApiClient.serverAddress()}/Items/${item.Id}/Ancestors`;
const queryResult = await window.apiHelper.getQuery(ancestorsUrl, { useCache: true });
let ancestors = queryResult.data;
if (!ancestors || !Array.isArray(ancestors)) {
ancestors = await queryResult.dataPromise;
}
const collectionFolder = ancestors.find(ancestor => ancestor.Type === 'CollectionFolder');
const userRootFolder = ancestors.find(ancestor => ancestor.Type === 'UserRootFolder');
const collectionFolder = ancestors.find((ancestor) => ancestor.Type === 'CollectionFolder');
const userRootFolder = ancestors.find((ancestor) => ancestor.Type === 'UserRootFolder');
if (collectionFolder) {
queryParams = `?topParentId=${collectionFolder.Id}&serverId=${ApiClient.serverId()}`;
} else if (userRootFolder) {
@@ -99,21 +99,20 @@
} else {
queryParams = `?serverId=${ApiClient.serverId()}`;
}
}
else if (ApiClient.serverId()) {
} else if (ApiClient.serverId()) {
queryParams = `?serverId=${ApiClient.serverId()}`;
}
if (item.Type === 'Movie') {
structure.elements = [
{ text: 'Movies', url: `#/movies${urlSuffix}${queryParams}&tab=0`, clickable: true },
{ text: item.Name, url: null, clickable: false }
{ text: item.Name, url: null, clickable: false },
];
} else if (item.Type === 'Series') {
structure.elements = [
{ text: 'Shows', url: `#/tv${urlSuffix}${queryParams}&tab=0`, clickable: true },
{ text: item.Name, url: null, clickable: false },
{ text: 'All Seasons', url: null, clickable: true, popover: true }
{ text: 'All Seasons', url: null, clickable: true, popover: true },
];
} else if (item.Type === 'Season') {
// Get series details for the series name
@@ -122,8 +121,12 @@
structure.elements = [
{ text: 'Shows', url: `#/tv${urlSuffix}${queryParams}&tab=0`, clickable: true },
{ text: seriesName, url: `${ApiClient._serverAddress}/web/#/details?id=${item.ParentId}&serverId=${ApiClient.serverId()}`, clickable: true },
{ text: item.Name || `Season ${item.IndexNumber}`, url: null, clickable: true, popover: true }
{
text: seriesName,
url: `${ApiClient._serverAddress}/web/#/details?id=${item.ParentId}&serverId=${ApiClient.serverId()}`,
clickable: true,
},
{ text: item.Name || `Season ${item.IndexNumber}`, url: null, clickable: true, popover: true },
];
} else if (item.Type === 'Episode') {
// Get series details for the series name
@@ -132,26 +135,36 @@
// Get season details for the season name
const seasonDetails = await getItemDetails(item.ParentId);
const seasonName = seasonDetails ? (seasonDetails.Name || `Season ${seasonDetails.IndexNumber}`) : `Season ${item.ParentIndexNumber}`;
const seasonName = seasonDetails
? seasonDetails.Name || `Season ${seasonDetails.IndexNumber}`
: `Season ${item.ParentIndexNumber}`;
structure.elements = [
{ text: 'Shows', url: `#/tv${urlSuffix}${queryParams}&tab=0`, clickable: true },
{ text: seriesName, url: `${ApiClient._serverAddress}/web/#/details?id=${item.SeriesId}&serverId=${ApiClient.serverId()}`, clickable: true },
{
text: seriesName,
url: `${ApiClient._serverAddress}/web/#/details?id=${item.SeriesId}&serverId=${ApiClient.serverId()}`,
clickable: true,
},
{ text: seasonName, url: null, clickable: true, popover: true },
{ text: `${item.ParentIndexNumber}x${padNumber(item.IndexNumber)} - ${item.Name}`, url: null, clickable: false }
{ text: `${item.ParentIndexNumber}x${padNumber(item.IndexNumber)} - ${item.Name}`, url: null, clickable: false },
];
} else if (item.Type === 'MusicArtist') {
structure.elements = [
{ text: 'Music', url: `#/music${urlSuffix}${queryParams}&tab=0`, clickable: true },
{ text: item.Name, url: null, clickable: false },
{ text: 'All Albums', url: null, clickable: true, popover: true }
{ text: 'All Albums', url: null, clickable: true, popover: true },
];
} else if (item.Type === 'MusicAlbum') {
structure.elements = [
{ text: 'Music', url: `#/music${urlSuffix}${queryParams}&tab=0`, clickable: true },
{ text: item.AlbumArtist, url: `${ApiClient._serverAddress}/web/#/details?id=${item.ParentId}&serverId=${ApiClient.serverId()}`, clickable: true },
{
text: item.AlbumArtist,
url: `${ApiClient._serverAddress}/web/#/details?id=${item.ParentId}&serverId=${ApiClient.serverId()}`,
clickable: true,
},
{ text: item.Name, url: null, clickable: true, popover: true },
{ text: 'All Songs', url: null, clickable: true, popover: true }
{ text: 'All Songs', url: null, clickable: true, popover: true },
];
} else if (item.Type === 'Audio') {
// Get album details for the album name and artist info
@@ -162,9 +175,13 @@
structure.elements = [
{ text: 'Music', url: `#/music${urlSuffix}${queryParams}&tab=0`, clickable: true },
{ text: artistName, url: artistId ? `${ApiClient._serverAddress}/web/#/details?id=${artistId}&serverId=${ApiClient.serverId()}` : null, clickable: !!artistId },
{
text: artistName,
url: artistId ? `${ApiClient._serverAddress}/web/#/details?id=${artistId}&serverId=${ApiClient.serverId()}` : null,
clickable: !!artistId,
},
{ text: albumName, url: null, clickable: true, popover: true },
{ text: item.Name, url: null, clickable: false }
{ text: item.Name, url: null, clickable: false },
];
}
@@ -176,8 +193,8 @@
const differences = [];
// Check if we have the right number of elements
const existingElements = Array.from(breadcrumbContainer.children).filter(child =>
child.classList.contains('kefinTweaks-breadcrumb-element')
const existingElements = Array.from(breadcrumbContainer.children).filter((child) =>
child.classList.contains('kefinTweaks-breadcrumb-element'),
);
log('Comparing breadcrumbs - existing:', existingElements.length, 'target:', target.elements.length);
@@ -200,14 +217,14 @@
const targetClickable = targetElement.clickable;
// Check if popover functionality needs to be updated
const needsPopoverUpdate = targetElement.popover && (
existingText !== targetElement.text ||
existingClickable !== targetClickable ||
// Force update for "All Seasons", "All Albums", and "All Songs" elements since the underlying data changes
targetElement.text === 'All Seasons' ||
targetElement.text === 'All Albums' ||
targetElement.text === 'All Songs'
);
const needsPopoverUpdate =
targetElement.popover &&
(existingText !== targetElement.text ||
existingClickable !== targetClickable ||
// Force update for "All Seasons", "All Albums", and "All Songs" elements since the underlying data changes
targetElement.text === 'All Seasons' ||
targetElement.text === 'All Albums' ||
targetElement.text === 'All Songs');
if (existingText !== targetElement.text || existingClickable !== targetClickable || needsPopoverUpdate) {
differences.push({ action: 'update', index: i, element: targetElement });
@@ -228,8 +245,8 @@
log('Applying breadcrumb differences:', differences.length, 'changes needed');
// Get existing elements
const existingElements = Array.from(breadcrumbContainer.children).filter(child =>
child.classList.contains('kefinTweaks-breadcrumb-element')
const existingElements = Array.from(breadcrumbContainer.children).filter((child) =>
child.classList.contains('kefinTweaks-breadcrumb-element'),
);
// Process differences in reverse order to maintain indices
@@ -256,7 +273,6 @@
}
log('Added new breadcrumb element:', diff.element.text);
} else if (diff.action === 'remove') {
// Remove element and its separator
const elementToRemove = existingElements[diff.index];
@@ -268,7 +284,6 @@
breadcrumbContainer.removeChild(elementToRemove);
log('Removed breadcrumb element at index:', diff.index);
}
} else if (diff.action === 'update') {
// Update existing element
const elementToUpdate = existingElements[diff.index];
@@ -386,7 +401,7 @@
window.location.href = `${ApiClient._serverAddress}/web/#/details?id=${seasons[0].Id}&serverId=${ApiClient.serverId()}`;
});
} else {
const currentSeason = seasons.find(season => season.Id === item.ParentId);
const currentSeason = seasons.find((season) => season.Id === item.ParentId);
element.addEventListener('click', (e) => {
e.stopPropagation();
const popover = createPopover(seasons, currentSeason, (selectedSeason) => {
@@ -401,14 +416,12 @@
let albumArtists = item.AlbumArtists;
if (!albumArtists || !Array.isArray(albumArtists) || albumArtists.length === 0) {
const albumDetails = await getItemDetails(item.Id);
albumArtists = (albumDetails && albumDetails.AlbumArtists) ? albumDetails.AlbumArtists : [];
albumArtists = albumDetails && albumDetails.AlbumArtists ? albumDetails.AlbumArtists : [];
}
const artistIds = (albumArtists || []).map(a => a.Id).filter(Boolean);
const albums = artistIds.length > 0
? await getAlbumsByArtistIds(artistIds)
: await getAlbumsIfNeeded(item.ParentId);
const artistIds = (albumArtists || []).map((a) => a.Id).filter(Boolean);
const albums = artistIds.length > 0 ? await getAlbumsByArtistIds(artistIds) : await getAlbumsIfNeeded(item.ParentId);
if (albums.length === 1) {
element.addEventListener('click', () => {
window.location.href = `${ApiClient._serverAddress}/web/#/details?id=${albums[0].Id}&serverId=${ApiClient.serverId()}`;
@@ -425,12 +438,13 @@
} else if (item.Type === 'Audio') {
// Audio (Song) page - clicking album name should show albums from all AlbumArtists of the album
const albumDetails = await getItemDetails(item.ParentId);
const albumArtists = (albumDetails && albumDetails.AlbumArtists) ? albumDetails.AlbumArtists : [];
const artistIds = albumArtists.map(a => a.Id).filter(Boolean);
const albums = artistIds.length > 0
? await getAlbumsByArtistIds(artistIds)
: await getAlbumsIfNeeded(albumDetails ? albumDetails.ParentId : null);
const albumArtists = albumDetails && albumDetails.AlbumArtists ? albumDetails.AlbumArtists : [];
const artistIds = albumArtists.map((a) => a.Id).filter(Boolean);
const albums =
artistIds.length > 0
? await getAlbumsByArtistIds(artistIds)
: await getAlbumsIfNeeded(albumDetails ? albumDetails.ParentId : null);
if (albums && albums.length === 1) {
element.addEventListener('click', () => {
window.location.href = `${ApiClient._serverAddress}/web/#/details?id=${albums[0].Id}&serverId=${ApiClient.serverId()}`;
@@ -475,28 +489,26 @@
if (ApiClient._appVersion && ApiClient._appVersion.startsWith('12') && localStorage.getItem('layout')?.length === 0) {
return true;
}
return !!document.querySelector('main.MuiBox-root');
}
function resolveBreadcrumbMount() {
if (isModernUI()) {
return document.querySelector('.MuiToolbar-root');
}
return document.querySelector('.skinHeader .headerLeft');
}
function createBreadcrumbContainer() {
if (breadcrumbContainer) {
return breadcrumbContainer;
}
const isModern = isModernUI();
const mount = resolveBreadcrumbMount();
if (!mount) {
error(isModern
? 'Could not find .MuiToolbar-root for breadcrumbs'
: 'Could not find .skinHeader .headerLeft element');
error(isModern ? 'Could not find .MuiToolbar-root for breadcrumbs' : 'Could not find .skinHeader .headerLeft element');
return null;
}
@@ -531,16 +543,18 @@
wrapper.appendChild(popoverContainer);
mount.appendChild(wrapper);
log(isModern
? 'Created breadcrumb container inside .MuiToolbar-root'
: 'Created breadcrumb container with wrapper structure inside .skinHeader .headerLeft');
log(
isModern
? 'Created breadcrumb container inside .MuiToolbar-root'
: 'Created breadcrumb container with wrapper structure inside .skinHeader .headerLeft',
);
return breadcrumbContainer;
}
function ensureBreadcrumbContainer(retries = 20) {
const existing = createBreadcrumbContainer();
if (existing) return Promise.resolve(existing);
return new Promise((resolve) => {
let attempts = 0;
const tryMount = () => {
@@ -591,7 +605,7 @@
}
closePopover();
}
function closePopover() {
if (activePopover) {
activePopover.remove();
@@ -608,7 +622,7 @@
let selectedItemElement = null;
items.forEach(item => {
items.forEach((item) => {
const itemElement = document.createElement('div');
itemElement.className = 'kefinTweaks-popover-item detailsGroupItem';
@@ -643,7 +657,7 @@
selectedItemElement.scrollIntoView({
behavior: 'instant',
block: 'nearest',
inline: 'nearest'
inline: 'nearest',
});
log('Auto-scrolled to selected item:', selectedItemElement.textContent);
}, 10);
@@ -714,14 +728,14 @@
if (!currentUserId) {
throw new Error('No user ID found');
}
const url = ApiClient.getUrl(`Users/${currentUserId}/Items/${itemId}`);
const response = await window.apiHelper.getQuery(url, { useCache: true });
if (!response.data) {
response.data = await response.dataPromise;
}
const item = response.data;
log('Retrieved item details:', item.Name, item.Type);
return item;
@@ -758,7 +772,7 @@
StartIndex: 0,
CollapseBoxSetItems: false,
AlbumArtistIds: artistId,
SortBy: 'PremiereDate,ProductionYear,Sortname'
SortBy: 'PremiereDate,ProductionYear,Sortname',
});
const response = await window.apiHelper.getQuery(url, { useCache: true });
if (!response.data) {
@@ -785,7 +799,7 @@
StartIndex: 0,
CollapseBoxSetItems: false,
AlbumArtistIds: idsParam,
SortBy: 'PremiereDate,ProductionYear,Sortname'
SortBy: 'PremiereDate,ProductionYear,Sortname',
});
const response = await window.apiHelper.getQuery(url, { useCache: true });
if (!response.data) {
@@ -818,7 +832,7 @@
Fields: 'ParentId',
Limit: 100,
StartIndex: 0,
SortBy: 'IndexNumber,SortName'
SortBy: 'IndexNumber,SortName',
});
const response = await window.apiHelper.getQuery(url, { useCache: true });
if (!response.data) {
@@ -841,6 +855,26 @@
return separator;
}
// Serialize page-change work so overlapping view callbacks cannot race the DOM.
// Latest-wins pending: overwrite the single pending slot; drain runs one at a time.
let pageChangePending = undefined; // undefined = empty; null/item = pending job
let pageChangeDrain = Promise.resolve();
function enqueuePageChange(item = null) {
pageChangePending = item;
pageChangeDrain = pageChangeDrain
.then(async () => {
if (pageChangePending === undefined) return;
const next = pageChangePending;
pageChangePending = undefined;
await handlePageChange(next);
})
.catch((err) => {
error('Breadcrumb page change queue failed:', err);
});
return pageChangeDrain;
}
// Page change handler
// Simplified page change handler
async function handlePageChange(item = null) {
@@ -856,7 +890,7 @@
hideBreadcrumbs();
return;
}
// Check if the user is logged in
if (!ApiClient._loggedIn) {
clearBreadcrumbs();
@@ -880,7 +914,12 @@
// Determine target breadcrumb structure
const targetStructure = await getTargetBreadcrumbStructure(item);
log('Target breadcrumb structure:', targetStructure.elements.map(e => e.text));
log(
'Target breadcrumb structure:',
targetStructure.elements.map((e) => e.text),
);
const breadcrumbContainer = getBreadcrumbContainer();
// Check if we need to create breadcrumbs from scratch
if (!breadcrumbContainer || !currentBreadcrumbs) {
@@ -893,13 +932,16 @@
}
// Update state
currentBreadcrumbs = targetStructure;
// Update the currentBreadcrumbs from the current page
if (breadcrumbContainer && breadcrumbContainer.children) {
currentBreadcrumbs = Array.from(breadcrumbContainer.children);
}
//currentBreadcrumbs = targetStructure;
currentItemId = item.Id;
log('About to show breadcrumbs...');
showBreadcrumbs();
log('Breadcrumbs updated successfully');
} catch (err) {
error('Error handling page change:', err);
clearBreadcrumbs();
@@ -936,11 +978,15 @@
breadcrumbContainer.innerHTML = '';
// Append all elements at once
elements.forEach(el => breadcrumbContainer.appendChild(el));
elements.forEach((el) => breadcrumbContainer.appendChild(el));
log('Created breadcrumbs from scratch with', targetStructure.elements.length, 'elements');
}
function getBreadcrumbContainer() {
return document.getElementById('kefinTweaks-breadcrumbs');
}
// Window resize handler
function handleResize() {
if (currentBreadcrumbs && breadcrumbContainer) {
@@ -961,18 +1007,19 @@
log('Registering breadcrumb handler with KefinTweaksUtils');
// Register handler for all pages (breadcrumbs can appear on any detail page)
window.KefinTweaksUtils.onViewPage(async (view, element, hash, itemPromise) => {
try {
// Await the item promise to get the actual item data
const item = await itemPromise;
// Run our custom code
handlePageChange(item);
} catch (err) {
error('Breadcrumb page change handler failed:', err);
}
}, {
pages: [] // Empty array means all pages
});
window.KefinTweaksUtils.onViewPage(
async (view, element, hash, itemPromise) => {
try {
const item = await itemPromise;
await enqueuePageChange(item);
} catch (err) {
error('Breadcrumb page change handler failed:', err);
}
},
{
pages: [], // Empty array means all pages
},
);
} else {
error('KefinTweaksUtils not available, breadcrumbs may not work correctly');
}
@@ -981,7 +1028,7 @@
window.addEventListener('resize', handleResize);
// Initial check
handlePageChange();
enqueuePageChange(null);
log('Breadcrumbs initialized successfully');
}
@@ -992,5 +1039,4 @@
} else {
initialize();
}
})();