mirror of
https://github.com/ranaldsgift/KefinTweaks.git
synced 2026-11-05 15:52:47 +01:00
New feature: Skin Manager, also minor fixes and improvements
This commit is contained in:
1 parent
0ffc967d8d
commit
629f524b4c
24 files changed
+6166
-4013
No files matched your search
+40
-94
@@ -11,7 +11,6 @@
|
||||
const CONFIG = {
|
||||
supportedTypes: ['Movie', 'Series', 'Season', 'Episode', 'MusicArtist', 'MusicAlbum'],
|
||||
minWidth: 768, // Only show on screens wider than 768px
|
||||
headerHeight: 80 // Position below .skinHeader
|
||||
};
|
||||
|
||||
// State management
|
||||
@@ -59,15 +58,26 @@
|
||||
elements: [],
|
||||
item: item
|
||||
};
|
||||
|
||||
// Don't append .html for server versions 10.11 and above
|
||||
const urlSuffix = ApiClient._serverVersion.split('.')[1] > 10 ? '' : '.html'
|
||||
|
||||
let queryParams = '';
|
||||
if (item.ParentId) {
|
||||
queryParams = `?topParentId=${item.ParentId}&serverId=${ApiClient.serverId()}`;
|
||||
}
|
||||
else if (ApiClient.serverId()) {
|
||||
queryParams = `?serverId=${ApiClient.serverId()}`;
|
||||
}
|
||||
|
||||
if (item.Type === 'Movie') {
|
||||
structure.elements = [
|
||||
{ text: 'Movies', url: '#/movies.html', clickable: true },
|
||||
{ text: 'Movies', url: `#/movies${urlSuffix}${queryParams}`, clickable: true },
|
||||
{ text: item.Name, url: null, clickable: false }
|
||||
];
|
||||
} else if (item.Type === 'Series') {
|
||||
structure.elements = [
|
||||
{ text: 'Shows', url: '#/tv.html', clickable: true },
|
||||
{ text: 'Shows', url: `#/tv${urlSuffix}${queryParams}`, clickable: true },
|
||||
{ text: item.Name, url: null, clickable: false },
|
||||
{ text: 'All Seasons', url: null, clickable: true, popover: true }
|
||||
];
|
||||
@@ -77,8 +87,8 @@
|
||||
const seriesName = seriesDetails ? seriesDetails.Name : 'Unknown Series';
|
||||
|
||||
structure.elements = [
|
||||
{ text: 'Shows', url: '#/tv.html', clickable: true },
|
||||
{ text: seriesName, url: `#/details?id=${item.ParentId}&serverId=${ApiClient.serverId()}`, clickable: true },
|
||||
{ text: 'Shows', url: `#/tv${urlSuffix}${queryParams}`, 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 }
|
||||
];
|
||||
} else if (item.Type === 'Episode') {
|
||||
@@ -91,21 +101,21 @@
|
||||
const seasonName = seasonDetails ? (seasonDetails.Name || `Season ${seasonDetails.IndexNumber}`) : `Season ${item.ParentIndexNumber}`;
|
||||
|
||||
structure.elements = [
|
||||
{ text: 'Shows', url: '#/tv.html', clickable: true },
|
||||
{ text: seriesName, url: `#/details?id=${item.SeriesId}&serverId=${ApiClient.serverId()}`, clickable: true },
|
||||
{ text: 'Shows', url: `#/tv${urlSuffix}${queryParams}`, 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 }
|
||||
];
|
||||
} else if (item.Type === 'MusicArtist') {
|
||||
structure.elements = [
|
||||
{ text: 'Music', url: '#/music.html', clickable: true },
|
||||
{ text: 'Music', url: `#/music${urlSuffix}${queryParams}`, clickable: true },
|
||||
{ text: item.Name, url: null, clickable: false },
|
||||
{ text: 'All Albums', url: null, clickable: true, popover: true }
|
||||
];
|
||||
} else if (item.Type === 'MusicAlbum') {
|
||||
structure.elements = [
|
||||
{ text: 'Music', url: '#/music.html', clickable: true },
|
||||
{ text: item.AlbumArtist, url: `#/details?id=${item.ParentId}&serverId=${ApiClient.serverId()}`, clickable: true },
|
||||
{ text: 'Music', url: `#/music${urlSuffix}${queryParams}`, 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 }
|
||||
];
|
||||
}
|
||||
@@ -233,7 +243,7 @@
|
||||
if (elementDef.url) {
|
||||
// Direct link
|
||||
element.addEventListener('click', () => {
|
||||
window.location.hash = elementDef.url;
|
||||
window.location.href = elementDef.url;
|
||||
});
|
||||
} else if (elementDef.popover) {
|
||||
// Popover functionality
|
||||
@@ -262,7 +272,7 @@
|
||||
if (elementDef.url) {
|
||||
// Direct link
|
||||
newElement.addEventListener('click', () => {
|
||||
window.location.hash = elementDef.url;
|
||||
window.location.href = elementDef.url;
|
||||
});
|
||||
} else if (elementDef.popover) {
|
||||
// Popover functionality
|
||||
@@ -281,7 +291,7 @@
|
||||
element.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const popover = createPopover(seasons, null, (selectedSeason) => {
|
||||
window.location.hash = `#/details?id=${selectedSeason.Id}&serverId=${ApiClient.serverId()}`;
|
||||
window.location.href = `${ApiClient._serverAddress}/web/#/details?id=${selectedSeason.Id}&serverId=${ApiClient.serverId()}`;
|
||||
});
|
||||
showPopover(element, popover);
|
||||
});
|
||||
@@ -291,7 +301,7 @@
|
||||
element.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const popover = createPopover(albums, null, (selectedAlbum) => {
|
||||
window.location.hash = `#/details?id=${selectedAlbum.Id}&serverId=${ApiClient.serverId()}`;
|
||||
window.location.href = `${ApiClient._serverAddress}/web/#/details?id=${selectedAlbum.Id}&serverId=${ApiClient.serverId()}`;
|
||||
});
|
||||
showPopover(element, popover);
|
||||
});
|
||||
@@ -304,7 +314,7 @@
|
||||
element.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const popover = createPopover(seasons, item, (selectedSeason) => {
|
||||
window.location.hash = `#/details?id=${selectedSeason.Id}&serverId=${ApiClient.serverId()}`;
|
||||
window.location.href = `${ApiClient._serverAddress}/web/#/details?id=${selectedSeason.Id}&serverId=${ApiClient.serverId()}`;
|
||||
});
|
||||
showPopover(element, popover);
|
||||
});
|
||||
@@ -314,14 +324,14 @@
|
||||
const seasons = await getSeasonsIfNeeded(item.SeriesId);
|
||||
if (seasons.length === 1) {
|
||||
element.addEventListener('click', () => {
|
||||
window.location.hash = `#/details?id=${seasons[0].Id}&serverId=${ApiClient.serverId()}`;
|
||||
window.location.href = `${ApiClient._serverAddress}/web/#/details?id=${seasons[0].Id}&serverId=${ApiClient.serverId()}`;
|
||||
});
|
||||
} else {
|
||||
const currentSeason = seasons.find(season => season.Id === item.ParentId);
|
||||
element.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const popover = createPopover(seasons, currentSeason, (selectedSeason) => {
|
||||
window.location.hash = `#/details?id=${selectedSeason.Id}&serverId=${ApiClient.serverId()}`;
|
||||
window.location.href = `${ApiClient._serverAddress}/web/#/details?id=${selectedSeason.Id}&serverId=${ApiClient.serverId()}`;
|
||||
});
|
||||
showPopover(element, popover);
|
||||
});
|
||||
@@ -331,13 +341,13 @@
|
||||
const albums = await getAlbumsIfNeeded(item.ParentId);
|
||||
if (albums.length === 1) {
|
||||
element.addEventListener('click', () => {
|
||||
window.location.hash = `#/details?id=${albums[0].Id}&serverId=${ApiClient.serverId()}`;
|
||||
window.location.href = `${ApiClient._serverAddress}/web/#/details?id=${albums[0].Id}&serverId=${ApiClient.serverId()}`;
|
||||
});
|
||||
} else {
|
||||
element.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const popover = createPopover(albums, item, (selectedAlbum) => {
|
||||
window.location.hash = `#/details?id=${selectedAlbum.Id}&serverId=${ApiClient.serverId()}`;
|
||||
window.location.href = `${ApiClient._serverAddress}/web/#/details?id=${selectedAlbum.Id}&serverId=${ApiClient.serverId()}`;
|
||||
});
|
||||
showPopover(element, popover);
|
||||
});
|
||||
@@ -354,52 +364,6 @@
|
||||
console.error(`[KefinTweaks Breadcrumbs] ${message}`, ...args);
|
||||
}
|
||||
|
||||
function debugBreadcrumbLinks() {
|
||||
if (!breadcrumbContainer) return;
|
||||
|
||||
const children = Array.from(breadcrumbContainer.children);
|
||||
log('Breadcrumb structure:');
|
||||
children.forEach((child, index) => {
|
||||
if (child.style.cursor === 'pointer') {
|
||||
log(` Element ${index}: "${child.textContent}" (clickable)`);
|
||||
} else {
|
||||
log(` Element ${index}: "${child.textContent}" (static)`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function ensureSeriesNameIsClickable(seriesId) {
|
||||
if (!breadcrumbContainer) return;
|
||||
|
||||
const children = Array.from(breadcrumbContainer.children);
|
||||
|
||||
// Find the series name element (should be the 3rd element: Shows / Series Name / ...)
|
||||
if (children.length >= 3) {
|
||||
const seriesElement = children[2]; // Shows (0) / Series Name (2) / ...
|
||||
|
||||
// Check if it's already clickable
|
||||
if (seriesElement.style.cursor !== 'pointer') {
|
||||
log('Making series name clickable:', seriesElement.textContent);
|
||||
|
||||
// Make it clickable
|
||||
seriesElement.style.cursor = 'pointer';
|
||||
|
||||
// Add click handler
|
||||
seriesElement.onclick = () => {
|
||||
window.location.hash = `#/details?id=${seriesId}&serverId=${ApiClient.serverId()}`;
|
||||
};
|
||||
|
||||
log('Series name is now clickable and links to series page:', seriesId);
|
||||
} else {
|
||||
// Update existing click handler to ensure it points to the series page
|
||||
seriesElement.onclick = () => {
|
||||
window.location.hash = `#/details?id=${seriesId}&serverId=${ApiClient.serverId()}`;
|
||||
};
|
||||
log('Updated series name click handler to point to series page:', seriesId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isWideScreen() {
|
||||
return window.innerWidth > CONFIG.minWidth;
|
||||
}
|
||||
@@ -491,12 +455,7 @@
|
||||
}
|
||||
closePopover();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function closePopover() {
|
||||
if (activePopover) {
|
||||
activePopover.remove();
|
||||
@@ -643,22 +602,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Breadcrumb generation
|
||||
function createBreadcrumbElement(text, url = null, isClickable = true) {
|
||||
const element = document.createElement('span');
|
||||
element.className = 'kefinTweaks-breadcrumb-element';
|
||||
element.textContent = text;
|
||||
|
||||
if (url && isClickable) {
|
||||
element.style.cursor = 'pointer';
|
||||
element.addEventListener('click', () => {
|
||||
window.location.hash = url;
|
||||
});
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
function createSeparator() {
|
||||
const separator = document.createElement('span');
|
||||
separator.className = 'kefinTweaks-breadcrumb-separator';
|
||||
@@ -747,23 +690,26 @@
|
||||
breadcrumbContainer = createBreadcrumbContainer();
|
||||
}
|
||||
|
||||
// Clear existing content
|
||||
clearBreadcrumbs();
|
||||
|
||||
// Add all elements
|
||||
// Build all elements first
|
||||
const elements = [];
|
||||
for (let i = 0; i < targetStructure.elements.length; i++) {
|
||||
const elementDef = targetStructure.elements[i];
|
||||
const element = await createBreadcrumbElementFromStructure(elementDef, targetStructure.item);
|
||||
|
||||
breadcrumbContainer.appendChild(element);
|
||||
elements.push(element);
|
||||
|
||||
// Add separator after each element except the last
|
||||
if (i < targetStructure.elements.length - 1) {
|
||||
const separator = createSeparator();
|
||||
breadcrumbContainer.appendChild(separator);
|
||||
elements.push(separator);
|
||||
}
|
||||
}
|
||||
|
||||
// Replace the innerHTML entirely to avoid any duplicate appending issues
|
||||
breadcrumbContainer.innerHTML = '';
|
||||
|
||||
// Append all elements at once
|
||||
elements.forEach(el => breadcrumbContainer.appendChild(el));
|
||||
|
||||
log('Created breadcrumbs from scratch with', targetStructure.elements.length, 'elements');
|
||||
}
|
||||
|
||||
|
||||
+119
-298
@@ -10,10 +10,12 @@
|
||||
/**
|
||||
* Main entry point function to build a Jellyfin card
|
||||
* @param {Object} item - The Jellyfin item object
|
||||
* @param {boolean} overflowCard - Use overflow card classes instead of normal card classes
|
||||
* @param {string} cardFormat - Override card format: 'portrait', 'backdrop', or 'square'
|
||||
* @returns {HTMLElement} - The constructed card element
|
||||
*/
|
||||
buildCard: function(item) {
|
||||
return createJellyfinCardElement(item);
|
||||
buildCard: function(item, overflowCard = false, cardFormat = null) {
|
||||
return createJellyfinCardElement(item, overflowCard, cardFormat);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -21,11 +23,12 @@
|
||||
* @param {Array} items - Array of Jellyfin item objects
|
||||
* @param {string} title - Title for the scrollable container
|
||||
* @param {string|Function} viewMoreUrl - Optional URL to make title clickable, or function to call on click
|
||||
* @param {number} sectionIndex - Index for debugging
|
||||
* @param {boolean} overflowCard - Use overflow card classes instead of normal card classes
|
||||
* @param {string} cardFormat - Override card format: 'portrait', 'backdrop', or 'square'
|
||||
* @returns {HTMLElement} - The constructed scrollable container
|
||||
*/
|
||||
renderCards: function(items, title, viewMoreUrl = null, sectionIndex = 0) {
|
||||
return createScrollableContainer(items, title, viewMoreUrl, sectionIndex);
|
||||
renderCards: function(items, title, viewMoreUrl = null, overflowCard = false, cardFormat = null) {
|
||||
return createScrollableContainer(items, title, viewMoreUrl, overflowCard, cardFormat);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -33,15 +36,16 @@
|
||||
* @param {Array} itemIds - Array of Jellyfin item IDs
|
||||
* @param {string} title - Title for the scrollable container
|
||||
* @param {string} viewMoreUrl - Optional URL to make title clickable
|
||||
* @param {number} sectionIndex - Index for debugging
|
||||
* @param {boolean} overflowCard - Use overflow card classes instead of normal card classes
|
||||
* @param {string} cardFormat - Override card format: 'portrait', 'backdrop', or 'square'
|
||||
* @returns {Promise<HTMLElement>} - The constructed scrollable container
|
||||
*/
|
||||
renderCardsFromIds: async function(itemIds, title, viewMoreUrl = null, sectionIndex = 0) {
|
||||
renderCardsFromIds: async function(itemIds, title, viewMoreUrl = null, overflowCard = false, cardFormat = null) {
|
||||
const LOG = (...args) => console.log('[CardBuilder]', ...args);
|
||||
|
||||
if (!itemIds || itemIds.length === 0) {
|
||||
LOG(`No item IDs provided for section: ${title}`);
|
||||
return createScrollableContainer([], title, viewMoreUrl, sectionIndex);
|
||||
return createScrollableContainer([], title, viewMoreUrl, overflowCard, cardFormat);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -51,10 +55,10 @@
|
||||
);
|
||||
|
||||
LOG(`Fetched ${items.length} items for section: ${title}`);
|
||||
return createScrollableContainer(items, title, viewMoreUrl, sectionIndex);
|
||||
return createScrollableContainer(items, title, viewMoreUrl, overflowCard, cardFormat);
|
||||
} catch (error) {
|
||||
console.error('[CardBuilder] Error fetching items:', error);
|
||||
return createScrollableContainer([], title, viewMoreUrl, sectionIndex);
|
||||
return createScrollableContainer([], title, viewMoreUrl, overflowCard, cardFormat);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -62,27 +66,49 @@
|
||||
/**
|
||||
* Creates a Jellyfin card element from an item
|
||||
* @param {Object} item - The Jellyfin item object
|
||||
* @param {boolean} overflowCard - Use overflow card classes instead of normal card classes
|
||||
* @param {string} cardFormat - Override card format: 'portrait', 'backdrop', or 'square'
|
||||
* @returns {HTMLElement} - The constructed card element
|
||||
*/
|
||||
function createJellyfinCardElement(item) {
|
||||
function createJellyfinCardElement(item, overflowCard = false, cardFormat = null) {
|
||||
const serverId = ApiClient.serverId();
|
||||
const serverAddress = ApiClient.serverAddress();
|
||||
|
||||
// Determine card type based on item type
|
||||
// Determine card type based on cardFormat override or item type
|
||||
let cardClass, padderClass, imageParams;
|
||||
if (item.Type === 'Episode' || item.Type === 'TvChannel') {
|
||||
cardClass = 'overflowBackdropCard';
|
||||
padderClass = 'cardPadder-overflowBackdrop';
|
||||
imageParams = 'fillHeight=267&fillWidth=474';
|
||||
} else if (['MusicAlbum', 'Audio', 'Artist', 'MusicArtist'].includes(item.Type)) {
|
||||
cardClass = 'overflowSquareCard';
|
||||
padderClass = 'cardPadder-overflowSquare';
|
||||
imageParams = 'fillHeight=297&fillWidth=297';
|
||||
|
||||
if (cardFormat) {
|
||||
// Use specified cardFormat
|
||||
if (cardFormat === 'backdrop' || cardFormat === 'thumb') {
|
||||
cardClass = overflowCard ? 'overflowBackdropCard' : 'backdropCard';
|
||||
padderClass = 'cardPadder-backdrop';
|
||||
imageParams = 'fillHeight=267&fillWidth=474';
|
||||
} else if (cardFormat === 'square') {
|
||||
cardClass = overflowCard ? 'overflowSquareCard' : 'squareCard';
|
||||
padderClass = 'cardPadder-square';
|
||||
imageParams = 'fillHeight=297&fillWidth=297';
|
||||
} else {
|
||||
// portrait (default)
|
||||
cardClass = overflowCard ? 'overflowPortraitCard' : 'portraitCard';
|
||||
padderClass = 'cardPadder-portrait';
|
||||
imageParams = 'fillHeight=446&fillWidth=297';
|
||||
}
|
||||
} else {
|
||||
// Default poster style for Movies, Series, etc.
|
||||
cardClass = 'overflowPortraitCard';
|
||||
padderClass = 'cardPadder-overflowPortrait';
|
||||
imageParams = 'fillHeight=446&fillWidth=297';
|
||||
// Use item type to determine card type
|
||||
if (item.Type === 'Episode' || item.Type === 'TvChannel') {
|
||||
cardClass = overflowCard ? 'overflowBackdropCard' : 'backdropCard';
|
||||
padderClass = 'cardPadder-backdrop';
|
||||
imageParams = 'fillHeight=267&fillWidth=474';
|
||||
} else if (['MusicAlbum', 'Audio', 'Artist', 'MusicArtist'].includes(item.Type)) {
|
||||
cardClass = overflowCard ? 'overflowSquareCard' : 'squareCard';
|
||||
padderClass = 'cardPadder-square';
|
||||
imageParams = 'fillHeight=297&fillWidth=297';
|
||||
} else {
|
||||
// Default poster style for Movies, Series, etc.
|
||||
cardClass = overflowCard ? 'overflowPortraitCard' : 'portraitCard';
|
||||
padderClass = 'cardPadder-portrait';
|
||||
imageParams = 'fillHeight=446&fillWidth=297';
|
||||
}
|
||||
}
|
||||
|
||||
// Create the main card container
|
||||
@@ -140,13 +166,49 @@
|
||||
|
||||
// Card image container
|
||||
const cardImageContainer = document.createElement('a');
|
||||
cardImageContainer.href = `#/details?id=${item.Id}&serverId=${serverId}`;
|
||||
cardImageContainer.href = `${ApiClient._serverAddress}/web/#/details?id=${item.Id}&serverId=${serverId}`;
|
||||
cardImageContainer.className = 'cardImageContainer coveredImage cardContent itemAction lazy blurhashed lazy-image-fadein-fast';
|
||||
cardImageContainer.setAttribute('data-action', 'link');
|
||||
cardImageContainer.setAttribute('aria-label', item.Name || 'Unknown');
|
||||
|
||||
// Set background image or icon
|
||||
if (item.ImageTags?.Primary) {
|
||||
|
||||
// Force specific image if card format is specified
|
||||
if (cardFormat === 'backdrop') {
|
||||
let imageUrl = '';
|
||||
if (item.BackdropImageTags[0]) {
|
||||
imageUrl = `${serverAddress}/Items/${item.Id}/Images/Backdrop?${imageParams}&quality=96&tag=${item.BackdropImageTags[0]}`;
|
||||
} else if (item.ParentBackdropImageTags[0]) {
|
||||
imageUrl = `${serverAddress}/Items/${item.ParentBackdropItemId}/Images/Backdrop?${imageParams}&quality=96&tag=${item.ParentBackdropImageTags[0]}`;
|
||||
} else {
|
||||
imageUrl = `${serverAddress}/Items/${item.Id}/Images/Primary?${imageParams}&quality=96&tag=${item.ImageTags?.Primary}`;
|
||||
}
|
||||
cardImageContainer.style.backgroundImage = `url("${imageUrl}")`;
|
||||
} else if (cardFormat === 'square') {
|
||||
if (item.Type === 'Season' || item.Type === 'Series' || item.Type === 'Movie') {
|
||||
const imageUrl = item.BackdropImageTags[0] ? `${serverAddress}/Items/${item.Id}/Images/Backdrop?${imageParams}&quality=96&tag=${item.BackdropImageTags[0]}` : `${serverAddress}/Items/${item.Id}/Images/Primary?${imageParams}&quality=96&tag=${item.ImageTags?.Primary}`;
|
||||
cardImageContainer.style.backgroundImage = `url("${imageUrl}")`;
|
||||
} else {
|
||||
const imageUrl = item.PrimaryImageTags[0] ? `${serverAddress}/Items/${item.Id}/Images/Primary?${imageParams}&quality=96&tag=${item.PrimaryImageTags[0]}` : `${serverAddress}/Items/${item.Id}/Images/Thumb?${imageParams}&quality=96&tag=${item.ImageTags?.Thumb}`;
|
||||
cardImageContainer.style.backgroundImage = `url("${imageUrl}")`;
|
||||
}
|
||||
} else if (cardFormat === 'portrait') {
|
||||
if (item.Type === 'Episode' && item.SeriesPrimaryImageTag) {
|
||||
const imageUrl = `${serverAddress}/Items/${item.SeriesId}/Images/Primary?${imageParams}&quality=96&tag=${item.SeriesPrimaryImageTag}`;
|
||||
cardImageContainer.style.backgroundImage = `url("${imageUrl}")`;
|
||||
} else {
|
||||
const imageUrl = item.ImageTags?.Primary ? `${serverAddress}/Items/${item.Id}/Images/Primary?${imageParams}&quality=96&tag=${item.ImageTags?.Primary}` : `${serverAddress}/Items/${item.Id}/Images/Thumb?${imageParams}&quality=96&tag=${item.ImageTags?.Thumb}`;
|
||||
cardImageContainer.style.backgroundImage = `url("${imageUrl}")`;
|
||||
}
|
||||
} else if (cardFormat === 'thumb') {
|
||||
let imageUrl = '';
|
||||
if (item.ImageTags?.Thumb) {
|
||||
imageUrl = `${serverAddress}/Items/${item.Id}/Images/Thumb?${imageParams}&quality=96&tag=${item.ImageTags?.Thumb}`;
|
||||
} else if (item.ParentThumbImageTag) {
|
||||
imageUrl = `${serverAddress}/Items/${item.SeriesId}/Images/Thumb?${imageParams}&quality=96&tag=${item.ParentThumbImageTag}`;
|
||||
} else {
|
||||
imageUrl = `${serverAddress}/Items/${item.Id}/Images/Primary?${imageParams}&quality=96&tag=${item.ImageTags?.Primary}`;
|
||||
}
|
||||
cardImageContainer.style.backgroundImage = `url("${imageUrl}")`;
|
||||
} else if (item.ImageTags?.Primary) {
|
||||
let imageUrl;
|
||||
if (item.IsJellyseerr) {
|
||||
// Jellyseerr items use external image URLs
|
||||
@@ -208,7 +270,7 @@
|
||||
|
||||
// Overlay link
|
||||
const overlayLink = document.createElement('a');
|
||||
overlayLink.href = `#/details?id=${item.Id}&serverId=${serverId}`;
|
||||
overlayLink.href = `${ApiClient._serverAddress}/web/#/details?id=${item.Id}&serverId=${serverId}`;
|
||||
overlayLink.className = 'cardImageContainer';
|
||||
|
||||
// Play button
|
||||
@@ -275,7 +337,7 @@
|
||||
if (item.Type === 'Episode') {
|
||||
// Episodes: Series name as primary, episode title as secondary
|
||||
const seriesLink = document.createElement('a');
|
||||
seriesLink.href = `#/details?id=${item.SeriesId || item.Id}&serverId=${serverId}`;
|
||||
seriesLink.href = `${ApiClient._serverAddress}/web/#/details?id=${item.SeriesId || item.Id}&serverId=${serverId}`;
|
||||
seriesLink.className = 'itemAction textActionButton';
|
||||
seriesLink.setAttribute('data-id', item.SeriesId || item.Id);
|
||||
seriesLink.setAttribute('data-serverid', serverId);
|
||||
@@ -295,7 +357,7 @@
|
||||
const secondaryText = document.createElement('div');
|
||||
secondaryText.className = 'cardText cardTextCentered cardText-secondary';
|
||||
const episodeLink = document.createElement('a');
|
||||
episodeLink.href = `#/details?id=${item.Id}&serverId=${serverId}`;
|
||||
episodeLink.href = `${ApiClient._serverAddress}/web/#/details?id=${item.Id}&serverId=${serverId}`;
|
||||
episodeLink.className = 'itemAction textActionButton';
|
||||
episodeLink.setAttribute('data-id', item.Id);
|
||||
episodeLink.setAttribute('data-serverid', serverId);
|
||||
@@ -313,7 +375,7 @@
|
||||
} else {
|
||||
// Default: Item name as primary, year as secondary
|
||||
const titleLink = document.createElement('a');
|
||||
titleLink.href = `#/details?id=${item.Id}&serverId=${serverId}`;
|
||||
titleLink.href = `${ApiClient._serverAddress}/web/#/details?id=${item.Id}&serverId=${serverId}`;
|
||||
titleLink.className = 'itemAction textActionButton';
|
||||
titleLink.setAttribute('data-id', item.Id);
|
||||
titleLink.setAttribute('data-serverid', serverId);
|
||||
@@ -352,7 +414,7 @@
|
||||
secondaryText.className = 'cardText cardTextCentered cardText-secondary';
|
||||
const episodeLink = document.createElement('a');
|
||||
const episodeName = item.IndexNumber && item.ParentIndexNumber ? `S${item.ParentIndexNumber}:E${item.IndexNumber} - ${item.Name}` : item.Name;
|
||||
episodeLink.href = `#/details?id=${item.Id}&serverId=${serverId}`;
|
||||
episodeLink.href = `${ApiClient._serverAddress}/web/#/details?id=${item.Id}&serverId=${serverId}`;
|
||||
episodeLink.className = 'itemAction textActionButton';
|
||||
episodeLink.setAttribute('data-id', item.Id);
|
||||
episodeLink.setAttribute('data-serverid', serverId);
|
||||
@@ -387,10 +449,11 @@
|
||||
* @param {Array} items - Array of Jellyfin item objects
|
||||
* @param {string} title - Title for the scrollable container
|
||||
* @param {string} viewMoreUrl - Optional URL to make title clickable
|
||||
* @param {number} sectionIndex - Index for debugging
|
||||
* @param {boolean} overflowCard - Use overflow card classes instead of normal card classes
|
||||
* @param {string} cardFormat - Override card format: 'portrait', 'backdrop', or 'square'
|
||||
* @returns {HTMLElement} - The constructed scrollable container
|
||||
*/
|
||||
function createScrollableContainer(items, title, viewMoreUrl = null, sectionIndex = 0) {
|
||||
function createScrollableContainer(items, title, viewMoreUrl = null, overflowCard = false, cardFormat = null) {
|
||||
const LOG = (...args) => console.log('[CardBuilder]', ...args);
|
||||
|
||||
// Create the main vertical section container
|
||||
@@ -398,8 +461,8 @@
|
||||
verticalSection.className = 'verticalSection emby-scroller-container custom-scroller-container';
|
||||
|
||||
// Create section title
|
||||
const sectionTitle = document.createElement('h2');
|
||||
sectionTitle.className = 'sectionTitle sectionTitle-cards focuscontainer-x padded-left padded-right';
|
||||
const sectionTitleContainer = document.createElement('div');
|
||||
sectionTitleContainer.className = 'sectionTitleContainer sectionTitleContainer-cards padded-left';
|
||||
|
||||
if (viewMoreUrl) {
|
||||
// Create clickable title with chevron icon
|
||||
@@ -417,7 +480,8 @@
|
||||
titleLink.href = viewMoreUrl;
|
||||
}
|
||||
|
||||
const titleText = document.createElement('span');
|
||||
const titleText = document.createElement('h2');
|
||||
titleText.className = 'sectionTitle sectionTitle-cards';
|
||||
titleText.textContent = title;
|
||||
|
||||
const chevronIcon = document.createElement('span');
|
||||
@@ -426,57 +490,23 @@
|
||||
|
||||
titleLink.appendChild(titleText);
|
||||
titleLink.appendChild(chevronIcon);
|
||||
sectionTitle.appendChild(titleLink);
|
||||
sectionTitleContainer.appendChild(titleLink);
|
||||
} else {
|
||||
// Regular non-clickable title
|
||||
sectionTitle.textContent = title;
|
||||
// Regular non-clickable title
|
||||
const titleText = document.createElement('h2');
|
||||
titleText.className = 'sectionTitle sectionTitle-cards';
|
||||
titleText.textContent = title;
|
||||
sectionTitleContainer.appendChild(titleText);
|
||||
}
|
||||
|
||||
// Create "Show All" button (will be added after items are created)
|
||||
const showAllButton = document.createElement('button');
|
||||
showAllButton.type = 'button';
|
||||
showAllButton.className = 'show-all-button';
|
||||
showAllButton.style.cssText = 'margin-left: 10px; font-size: 12px; padding: 4px 8px; min-width: auto; background: transparent; border: 1px solid rgba(255, 255, 255, 0.3) !important; border-radius: 4px; cursor: pointer;';
|
||||
showAllButton.style.cssText = 'margin-left: 10px; font-size: 12px; padding: 4px 8px; min-width: auto; background: transparent; border: 1px solid rgba(255, 255, 255, 0.3) !important; border-radius: 4px; cursor: pointer; color: var(--main-text, #fff) !important; margin-bottom: .35em; align-self: center;';
|
||||
showAllButton.textContent = 'Show All';
|
||||
showAllButton.title = 'Show all items in a grid layout';
|
||||
|
||||
// Create scroll buttons container
|
||||
const scrollButtons = document.createElement('div');
|
||||
//scrollButtons.setAttribute('is', 'emby-scrollbuttons');
|
||||
scrollButtons.className = 'emby-scrollbuttons custom-scrollbuttons padded-right';
|
||||
|
||||
// Previous button
|
||||
const prevButton = document.createElement('button');
|
||||
prevButton.type = 'button';
|
||||
prevButton.setAttribute('is', 'paper-icon-button-light');
|
||||
prevButton.setAttribute('data-ripple', 'false');
|
||||
prevButton.setAttribute('data-direction', 'left');
|
||||
prevButton.title = 'Previous';
|
||||
prevButton.className = 'emby-scrollbuttons-button paper-icon-button-light';
|
||||
prevButton.disabled = true;
|
||||
|
||||
const prevIcon = document.createElement('span');
|
||||
prevIcon.className = 'material-icons chevron_left';
|
||||
prevIcon.setAttribute('aria-hidden', 'true');
|
||||
prevButton.appendChild(prevIcon);
|
||||
|
||||
// Next button
|
||||
const nextButton = document.createElement('button');
|
||||
nextButton.type = 'button';
|
||||
nextButton.setAttribute('is', 'paper-icon-button-light');
|
||||
nextButton.setAttribute('data-ripple', 'false');
|
||||
nextButton.setAttribute('data-direction', 'right');
|
||||
nextButton.title = 'Next';
|
||||
nextButton.className = 'emby-scrollbuttons-button paper-icon-button-light';
|
||||
|
||||
const nextIcon = document.createElement('span');
|
||||
nextIcon.className = 'material-icons chevron_right';
|
||||
nextIcon.setAttribute('aria-hidden', 'true');
|
||||
nextButton.appendChild(nextIcon);
|
||||
|
||||
scrollButtons.appendChild(prevButton);
|
||||
scrollButtons.appendChild(nextButton);
|
||||
|
||||
// Create scroller container
|
||||
const scroller = document.createElement('div');
|
||||
scroller.setAttribute('is', 'emby-scroller');
|
||||
@@ -491,13 +521,10 @@
|
||||
itemsContainer.setAttribute('is', 'emby-itemscontainer');
|
||||
itemsContainer.className = 'focuscontainer-x itemsContainer scrollSlider animatedScrollX';
|
||||
itemsContainer.style.whiteSpace = 'nowrap';
|
||||
itemsContainer.style.willChange = 'transform';
|
||||
itemsContainer.style.transition = 'transform 270ms ease-out';
|
||||
itemsContainer.style.transform = 'translateX(0px)';
|
||||
|
||||
// Add items to container
|
||||
items.forEach((item, index) => {
|
||||
const card = createJellyfinCardElement(item);
|
||||
const card = createJellyfinCardElement(item, overflowCard, cardFormat);
|
||||
card.setAttribute('data-index', index);
|
||||
itemsContainer.appendChild(card);
|
||||
});
|
||||
@@ -506,7 +533,8 @@
|
||||
|
||||
// Add "Show All" button to title if there are more than 20 items
|
||||
if (items.length > 20) {
|
||||
sectionTitle.appendChild(showAllButton);
|
||||
sectionTitleContainer.style.display = 'flex';
|
||||
sectionTitleContainer.appendChild(showAllButton);
|
||||
}
|
||||
|
||||
// Toggle between scroll and grid view
|
||||
@@ -514,241 +542,34 @@
|
||||
const originalItemsContainerStyle = itemsContainer.style.cssText;
|
||||
|
||||
showAllButton.addEventListener('click', () => {
|
||||
// Find the scroll buttons for this container
|
||||
const scrollerContainer = showAllButton.closest('.emby-scroller-container');
|
||||
const scrollButtons = scrollerContainer ? scrollerContainer.querySelector('.emby-scrollbuttons') : null;
|
||||
|
||||
if (isShowingAll) {
|
||||
// Switch back to scroll view
|
||||
itemsContainer.style.cssText = originalItemsContainerStyle;
|
||||
scrollButtons.style.display = '';
|
||||
if (scrollButtons) scrollButtons.style.display = '';
|
||||
showAllButton.textContent = 'Show All';
|
||||
showAllButton.title = 'Show all items in a grid layout';
|
||||
isShowingAll = false;
|
||||
} else {
|
||||
// Switch to grid view
|
||||
itemsContainer.style.cssText = 'display: flex; flex-wrap: wrap; gap: 12px; white-space: normal; transform: none !important; transition: none !important;';
|
||||
scrollButtons.style.display = 'none';
|
||||
if (scrollButtons) scrollButtons.style.display = 'none';
|
||||
showAllButton.textContent = 'Show Less';
|
||||
showAllButton.title = 'Show items in scrollable layout';
|
||||
isShowingAll = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Scroll functionality - calculate dynamically when needed
|
||||
let scrollPosition = 0;
|
||||
|
||||
// Calculate scroll values dynamically based on current DOM state
|
||||
const calculateScrollValues = () => {
|
||||
const firstCard = itemsContainer.querySelector('.card');
|
||||
if (!firstCard) {
|
||||
LOG(`[SCROLL ERROR] No cards found in scroller ${sectionIndex}`);
|
||||
return { cardWidth: 212, visibleCards: 1, scrollStep: 1, maxScroll: 0 };
|
||||
}
|
||||
|
||||
const cardRect = firstCard.getBoundingClientRect();
|
||||
const computedStyle = window.getComputedStyle(firstCard);
|
||||
const marginRight = parseFloat(computedStyle.marginRight) || 0;
|
||||
const cardWidth = cardRect.width + marginRight; // Card width + gap
|
||||
|
||||
const visibleCards = Math.floor(scroller.offsetWidth / cardWidth);
|
||||
const scrollStep = Math.max(1, visibleCards); // Scroll by the number of fully visible cards
|
||||
const maxScroll = Math.max(0, items.length - visibleCards);
|
||||
|
||||
LOG(`[SCROLL CALC] Scroller ${sectionIndex}: containerWidth=${scroller.offsetWidth}, cardWidth=${cardWidth}, visibleCards=${visibleCards}, scrollStep=${scrollStep}, totalItems=${items.length}, maxScroll=${maxScroll}`);
|
||||
|
||||
return { cardWidth, visibleCards, scrollStep, maxScroll };
|
||||
};
|
||||
|
||||
const updateScrollButtons = () => {
|
||||
const { maxScroll } = calculateScrollValues();
|
||||
prevButton.disabled = scrollPosition <= 0;
|
||||
nextButton.disabled = scrollPosition >= maxScroll;
|
||||
};
|
||||
|
||||
const scrollTo = (position, smooth = true) => {
|
||||
const { cardWidth, maxScroll } = calculateScrollValues();
|
||||
|
||||
scrollPosition = Math.max(0, Math.min(position, maxScroll));
|
||||
|
||||
if (smooth) {
|
||||
itemsContainer.style.transition = 'transform 500ms cubic-bezier(0.25, 0.46, 0.45, 0.94)';
|
||||
} else {
|
||||
itemsContainer.style.transition = 'none';
|
||||
}
|
||||
|
||||
itemsContainer.style.transform = `translateX(-${scrollPosition * cardWidth}px)`;
|
||||
updateScrollButtons();
|
||||
|
||||
if (smooth) {
|
||||
setTimeout(() => {
|
||||
itemsContainer.style.transition = 'transform 270ms ease-out';
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
// Check if buttons already have event listeners (from Jellyfin's default handlers)
|
||||
const hasExistingListeners = () => {
|
||||
// Check if the buttons have any event listeners attached
|
||||
// We'll look for the presence of Jellyfin's default scroll behavior
|
||||
const existingButtons = document.querySelectorAll('.emby-scrollbuttons-button.paper-icon-button-light');
|
||||
return existingButtons.length > 0 && existingButtons[0].onclick !== null;
|
||||
};
|
||||
|
||||
// Native horizontal scrolling with momentum and snap
|
||||
let touchStartX = 0;
|
||||
let touchStartY = 0;
|
||||
let touchCurrentX = 0;
|
||||
let isDragging = false;
|
||||
let startScrollPosition = 0;
|
||||
let currentScrollOffset = 0;
|
||||
let lastTouchTime = 0;
|
||||
let velocity = 0;
|
||||
let lastTouchX = 0;
|
||||
let animationFrame = null;
|
||||
let momentumAnimation = null;
|
||||
|
||||
const handleTouchStart = (e) => {
|
||||
if (e.touches.length !== 1) return;
|
||||
|
||||
// Cancel any ongoing momentum animation
|
||||
if (momentumAnimation) {
|
||||
cancelAnimationFrame(momentumAnimation);
|
||||
momentumAnimation = null;
|
||||
}
|
||||
|
||||
touchStartX = e.touches[0].clientX;
|
||||
touchStartY = e.touches[0].clientY;
|
||||
lastTouchX = touchStartX;
|
||||
lastTouchTime = Date.now();
|
||||
isDragging = true;
|
||||
startScrollPosition = scrollPosition;
|
||||
currentScrollOffset = 0;
|
||||
velocity = 0;
|
||||
|
||||
// Disable smooth transitions during drag
|
||||
itemsContainer.style.transition = 'none';
|
||||
|
||||
// Don't prevent default here - let touch move determine if we should handle it
|
||||
};
|
||||
|
||||
const handleTouchMove = (e) => {
|
||||
if (!isDragging || e.touches.length !== 1) return;
|
||||
|
||||
touchCurrentX = e.touches[0].clientX;
|
||||
const deltaX = touchStartX - touchCurrentX;
|
||||
const deltaY = touchStartY - e.touches[0].clientY;
|
||||
|
||||
// Only process horizontal swipes
|
||||
if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > 10) {
|
||||
e.preventDefault(); // Prevent default scrolling behavior
|
||||
|
||||
// Calculate velocity for momentum
|
||||
const currentTime = Date.now();
|
||||
const timeDelta = currentTime - lastTouchTime;
|
||||
if (timeDelta > 0) {
|
||||
velocity = (lastTouchX - touchCurrentX) / timeDelta;
|
||||
}
|
||||
lastTouchX = touchCurrentX;
|
||||
lastTouchTime = currentTime;
|
||||
|
||||
// Update scroll position in real-time
|
||||
const { cardWidth } = calculateScrollValues();
|
||||
currentScrollOffset = deltaX / cardWidth;
|
||||
const newPosition = startScrollPosition + currentScrollOffset;
|
||||
|
||||
// Apply the transform immediately
|
||||
itemsContainer.style.transform = `translateX(-${newPosition * cardWidth}px)`;
|
||||
}
|
||||
};
|
||||
|
||||
const handleTouchEnd = (e) => {
|
||||
if (!isDragging) return;
|
||||
|
||||
isDragging = false;
|
||||
|
||||
// Calculate final position with momentum
|
||||
const { cardWidth, maxScroll } = calculateScrollValues();
|
||||
let finalPosition = startScrollPosition + currentScrollOffset;
|
||||
|
||||
// Apply momentum if velocity is significant
|
||||
if (Math.abs(velocity) > 0.1) {
|
||||
const momentumDistance = velocity * 200; // Adjust multiplier for momentum strength
|
||||
finalPosition += momentumDistance / cardWidth;
|
||||
}
|
||||
|
||||
// Clamp to bounds
|
||||
finalPosition = Math.max(0, Math.min(finalPosition, maxScroll));
|
||||
|
||||
// Snap to nearest card
|
||||
const snapPosition = Math.round(finalPosition);
|
||||
finalPosition = Math.max(0, Math.min(snapPosition, maxScroll));
|
||||
|
||||
// Update scroll position and animate to final position
|
||||
scrollPosition = finalPosition;
|
||||
|
||||
// Re-enable smooth transitions
|
||||
itemsContainer.style.transition = 'transform 300ms cubic-bezier(0.25, 0.46, 0.45, 0.94)';
|
||||
itemsContainer.style.transform = `translateX(-${scrollPosition * cardWidth}px)`;
|
||||
|
||||
// Update button states
|
||||
updateScrollButtons();
|
||||
|
||||
LOG(`[TOUCH END] Scroller ${sectionIndex}: velocity=${velocity.toFixed(3)}, finalPosition=${finalPosition}, snapPosition=${snapPosition}`);
|
||||
|
||||
// Reset velocity
|
||||
velocity = 0;
|
||||
currentScrollOffset = 0;
|
||||
};
|
||||
|
||||
// Add touch event listeners to the scroller
|
||||
scroller.addEventListener('touchstart', handleTouchStart, { passive: false });
|
||||
scroller.addEventListener('touchmove', handleTouchMove, { passive: false });
|
||||
scroller.addEventListener('touchend', handleTouchEnd, { passive: false });
|
||||
scroller.addEventListener('touchcancel', handleTouchEnd, { passive: false });
|
||||
|
||||
// Only attach our event listeners if there are no existing ones
|
||||
if (true) {
|
||||
prevButton.addEventListener('click', () => {
|
||||
const { scrollStep, maxScroll } = calculateScrollValues();
|
||||
const newPosition = Math.max(0, scrollPosition - scrollStep);
|
||||
LOG(`[PREV CLICK] Scroller ${sectionIndex}: currentPosition=${scrollPosition}, scrollStep=${scrollStep}, newPosition=${newPosition}`);
|
||||
scrollTo(newPosition, true);
|
||||
});
|
||||
|
||||
nextButton.addEventListener('click', () => {
|
||||
const { scrollStep, maxScroll } = calculateScrollValues();
|
||||
const newPosition = Math.min(maxScroll, scrollPosition + scrollStep);
|
||||
LOG(`[NEXT CLICK] Scroller ${sectionIndex}: currentPosition=${scrollPosition}, scrollStep=${scrollStep}, newPosition=${newPosition}`);
|
||||
scrollTo(newPosition, true);
|
||||
});
|
||||
}
|
||||
|
||||
// Initial state
|
||||
updateScrollButtons();
|
||||
|
||||
// Assemble the section
|
||||
verticalSection.appendChild(sectionTitle);
|
||||
verticalSection.appendChild(scrollButtons);
|
||||
verticalSection.appendChild(sectionTitleContainer);
|
||||
verticalSection.appendChild(scroller);
|
||||
|
||||
return verticalSection;
|
||||
}
|
||||
|
||||
// Add CSS to hide emby-scrollbuttons that aren't custom-scrollbuttons and improve touch experience
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
/* Native horizontal scrolling experience - ONLY for our custom scrollers */
|
||||
|
||||
|
||||
/* Ensure smooth scrolling on mobile */
|
||||
@media (max-width: 768px) {
|
||||
.emby-scroller.custom-scroller {
|
||||
scroll-behavior: smooth !important;
|
||||
}
|
||||
.custom-scroller-container .emby-scrollbuttons {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// Expose the cardBuilder to the global window object
|
||||
window.cardBuilder = cardBuilder;
|
||||
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
console.log("[DashboardButtonFix]: No history to go back. Redirecting to home page.");
|
||||
|
||||
// Changes this to the page you'd like to return to by default
|
||||
Dashboard.navigate('/home.html');
|
||||
const homeUrl = ApiClient.serverVersion().split('.')[1] > 10 ? 'home' : 'home.html';
|
||||
Dashboard.navigate(`/${homeUrl}`);
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
|
||||
+26
-78
@@ -10,9 +10,9 @@
|
||||
const WARN = (...args) => console.warn('[HeaderTabs]', ...args);
|
||||
const ERR = (...args) => console.error('[HeaderTabs]', ...args);
|
||||
|
||||
LOG('Script loaded - JS Injector mode');
|
||||
LOG('Initializing...');
|
||||
|
||||
const SUPPORTED_PAGES = ['home.html', 'tv.html', 'movies.html', 'music.html', 'livetv.html'];
|
||||
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() {
|
||||
@@ -87,10 +87,12 @@
|
||||
newHash = `${currentHash}`;
|
||||
}
|
||||
}
|
||||
|
||||
const newUrl = window.location.origin + window.location.pathname + '#' + newHash.substring(1);
|
||||
|
||||
// Manually notify handlers for Header Tab navigation because it doesn't trigger normal Jellyfin navigation
|
||||
window.KefinTweaksUtils.notifyHandlers(currentPage, document);
|
||||
|
||||
// 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);
|
||||
}
|
||||
@@ -139,7 +141,7 @@
|
||||
// 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.html' || currentTabFromUrl !== 0) {
|
||||
if (currentPage !== 'home' && currentPage !== 'home.html' || currentTabFromUrl !== 0) {
|
||||
const mediaBarSlideshow = document.getElementById('slides-container');
|
||||
if (mediaBarSlideshow) {
|
||||
mediaBarSlideshow.style.display = 'none';
|
||||
@@ -149,6 +151,23 @@
|
||||
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) {
|
||||
activeTabContent.classList.remove('is-active');
|
||||
}
|
||||
|
||||
// Add is-active class to the current tab content if it's not already active
|
||||
if (!activeTabContentIndex || activeTabContentIndex !== currentTabFromUrl) {
|
||||
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;
|
||||
}
|
||||
@@ -168,23 +187,6 @@
|
||||
correctButton.classList.add('emby-tab-button-active');
|
||||
LOG('Set active tab button to index:', currentTabFromUrl);
|
||||
}
|
||||
|
||||
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) {
|
||||
activeTabContent.classList.remove('is-active');
|
||||
}
|
||||
|
||||
// Add is-active class to the current tab content if it's not already active
|
||||
if (!activeTabContentIndex || activeTabContentIndex !== currentTabFromUrl) {
|
||||
const targetContent = document.querySelector(`.libraryPage:not(.hide) .pageTabContent[data-index="${currentTabFromUrl}"]`);
|
||||
if (targetContent) {
|
||||
targetContent.classList.add('is-active');
|
||||
LOG('Set active tab content:', currentTabFromUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add click listeners to tab buttons
|
||||
@@ -203,6 +205,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 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');
|
||||
@@ -331,63 +334,8 @@
|
||||
|
||||
// Check on hash changes
|
||||
window.addEventListener('hashchange', checkPageChange);
|
||||
|
||||
// Fallback: check URL periodically but less frequently
|
||||
/* urlCheckInterval = setInterval(() => {
|
||||
if (window.location.href !== lastUrl) {
|
||||
checkPageChange();
|
||||
}
|
||||
}, 500); */
|
||||
|
||||
// Check periodically in case we miss something
|
||||
//setInterval(checkPageChange, 1000);
|
||||
}
|
||||
|
||||
// Setup home navigation click handler with retry logic
|
||||
/* function setupHomeNavClickHandler() {
|
||||
const homeNavOption = document.querySelector('.mainDrawer .navMenuOption[href="#/home.html"]');
|
||||
if (homeNavOption) {
|
||||
homeNavOption.addEventListener('click', function(event) {
|
||||
const headerTabs = document.querySelector('.headerTabs');
|
||||
if (headerTabs) {
|
||||
const firstTab = headerTabs.querySelector('.emby-tab-button[data-index="0"]');
|
||||
if (firstTab) {
|
||||
LOG('Triggering click on first homepage tab');
|
||||
firstTab.click();
|
||||
} else {
|
||||
WARN('First homepage tab not found');
|
||||
}
|
||||
} else {
|
||||
WARN('Header tabs not found');
|
||||
}
|
||||
});
|
||||
LOG('Added click listener to home navigation option');
|
||||
return true; // Success
|
||||
} else {
|
||||
WARN('Home navigation option not found, will retry...');
|
||||
return false; // Failed, needs retry
|
||||
}
|
||||
}
|
||||
|
||||
// Retry setup until successful
|
||||
function retryHomeNavSetup() {
|
||||
const maxRetries = 50; // Try for up to 25 seconds (50 * 500ms)
|
||||
let retryCount = 0;
|
||||
|
||||
const retryInterval = setInterval(() => {
|
||||
if (setupHomeNavClickHandler()) {
|
||||
clearInterval(retryInterval);
|
||||
LOG('Home navigation click handler successfully attached');
|
||||
} else {
|
||||
retryCount++;
|
||||
if (retryCount >= maxRetries) {
|
||||
clearInterval(retryInterval);
|
||||
ERR('Failed to attach home navigation click handler after maximum retries');
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
} */
|
||||
|
||||
// Register onViewPage handler to sync active tab state
|
||||
if (window.KefinTweaksUtils && window.KefinTweaksUtils.onViewPage) {
|
||||
window.KefinTweaksUtils.onViewPage((view, element) => {
|
||||
@@ -405,7 +353,7 @@
|
||||
|
||||
// Initialize
|
||||
setupPageMonitor();
|
||||
//retryHomeNavSetup();
|
||||
setupHeaderTabsObserver();
|
||||
|
||||
LOG('Header tabs functionality initialized');
|
||||
|
||||
|
||||
@@ -6,12 +6,20 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sections.homeSectionsContainer>div {
|
||||
order: 100;
|
||||
}
|
||||
|
||||
.sections.homeSectionsContainer>div:has(a.homeLibraryButton),
|
||||
.sections.homeSectionsContainer>div:has([data-type="CollectionFolder"]) {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.sections.homeSectionsContainer>div:has(div[data-positionticks]) {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.sections.homeSectionsContainer>div:has(a[href*="type=nextup"]) {
|
||||
order: 3;
|
||||
}
|
||||
@@ -27,4 +35,13 @@
|
||||
background-image: url('/files/KefinTweaks/pages/images/halloween.jpg');
|
||||
background-position: 0px -250px;
|
||||
background-size: auto 102%;
|
||||
}
|
||||
|
||||
.home-screen h2.sectionTitle {
|
||||
margin-bottom: .35em;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.sectionTitleContainer.sectionTitleContainer-cards.padded-left > .sectionTitle.sectionTitle-cards {
|
||||
display: flex !important;
|
||||
}
|
||||
+2783
-3103
File diff suppressed because it is too large.
Load diff
+25
-1
@@ -7,8 +7,10 @@
|
||||
const WARN = (...args) => console.warn('[InfiniteScroll]', ...args);
|
||||
const ERR = (...args) => console.error('[InfiniteScroll]', ...args);
|
||||
|
||||
LOG('Initializing...');
|
||||
|
||||
// Supported pages (same as headerTabs)
|
||||
const SUPPORTED_PAGES = ['tv.html', 'movies.html', 'music.html'];
|
||||
const SUPPORTED_PAGES = ['tv.html', 'movies.html', 'music.html', 'tv', 'movies', 'music'];
|
||||
|
||||
// Media type configurations - Read from centralized config or use defaults
|
||||
const MEDIA_CONFIGS = {
|
||||
@@ -22,6 +24,26 @@
|
||||
context: 'tvshows',
|
||||
sortBy: 'SortName,ProductionYear'
|
||||
},
|
||||
'music.html': {
|
||||
includeItemTypes: 'Music',
|
||||
context: 'music',
|
||||
sortBy: 'SortName,ProductionYear'
|
||||
},
|
||||
'movies': {
|
||||
includeItemTypes: 'Movie',
|
||||
context: 'movies',
|
||||
sortBy: 'SortName,ProductionYear'
|
||||
},
|
||||
'tv': {
|
||||
includeItemTypes: 'Series',
|
||||
context: 'tvshows',
|
||||
sortBy: 'SortName,ProductionYear'
|
||||
},
|
||||
'music': {
|
||||
includeItemTypes: 'Music',
|
||||
context: 'music',
|
||||
sortBy: 'SortName,ProductionYear'
|
||||
}
|
||||
};
|
||||
|
||||
let userId = null;
|
||||
@@ -813,4 +835,6 @@
|
||||
|
||||
// Cleanup on page unload
|
||||
window.addEventListener('beforeunload', cleanup);
|
||||
|
||||
LOG('Initialized successfully');
|
||||
})();
|
||||
+107
-36
@@ -4,9 +4,12 @@
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
console.log('[KefinTweaks Injector] Initializing...');
|
||||
|
||||
// Configuration: Read from centralized config or use defaults
|
||||
const ENABLED_SCRIPTS = window.KefinTweaksConfig?.scripts || {
|
||||
// Configuration: Start with defaults, then merge user config
|
||||
// This allows new scripts to work out of the box without requiring config updates
|
||||
const ENABLED_SCRIPTS = {
|
||||
// UI and functional enhancements
|
||||
watchlist: true, // Watchlist functionality
|
||||
homeScreen: true, // Custom home screen sections
|
||||
@@ -20,10 +23,14 @@
|
||||
infiniteScroll: true, // Infinite scroll functionality
|
||||
removeContinue: true, // Remove from continue watching functionality
|
||||
subtitleSearch: true, // Subtitle search functionality
|
||||
playlist: true // Playlist view page modifications
|
||||
playlist: true, // Playlist view page modifications
|
||||
skinManager: true, // Skin selection and management
|
||||
|
||||
// Note: Core functionality scripts (utils, cardBuilder, localStorageCache, modal)
|
||||
// are automatically enabled when needed by other scripts
|
||||
|
||||
// Merge user config on top of defaults (allows users to override defaults)
|
||||
...window.KefinTweaksConfig?.scripts
|
||||
};
|
||||
|
||||
// Script definitions with dependencies and metadata
|
||||
@@ -35,6 +42,20 @@
|
||||
dependencies: [],
|
||||
description: 'Common utilities for page view management and MutationObserver conversion'
|
||||
},
|
||||
{
|
||||
name: 'userConfig',
|
||||
script: 'userConfig.js',
|
||||
css: null,
|
||||
dependencies: [],
|
||||
description: 'User-specific configuration for skins and other settings'
|
||||
},
|
||||
{
|
||||
name: 'skinManager',
|
||||
script: 'skinManager.js',
|
||||
css: null,
|
||||
dependencies: ['utils', 'userConfig'],
|
||||
description: 'Adds skin selection dropdown to display preferences page and manages skin CSS loading'
|
||||
},
|
||||
{
|
||||
name: 'cardBuilder',
|
||||
script: 'cardBuilder.js',
|
||||
@@ -153,7 +174,7 @@
|
||||
css: null,
|
||||
dependencies: ['cardBuilder', 'utils'],
|
||||
description: 'Modifies playlist view page behavior to navigate to item details instead of playing, and adds play button to playlist items'
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
// Auto-enable dependencies for enabled scripts
|
||||
@@ -243,7 +264,7 @@
|
||||
function loadScript(filename) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Check if script is already loaded
|
||||
const existingScript = document.querySelector(`script[src*="${filename}"]`);
|
||||
const existingScript = document.querySelector(`script[src="${getScriptRoot()}${filename}"]`);
|
||||
if (existingScript) {
|
||||
console.log(`[KefinTweaks Injector] Script already loaded: ${filename}`);
|
||||
resolve();
|
||||
@@ -268,33 +289,55 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Load a script and its dependencies
|
||||
async function loadScriptWithDependencies(scriptDef) {
|
||||
try {
|
||||
// Load dependencies first - only if they are enabled
|
||||
for (const depName of scriptDef.dependencies) {
|
||||
const depScript = SCRIPT_DEFINITIONS.find(s => s.name === depName);
|
||||
if (depScript && ENABLED_SCRIPTS[depName]) {
|
||||
await loadScriptWithDependencies(depScript);
|
||||
} else if (depScript && !ENABLED_SCRIPTS[depName]) {
|
||||
console.warn(`[KefinTweaks Injector] Dependency '${depName}' is disabled, skipping for '${scriptDef.name}'`);
|
||||
}
|
||||
}
|
||||
|
||||
// Load CSS if specified
|
||||
if (scriptDef.css) {
|
||||
await loadCSS(scriptDef.css);
|
||||
}
|
||||
|
||||
// Load the script
|
||||
await loadScript(scriptDef.script);
|
||||
|
||||
console.log(`[KefinTweaks Injector] Successfully loaded: ${scriptDef.name}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[KefinTweaks Injector] Error loading ${scriptDef.name}:`, error);
|
||||
throw error;
|
||||
// Recursively collect all dependencies for a given script
|
||||
function collectAllDependencies(scriptDef, collected = new Set(), visited = new Set()) {
|
||||
// Avoid infinite recursion
|
||||
if (visited.has(scriptDef.name)) {
|
||||
return collected;
|
||||
}
|
||||
visited.add(scriptDef.name);
|
||||
|
||||
// For each dependency
|
||||
for (const depName of scriptDef.dependencies) {
|
||||
// Only process enabled dependencies
|
||||
if (!ENABLED_SCRIPTS[depName]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const depScript = SCRIPT_DEFINITIONS.find(s => s.name === depName);
|
||||
if (!depScript) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add to collected if we haven't already
|
||||
if (!collected.has(depScript.name)) {
|
||||
collected.add(depScript.name);
|
||||
// Recursively collect this dependency's dependencies
|
||||
collectAllDependencies(depScript, collected, visited);
|
||||
}
|
||||
}
|
||||
|
||||
return collected;
|
||||
}
|
||||
|
||||
// Load a single script (assumes dependencies are already loaded)
|
||||
async function loadScriptSync(scriptDef) {
|
||||
// Check if already loaded
|
||||
const isAlreadyLoaded = document.querySelector(`script[src*="${scriptDef.script}"]`);
|
||||
if (isAlreadyLoaded) {
|
||||
console.log(`[KefinTweaks Injector] Script already loaded: ${scriptDef.name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Load CSS if specified
|
||||
if (scriptDef.css) {
|
||||
await loadCSS(scriptDef.css);
|
||||
}
|
||||
|
||||
// Load the script
|
||||
await loadScript(scriptDef.script);
|
||||
|
||||
console.log(`[KefinTweaks Injector] Successfully loaded: ${scriptDef.name}`);
|
||||
}
|
||||
|
||||
// Main initialization function
|
||||
@@ -307,17 +350,45 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Get enabled scripts in dependency order
|
||||
// Get enabled scripts
|
||||
const enabledScripts = SCRIPT_DEFINITIONS.filter(script => ENABLED_SCRIPTS[script.name]);
|
||||
|
||||
console.log(`[KefinTweaks Injector] Loading ${enabledScripts.length} scripts:`,
|
||||
enabledScripts.map(s => s.name));
|
||||
// Step 1: Collect all dependencies from all enabled scripts
|
||||
const allDependencyNames = new Set();
|
||||
for (const script of enabledScripts) {
|
||||
const deps = collectAllDependencies(script);
|
||||
deps.forEach(dep => allDependencyNames.add(dep));
|
||||
}
|
||||
|
||||
// Load all enabled scripts
|
||||
const loadPromises = enabledScripts.map(script => loadScriptWithDependencies(script));
|
||||
// Step 2: Separate dependencies from non-dependencies
|
||||
const dependencyScripts = SCRIPT_DEFINITIONS.filter(script =>
|
||||
allDependencyNames.has(script.name) && ENABLED_SCRIPTS[script.name]
|
||||
);
|
||||
|
||||
const nonDependencyScripts = enabledScripts.filter(script =>
|
||||
!allDependencyNames.has(script.name)
|
||||
);
|
||||
|
||||
console.log(`[KefinTweaks Injector] Found ${dependencyScripts.length} dependency scripts:`,
|
||||
dependencyScripts.map(s => s.name));
|
||||
console.log(`[KefinTweaks Injector] Found ${nonDependencyScripts.length} non-dependency scripts:`,
|
||||
nonDependencyScripts.map(s => s.name));
|
||||
|
||||
try {
|
||||
// Step 3: Load all dependencies first
|
||||
console.log('[KefinTweaks Injector] Loading dependencies first...');
|
||||
console.log(`[KefinTweaks Injector] Dependency load order:`, dependencyScripts.map(s => s.name));
|
||||
|
||||
let dependencyLoadPromises = [];
|
||||
dependencyLoadPromises.push(...dependencyScripts.map(script => loadScriptSync(script)));
|
||||
await Promise.all(dependencyLoadPromises);
|
||||
|
||||
// Step 4: Load non-dependencies in parallel (their dependencies are already loaded)
|
||||
let loadPromises = [];
|
||||
console.log('[KefinTweaks Injector] Loading non-dependencies...');
|
||||
loadPromises.push(...nonDependencyScripts.map(script => loadScriptSync(script)));
|
||||
await Promise.all(loadPromises);
|
||||
|
||||
console.log('[KefinTweaks Injector] All scripts loaded successfully!');
|
||||
|
||||
// Dispatch custom event when all scripts are loaded
|
||||
|
||||
+71
-3
@@ -68,19 +68,22 @@
|
||||
breadcrumbs: true, // Breadcrumb navigation for Movies, Series, Seasons, Episodes, Music
|
||||
|
||||
// Branding and external integrations
|
||||
updoot: false, // Integration with Jellyfin Updoot upvote functionality
|
||||
updoot: false, // EXPERIMENTAL: Integration with Jellyfin Updoot upvote functionality - Needs more work
|
||||
// REQUIRES: ExclusiveElsewhere requires JellyfinEnhanced plugin https://github.com/n00bcodr/Jellyfin-Enhanced
|
||||
exclusiveElsewhere: false, // Custom branding when items aren't available on streaming services
|
||||
|
||||
// Performance and stability improvements
|
||||
backdropLeakFix: true, // Fixes memory leaks from backdrop images when tab isn't focused
|
||||
dashboardButtonFix: true, // Proper back button behavior on dashboard
|
||||
infiniteScroll: true, // Smooth infinite scrolling for large media libraries
|
||||
infiniteScroll: true, // Smooth infinite scrolling. Supports Movie and TV libraries. Music to be added in the future.
|
||||
|
||||
// Playback and media enhancements
|
||||
removeContinue: true, // Add "Remove from Continue Watching" functionality
|
||||
subtitleSearch: true, // Search and download subtitles directly from video OSD
|
||||
playlist: true // Enhanced playlist view with better navigation
|
||||
playlist: true, // Enhanced playlist view with better navigation
|
||||
|
||||
// UI customization
|
||||
skinManager: true // Skin selection and management for display preferences
|
||||
},
|
||||
|
||||
// ============================================================================
|
||||
@@ -158,6 +161,71 @@
|
||||
enableJellyseerr: false // Enable Jellyseerr integration for request functionality
|
||||
},
|
||||
|
||||
// ============================================================================
|
||||
// SKIN CONFIGURATION
|
||||
// ============================================================================
|
||||
// Configure additional skins available in the skin manager
|
||||
// These will be merged with skins from userConfig.js
|
||||
// Each skin can have a single URL (string) or multiple URLs (array) for complex themes
|
||||
// Each skin can also have colorSchemes array for color variations specific to that skin
|
||||
// Each skin should have an author field indicating who created the skin
|
||||
skins: [
|
||||
// Add additional skins here that will be available to all users
|
||||
// These are example skins - replace with your own or remove if not needed
|
||||
/* Example of adding more skins:
|
||||
{
|
||||
name: 'Custom Theme',
|
||||
author: 'username',
|
||||
url: 'https://cdn.jsdelivr.net/gh/username/custom-theme.css',
|
||||
colorSchemes: []
|
||||
},
|
||||
{
|
||||
name: 'Multi-file Theme',
|
||||
author: 'username',
|
||||
url: [
|
||||
'https://cdn.jsdelivr.net/gh/username/theme-base.css',
|
||||
'https://cdn.jsdelivr.net/gh/username/theme-extensions.css'
|
||||
],
|
||||
colorSchemes: [
|
||||
{
|
||||
name: 'Dark Blue',
|
||||
url: 'https://cdn.jsdelivr.net/gh/username/theme-dark-blue.css'
|
||||
},
|
||||
{
|
||||
name: 'Light Green',
|
||||
url: 'https://cdn.jsdelivr.net/gh/username/theme-light-green.css'
|
||||
}
|
||||
]
|
||||
},
|
||||
*/
|
||||
],
|
||||
|
||||
// Default skin to use when no skin is selected
|
||||
// This overrides the default selection from userConfig.js
|
||||
defaultSkin: null, // Set to skin name (e.g., 'Dark Theme') or null to use userConfig default
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// THEME CONFIGURATION
|
||||
// ============================================================================
|
||||
// Configure additional themes available in the theme manager
|
||||
// These will be merged with themes from userConfig.js
|
||||
// Each theme should have a name and URL pointing to a CSS file
|
||||
themes: [
|
||||
// Add additional themes here that will be available to all users
|
||||
// These are example themes - replace with your own or remove if not needed
|
||||
/* Example of adding more themes:
|
||||
{
|
||||
name: 'Custom Dark Theme',
|
||||
url: 'https://cdn.jsdelivr.net/gh/username/custom-dark-theme.css'
|
||||
},
|
||||
{
|
||||
name: 'Custom Light Theme',
|
||||
url: 'https://cdn.jsdelivr.net/gh/username/custom-light-theme.css'
|
||||
},
|
||||
*/
|
||||
],
|
||||
|
||||
// ============================================================================
|
||||
// CUSTOM MENU LINKS CONFIGURATION
|
||||
// ============================================================================
|
||||
|
||||
+96
-52
@@ -11,7 +11,7 @@
|
||||
const WARN = (...args) => console.warn('[Playlist]', ...args);
|
||||
const ERR = (...args) => console.error('[Playlist]', ...args);
|
||||
|
||||
LOG('Script loaded - JS Injector mode');
|
||||
LOG('Initializing...');
|
||||
|
||||
let currentPlaylistId = null;
|
||||
|
||||
@@ -25,9 +25,13 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
const childrenItemsContainer = libraryPage.querySelector('.childrenItemsContainer');
|
||||
let childrenItemsContainer = libraryPage.querySelector('.childrenItemsContainer');
|
||||
if (!childrenItemsContainer) {
|
||||
return false;
|
||||
childrenItemsContainer = libraryPage.querySelector('#listChildrenCollapsible');
|
||||
if (!childrenItemsContainer) {
|
||||
WARN('Children items container not found');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const playlistItems = childrenItemsContainer.querySelectorAll('.listItem[data-playlistitemid]');
|
||||
@@ -90,16 +94,24 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const childrenItemsContainer = libraryPage.querySelector('.childrenItemsContainer');
|
||||
let childrenItemsContainer = libraryPage.querySelector('.childrenItemsContainer');
|
||||
if (!childrenItemsContainer) {
|
||||
WARN('Children items container not found');
|
||||
return;
|
||||
childrenItemsContainer = libraryPage.querySelector('#listChildrenCollapsible');
|
||||
if (!childrenItemsContainer) {
|
||||
WARN('Children items container not found');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const playlistItems = childrenItemsContainer.querySelectorAll('.listItem[data-playlistitemid]');
|
||||
LOG(`Found ${playlistItems.length} playlist items`);
|
||||
|
||||
playlistItems.forEach((item, index) => {
|
||||
if (item.dataset.customPlaylistButton === 'true') {
|
||||
LOG(`Playlist item ${index} already has a custom button, skipping`);
|
||||
return;
|
||||
}
|
||||
|
||||
const playlistItemId = item.getAttribute('data-playlistitemid');
|
||||
const serverId = ApiClient.serverId();
|
||||
|
||||
@@ -114,31 +126,38 @@
|
||||
|
||||
// Add new click handler to navigate to details page
|
||||
item.addEventListener('click', (e) => {
|
||||
// Check if the click target is within the listViewUserDataButtons container
|
||||
const buttonsContainer = item.querySelector('.listViewUserDataButtons');
|
||||
if (buttonsContainer && buttonsContainer.contains(e.target)) {
|
||||
LOG(`Click on button detected, not navigating for item: ${playlistItemId}`);
|
||||
return; // Don't navigate when clicking on buttons
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const detailsUrl = `#/details?id=${playlistItemId}&serverId=${serverId}`;
|
||||
LOG(`Navigating to: ${detailsUrl}`);
|
||||
|
||||
// Use Jellyfin's navigation method
|
||||
if (window.Emby && window.Emby.Page) {
|
||||
window.Emby.Page.show(detailsUrl);
|
||||
} else {
|
||||
// Fallback to direct navigation
|
||||
window.location.hash = detailsUrl;
|
||||
}
|
||||
handlePlaylistItemClick(e, item, playlistItemId, serverId);
|
||||
});
|
||||
|
||||
item.dataset.customPlaylistButton = 'true';
|
||||
|
||||
LOG(`Modified click handler for playlist item: ${playlistItemId}`);
|
||||
});
|
||||
}
|
||||
|
||||
function handlePlaylistItemClick(e, item, playlistItemId, serverId) {
|
||||
// Check if the click target is within the listViewUserDataButtons container
|
||||
const buttonsContainer = item.querySelector('.listViewUserDataButtons');
|
||||
if (buttonsContainer && buttonsContainer.contains(e.target)) {
|
||||
LOG(`Click on button detected, not navigating for item: ${playlistItemId}`);
|
||||
return; // Don't navigate when clicking on buttons
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const detailsUrl = `/details?id=${playlistItemId}&serverId=${serverId}`;
|
||||
LOG(`Navigating to: ${detailsUrl}`);
|
||||
|
||||
// Use Jellyfin's navigation method
|
||||
if (Dashboard) {
|
||||
Dashboard.navigate(detailsUrl);
|
||||
} else {
|
||||
// Fallback to direct navigation
|
||||
const fullDetailsUrl = `${ApiClient.serverAddress()}/web/#${detailsUrl}`;
|
||||
window.location.href = fullDetailsUrl;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds play button to each playlist item's listViewUserDataButtons container
|
||||
@@ -150,10 +169,13 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const childrenItemsContainer = libraryPage.querySelector('.childrenItemsContainer');
|
||||
let childrenItemsContainer = libraryPage.querySelector('.childrenItemsContainer');
|
||||
if (!childrenItemsContainer) {
|
||||
WARN('Children items container not found');
|
||||
return;
|
||||
childrenItemsContainer = libraryPage.querySelector('#listChildrenCollapsible');
|
||||
if (!childrenItemsContainer) {
|
||||
WARN('Children items container not found for play buttons');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const playlistItems = childrenItemsContainer.querySelectorAll('.listItem[data-playlistitemid]');
|
||||
@@ -161,7 +183,6 @@
|
||||
|
||||
playlistItems.forEach((item, index) => {
|
||||
const playlistItemId = item.getAttribute('data-playlistitemid');
|
||||
const serverId = ApiClient.serverId();
|
||||
|
||||
if (!playlistItemId) {
|
||||
WARN(`Playlist item ${index} has no data-playlistitemid`);
|
||||
@@ -215,12 +236,54 @@
|
||||
LOG(`Modifying playlist page for item: ${itemId}`);
|
||||
currentPlaylistId = itemId;
|
||||
|
||||
// Wait a bit for the page to fully load
|
||||
setTimeout(() => {
|
||||
// Poll for page elements to be ready (every 100ms for up to 10 seconds)
|
||||
const pollInterval = 100;
|
||||
const maxAttempts = 100; // 100ms * 100 = 10 seconds
|
||||
let attempts = 0;
|
||||
|
||||
const pollForElements = () => {
|
||||
attempts++;
|
||||
|
||||
const libraryPage = document.querySelector('.libraryPage:not(.hide)');
|
||||
if (!libraryPage) {
|
||||
if (attempts < maxAttempts) {
|
||||
setTimeout(pollForElements, pollInterval);
|
||||
} else {
|
||||
WARN('Library page not found after 10 seconds');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let childrenItemsContainer = libraryPage.querySelector('.childrenItemsContainer');
|
||||
if (!childrenItemsContainer) {
|
||||
childrenItemsContainer = libraryPage.querySelector('#listChildrenCollapsible');
|
||||
if (!childrenItemsContainer) {
|
||||
if (attempts < maxAttempts) {
|
||||
setTimeout(pollForElements, pollInterval);
|
||||
} else {
|
||||
WARN('Children items container not found after 10 seconds');
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const playlistItems = childrenItemsContainer.querySelectorAll('.listItem[data-playlistitemid]');
|
||||
if (playlistItems.length === 0) {
|
||||
if (attempts < maxAttempts) {
|
||||
setTimeout(pollForElements, pollInterval);
|
||||
} else {
|
||||
WARN('No playlist items found after 10 seconds');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Elements found, proceed with modifications
|
||||
modifyPlaylistItemClicks();
|
||||
addPlayButtons();
|
||||
LOG('Playlist page modifications complete');
|
||||
}, 500);
|
||||
};
|
||||
|
||||
pollForElements();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -286,24 +349,5 @@
|
||||
// Initialize the hook when the script loads
|
||||
initializePlaylistHook();
|
||||
|
||||
// Also check current page on script load in case we're already on a playlist page
|
||||
const currentUrl = window.location.hash;
|
||||
if (currentUrl.includes('#/details')) {
|
||||
const urlParams = new URLSearchParams(currentUrl.split('?')[1]);
|
||||
const itemId = urlParams.get('id');
|
||||
|
||||
if (itemId) {
|
||||
LOG(`Script loaded on details page with item ID: ${itemId}`);
|
||||
isPlaylistPage(itemId).then(isPlaylist => {
|
||||
if (isPlaylist) {
|
||||
LOG(`Already on playlist page: ${itemId}`);
|
||||
modifyPlaylistPage(itemId);
|
||||
}
|
||||
}).catch(error => {
|
||||
ERR('Error checking current playlist page:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[Playlist] Module loaded and ready');
|
||||
})();
|
||||
+3
-3
@@ -12,8 +12,8 @@
|
||||
const LOG = (...args) => console.log('[RemoveContinue]', ...args);
|
||||
const WARN = (...args) => console.warn('[RemoveContinue]', ...args);
|
||||
const ERROR = (...args) => console.error('[RemoveContinue]', ...args);
|
||||
|
||||
LOG('Remove continue watching functionality initialized');
|
||||
|
||||
LOG('Initializing...');
|
||||
|
||||
// Add CSS for positioning the remove button in top right corner
|
||||
const style = document.createElement('style');
|
||||
@@ -291,5 +291,5 @@
|
||||
processExistingOverlayContainers();
|
||||
};
|
||||
|
||||
LOG('Remove continue watching functionality loaded successfully');
|
||||
LOG('Initialized successfully');
|
||||
})();
|
||||
+12
@@ -47,4 +47,16 @@ div#searchPage>.jellyseerr-section {
|
||||
div#smart-search-results {
|
||||
padding-bottom: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
label[for="searchTextInput"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.searchFieldsInner>.searchfields-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
@@ -4,8 +4,8 @@
|
||||
const LOG = (...args) => console.log('[SmartSearch]', ...args);
|
||||
const WARN = (...args) => console.warn('[SmartSearch]', ...args);
|
||||
const ERR = (...args) => console.error('[SmartSearch]', ...args);
|
||||
|
||||
LOG('script load start');
|
||||
|
||||
LOG('Initializing...');
|
||||
|
||||
// Configuration
|
||||
const CONFIG = window.KefinTweaksConfig?.search || {
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
// Track if we should block XHR requests (only for direct URL navigation with query params)
|
||||
let shouldBlockXHR = false;
|
||||
let hasBlockedInitialSearch = false;
|
||||
//let hasBlockedInitialSearch = false;
|
||||
|
||||
// Override XHR to block automatic search requests from direct URL navigation
|
||||
const OriginalXHR = window.XMLHttpRequest;
|
||||
@@ -23,17 +23,15 @@
|
||||
const originalOpen = xhr.open;
|
||||
const originalSend = xhr.send;
|
||||
|
||||
xhr.open = function(method, url, ...args) {
|
||||
LOG('XHR request:', method, url);
|
||||
|
||||
xhr.open = function(method, url, ...args) {
|
||||
// Check if this is a search request we want to block
|
||||
// Only block if we're in smart search mode and haven't blocked yet
|
||||
const isSearchRequest = url.includes('/Items?') &&
|
||||
(url.includes('searchTerm=') || url.includes('query='));
|
||||
|
||||
if (isSearchRequest && shouldBlockXHR && !hasBlockedInitialSearch) {
|
||||
if (isSearchRequest && shouldBlockXHR) {
|
||||
LOG('Blocking initial automatic search XHR request:', url);
|
||||
hasBlockedInitialSearch = true;
|
||||
//hasBlockedInitialSearch = true;
|
||||
// Disable blocking after this first request
|
||||
shouldBlockXHR = false;
|
||||
LOG('XHR blocking disabled after initial search');
|
||||
@@ -68,10 +66,11 @@
|
||||
(function() {
|
||||
// Parse query from hash since Jellyfin uses hash-based routing
|
||||
let queryFromUrl = null;
|
||||
if (window.location.hash.includes('search.html') && window.location.hash.includes('query=')) {
|
||||
if (window.location.hash.includes('#/search') && window.location.hash.includes('query=')) {
|
||||
const hashQuery = window.location.hash.split('query=')[1];
|
||||
if (hashQuery) {
|
||||
queryFromUrl = hashQuery.split('&')[0]; // Get query before any other params
|
||||
queryFromUrl = decodeURIComponent(queryFromUrl).replace(/\+/g, ' ');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +78,7 @@
|
||||
LOG('Current pathname:', window.location.pathname);
|
||||
LOG('Current hash:', window.location.hash);
|
||||
|
||||
if (queryFromUrl && (window.location.pathname.includes('search.html') || window.location.hash.includes('search'))) {
|
||||
if (queryFromUrl && window.location.pathname.includes('#/search')) {
|
||||
LOG('URL query detected, enabling XHR blocking for automatic search prevention');
|
||||
shouldBlockXHR = true; // Enable XHR blocking for initial URL query
|
||||
}
|
||||
@@ -127,19 +126,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Function to wait for jellyseerrAPI to be available
|
||||
async function waitForJellyseerrAPI(maxAttempts = 50, interval = 200) {
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
if (window.JellyfinEnhanced?.jellyseerrAPI) {
|
||||
LOG('jellyseerrAPI found after', i * interval, 'ms');
|
||||
return true;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, interval));
|
||||
}
|
||||
WARN('jellyseerrAPI not found after', maxAttempts * interval, 'ms');
|
||||
return false;
|
||||
}
|
||||
|
||||
// styles (dedupe by id)
|
||||
function addCustomStyles() {
|
||||
if (document.getElementById('smart-search-styles')) return;
|
||||
@@ -147,18 +133,17 @@
|
||||
const style = document.createElement('style');
|
||||
style.id = 'smart-search-styles';
|
||||
style.textContent = `
|
||||
.smart-search-wrapper { margin: 12px 0 20px 0; }
|
||||
.smart-search-wrapper { margin: 20px 0; }
|
||||
.smart-search-input { width:100%; padding:8px; border-radius:4px; box-sizing:border-box; }
|
||||
.smart-search-buttons { display:flex; gap:8px; margin-top:8px; flex-wrap:wrap; justify-content: center; }
|
||||
.smart-search-btn { padding:8px 12px; border:none; border-radius:4px; cursor:pointer; background:#444; color:#fff; }
|
||||
.smart-search-btn.active { background:var(--btnSubmitColor); }
|
||||
.smart-search-btn.active { background:#673AB7; }
|
||||
.smart-search-results { margin-top:12px; }
|
||||
.smart-search-stats { color:#999; font-size:12px; margin-top:6px; display: none; }
|
||||
#persistent-toggle-btn { display:none; position:relative; padding:8px 12px; background:#00a4dc; color:#fff; border:none; border-radius:4px; cursor:pointer; }
|
||||
|
||||
/* Smart search mode - hide search icon */
|
||||
.smart-search-mode .searchfields-icon {
|
||||
top: 37px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
right: 0;
|
||||
@@ -172,14 +157,15 @@
|
||||
function updateSearchUrl(searchTerm, searchType = 'videos') {
|
||||
try {
|
||||
// Jellyfin uses hash-based routing, so we need to update the hash
|
||||
const baseHash = '#/search.html';
|
||||
const urlSuffix = ApiClient._serverVersion.split('.')[1] > 10 ? '' : '.html';
|
||||
const baseHash = `#/search${urlSuffix}`;
|
||||
const params = new URLSearchParams();
|
||||
const trimmed = searchTerm.trim();
|
||||
|
||||
if (searchTerm && searchTerm.trim()) {
|
||||
params.set('query', searchTerm.trim());
|
||||
if (trimmed) {
|
||||
params.set('query', trimmed);
|
||||
}
|
||||
params.set('type', searchType);
|
||||
|
||||
const newHash = `${baseHash}?${params.toString()}`;
|
||||
window.history.replaceState({}, '', window.location.pathname + newHash);
|
||||
LOG('Updated search URL hash:', newHash);
|
||||
@@ -197,7 +183,7 @@
|
||||
userId,
|
||||
limit: '100',
|
||||
recursive: 'true',
|
||||
searchTerm,
|
||||
searchTerm: searchTerm.trim(),
|
||||
fields: 'PrimaryImageAspectRatio,CanDelete',
|
||||
imageTypeLimit: '1',
|
||||
enableTotalRecordCount: 'false',
|
||||
@@ -413,11 +399,11 @@
|
||||
// Define the order to display sections
|
||||
const sectionOrder = ['Movie', 'Series', 'Episode', 'Person', 'MusicAlbum', 'Audio', 'Artist', 'Playlist', 'Book', 'AudioBook', 'Photo', 'PhotoAlbum', 'TvChannel', 'LiveTvProgram', 'BoxSet'];
|
||||
|
||||
sectionOrder.forEach((itemType, index) => {
|
||||
sectionOrder.forEach((itemType) => {
|
||||
if (results.groupedItems[itemType] && results.groupedItems[itemType].length > 0) {
|
||||
const items = results.groupedItems[itemType];
|
||||
const title = getTypeDisplayName(itemType);
|
||||
const section = window.cardBuilder.renderCards(items, title, null, index);
|
||||
const section = window.cardBuilder.renderCards(items, title, null);
|
||||
frag.appendChild(section);
|
||||
totalShown += items.length;
|
||||
}
|
||||
@@ -439,10 +425,11 @@
|
||||
// Handle URL query parameter on page load
|
||||
// Parse query from hash since Jellyfin uses hash-based routing
|
||||
let queryFromUrl = null;
|
||||
if (window.location.hash.includes('search.html') && window.location.hash.includes('query=')) {
|
||||
if (window.location.hash.includes('#/search') && window.location.hash.includes('query=')) {
|
||||
const hashQuery = window.location.hash.split('query=')[1];
|
||||
if (hashQuery) {
|
||||
queryFromUrl = hashQuery.split('&')[0]; // Get query before any other params
|
||||
queryFromUrl = decodeURIComponent(queryFromUrl).replace(/\+/g, ' ');
|
||||
}
|
||||
}
|
||||
LOG('initSmartSearch - URL query:', queryFromUrl);
|
||||
@@ -462,6 +449,11 @@
|
||||
wrapper.id = 'smart-search-wrapper';
|
||||
wrapper.className = 'smart-search-wrapper';
|
||||
|
||||
// Create the search icon element
|
||||
const searchIcon = document.createElement('span');
|
||||
searchIcon.className = 'searchfields-icon material-icons search';
|
||||
searchIcon.setAttribute('aria-hidden', 'true');
|
||||
|
||||
// Replace the original input with our smart search input
|
||||
const input = originalInput.cloneNode(true);
|
||||
input.id = 'searchTextInput'; // Use the same ID as original
|
||||
@@ -509,7 +501,14 @@
|
||||
stats.id = 'smart-search-stats';
|
||||
stats.className = 'smart-search-stats';
|
||||
|
||||
wrapper.appendChild(input);
|
||||
// Create input wrapper
|
||||
const inputWrapper = document.createElement('div');
|
||||
inputWrapper.className = 'input-wrapper';
|
||||
|
||||
inputWrapper.appendChild(searchIcon);
|
||||
inputWrapper.appendChild(input);
|
||||
|
||||
wrapper.appendChild(inputWrapper);
|
||||
wrapper.appendChild(btnRow);
|
||||
wrapper.appendChild(stats);
|
||||
|
||||
@@ -538,7 +537,10 @@
|
||||
smartCoreBtn.classList.remove('active');
|
||||
smartMusicBtn.classList.remove('active');
|
||||
smartBooksBtn.classList.remove('active');
|
||||
smartRequestBtn.classList.remove('active');
|
||||
|
||||
if (smartRequestBtn) {
|
||||
smartRequestBtn.classList.remove('active');
|
||||
}
|
||||
|
||||
// Add active class to the selected button
|
||||
if (activeType === 'all') {
|
||||
@@ -549,7 +551,7 @@
|
||||
smartMusicBtn.classList.add('active');
|
||||
} else if (activeType === 'books') {
|
||||
smartBooksBtn.classList.add('active');
|
||||
} else if (activeType === 'request') {
|
||||
} else if (activeType === 'request' && smartRequestBtn) {
|
||||
smartRequestBtn.classList.add('active');
|
||||
}
|
||||
}
|
||||
@@ -698,31 +700,6 @@
|
||||
setMode(true);
|
||||
}
|
||||
|
||||
function init() {
|
||||
LOG('init() called');
|
||||
LOG('Pathname:', window.location.pathname);
|
||||
LOG('Hash:', window.location.hash);
|
||||
|
||||
if (window.location.pathname.includes('search.html') || window.location.hash.includes('search')) {
|
||||
LOG('Search page detected');
|
||||
|
||||
// Check for URL query parameter and prevent default search early
|
||||
// Parse query from hash since Jellyfin uses hash-based routing
|
||||
let queryFromUrl = null;
|
||||
if (window.location.hash.includes('search.html') && window.location.hash.includes('query=')) {
|
||||
const hashQuery = window.location.hash.split('query=')[1];
|
||||
if (hashQuery) {
|
||||
queryFromUrl = hashQuery.split('&')[0]; // Get query before any other params
|
||||
}
|
||||
}
|
||||
LOG('URL query in init:', queryFromUrl);
|
||||
|
||||
|
||||
LOG('Calling initSmartSearch');
|
||||
initSmartSearch();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize search hook using utils
|
||||
function initializeSearchHook() {
|
||||
if (!window.KefinTweaksUtils) {
|
||||
@@ -753,14 +730,14 @@
|
||||
}
|
||||
|
||||
// Initialize the hook when the script loads
|
||||
initializeSearchHook();
|
||||
//initializeSearchHook();
|
||||
|
||||
if (document.readyState==='loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
init();
|
||||
initializeSearchHook();
|
||||
});
|
||||
} else {
|
||||
init();
|
||||
initializeSearchHook();
|
||||
}
|
||||
LOG('script load end');
|
||||
LOG('Initialized successfully');
|
||||
})();
|
||||
+1505
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,3 @@
|
||||
#indexPage, #moviesPage, #tvRecommendedPage, #musicRecommendedPage {
|
||||
overflow-y: visible !important;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
.layout-desktop .streaming-lookup-container {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
z-index: 2;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.layout-desktop .nextUpSection.verticalSection.detailVerticalSection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: end;
|
||||
outline: none;
|
||||
border: var(--defaultBorder);
|
||||
border-radius: var(--smallRadius);
|
||||
background-color: var(--lighterGradientPointAlpha);
|
||||
}
|
||||
|
||||
.layout-desktop .nextUpSection .card.overflowBackdropCard {
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.layout-desktop .nextUpSection h2.sectionTitle.sectionTitle-cards {
|
||||
border-bottom: var(--defaultBorder);
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.layout-desktop .nextUpItems {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
.layout-desktop .nextUpSection .cardScalable, .layout-desktop .nextUpSection .cardText {
|
||||
width: 18em;
|
||||
}
|
||||
|
||||
.layout-desktop .nextUpSection .cardBox {
|
||||
}
|
||||
|
||||
.layout-desktop .nextUpSection .cardOverlayContainer {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.layout-desktop .nextUpSection .cardBox {
|
||||
margin-top: 0;
|
||||
}
|
||||
.layout-desktop h3.parentName.musicParentName {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.layout-desktop .nextUpSection .cardBox {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.layout-desktop .nextUpSection {
|
||||
position: absolute;
|
||||
right: 2em;
|
||||
bottom: 18vh;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.layout-desktop .nextUpSection h2.sectionTitle {
|
||||
margin-left: 2em;
|
||||
margin-bottom: -0.6em;
|
||||
}
|
||||
|
||||
.layout-desktop .nextUpSection .card.overflowBackdropCard {
|
||||
width: 18vw !important;
|
||||
}
|
||||
|
||||
#slides-container {
|
||||
top: 2em !important;
|
||||
}
|
||||
|
||||
.slide .watchlist-button:not(:hover) {
|
||||
background: transparent;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/* Media bar slides container */
|
||||
div#slides-container {
|
||||
margin-left: 250px !important;
|
||||
width: calc(100% - 250px) !important;
|
||||
}
|
||||
/* body.home-screen .emby-tab-button {
|
||||
display: flex;
|
||||
}
|
||||
body.home-screen .emby-tabs-slider {
|
||||
display: flex;
|
||||
}
|
||||
body.home-screen .emby-tab-button > div {
|
||||
align-self: normal;
|
||||
} */
|
||||
@@ -0,0 +1,29 @@
|
||||
.kefinTweaks-breadcrumbs-wrapper {
|
||||
margin-left: 180px;
|
||||
color: var(--theme-text-colour);
|
||||
border-radius: var(--theme-roundness);
|
||||
box-shadow: inset 0 1px var(--theme-menu-shadow-colour);
|
||||
background-color: var(--theme-menu-background-colour);
|
||||
backdrop-filter: blur(var(--theme-blur)) !important;
|
||||
-webkit-backdrop-filter: blur(var(--theme-blur)) !important;
|
||||
-moz-backdrop-filter: blur(var(--theme-blur)) !important;
|
||||
-o-backdrop-filter: blur(var(--theme-blur)) !important;
|
||||
-ms-backdrop-filter: blur(var(--theme-blur)) !important;
|
||||
height: 100%;
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.watchlist-icon .detailButton-content:before {
|
||||
content: 'Watchlist';
|
||||
}
|
||||
|
||||
.kefinTweaks-breadcrumbs-wrapper:hover {
|
||||
background: var(--theme-menu-background-colour);
|
||||
}
|
||||
|
||||
/* Media bar slides container */
|
||||
div#slides-container {
|
||||
margin-left: 250px !important;
|
||||
width: calc(100% - 250px) !important;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/* Media bar slides container */
|
||||
div#slides-container {
|
||||
margin-left: 250px !important;
|
||||
width: calc(100% - 250px) !important;
|
||||
}
|
||||
body.home-screen .emby-tab-button {
|
||||
display: flex;
|
||||
}
|
||||
body.home-screen .emby-tabs-slider {
|
||||
display: flex;
|
||||
}
|
||||
body.home-screen .emby-tab-button > div {
|
||||
align-self: normal;
|
||||
}
|
||||
+3
-2
@@ -10,7 +10,7 @@
|
||||
const WARN = (...args) => console.warn('[SubtitleSearch]', ...args);
|
||||
const ERR = (...args) => console.error('[SubtitleSearch]', ...args);
|
||||
|
||||
LOG('Script loaded - Subtitle Search functionality');
|
||||
LOG('Initializing...');
|
||||
|
||||
// State management
|
||||
let isInitialized = false;
|
||||
@@ -1259,5 +1259,6 @@
|
||||
window.setSubtitleStreamIndex = setSubtitleStreamIndex;
|
||||
window.listenForSubtitleStreamChange = listenForSubtitleStreamChange;
|
||||
window.triggerPlaybackProgressEvent = triggerPlaybackProgressEvent;
|
||||
|
||||
|
||||
LOG('Initialized successfully');
|
||||
})();
|
||||
+306
@@ -0,0 +1,306 @@
|
||||
// KefinTweaks User Configuration
|
||||
// User-specific settings and configurations for KefinTweaks features
|
||||
// This file contains settings that users can customize for their specific needs
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// USER CONFIGURATION
|
||||
// ============================================================================
|
||||
|
||||
const USER_CONFIG = {
|
||||
// ============================================================================
|
||||
// SKIN CONFIGURATION
|
||||
// ============================================================================
|
||||
// Configure available skins for the display settings page
|
||||
// These skins will be merged with skins from the main KefinTweaks config
|
||||
// User-defined skins take priority over main config skins (no duplicates by name)
|
||||
// Each skin can have a single URL (string) or multiple URLs (array) for complex themes
|
||||
// Each skin can also have colorSchemes array for color variations specific to that skin
|
||||
// Each skin should have an author field indicating who created the skin
|
||||
skins: [
|
||||
{
|
||||
name: 'Default',
|
||||
author: 'Jellyfin',
|
||||
url: null, // null means no additional CSS (default Jellyfin theme)
|
||||
colorSchemes: []
|
||||
},
|
||||
{
|
||||
name: 'ElegantFin',
|
||||
author: 'lscambo13',
|
||||
url: [
|
||||
'https://cdn.jsdelivr.net/gh/lscambo13/ElegantFin@main/Theme/ElegantFin-jellyfin-theme-build-latest-minified.css',
|
||||
'https://cdn.jsdelivr.net/gh/lscambo13/ElegantFin@main/Theme/assets/add-ons/media-bar-plugin-support-latest-min.css',
|
||||
'https://cdn.jsdelivr.net/gh/LitCastVlog/Flow@main/CSS/ScyFLow-RoundCastCrew.css',
|
||||
'https://cdn.jsdelivr.net/gh/CTalvio/Ultrachromic/episodelist/episodes_grid.css',
|
||||
`${KefinTweaksConfig.scriptRoot}skins/elegantKefin.css`
|
||||
],
|
||||
colorSchemes: []
|
||||
},
|
||||
{
|
||||
name: 'Jellypane',
|
||||
author: 'tedhinklater',
|
||||
url: 'https://cdn.jsdelivr.net/gh/tedhinklater/Jellypane@main/Jellypane.css',
|
||||
colorSchemes: []
|
||||
},
|
||||
{
|
||||
name: 'Jellyfish',
|
||||
author: 'n00bcodr',
|
||||
url: 'https://cdn.jsdelivr.net/gh/n00bcodr/jellyfish@main/theme.css',
|
||||
colorSchemes: [
|
||||
{
|
||||
name: 'Default',
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
name: 'Banana',
|
||||
url: 'https://cdn.jsdelivr.net/gh/n00bcodr/Jellyfish/colors/banana.css'
|
||||
},
|
||||
{
|
||||
name: 'Coal',
|
||||
url: 'https://cdn.jsdelivr.net/gh/n00bcodr/Jellyfish/colors/coal.css'
|
||||
},
|
||||
{
|
||||
name: 'Coral',
|
||||
url: 'https://cdn.jsdelivr.net/gh/n00bcodr/Jellyfish/colors/coral.css'
|
||||
},
|
||||
{
|
||||
name: 'Grass',
|
||||
url: 'https://cdn.jsdelivr.net/gh/n00bcodr/Jellyfish/colors/grass.css'
|
||||
},
|
||||
{
|
||||
name: 'Jellyblue',
|
||||
url: 'https://cdn.jsdelivr.net/gh/n00bcodr/Jellyfish/colors/jellyblue.css'
|
||||
},
|
||||
{
|
||||
name: 'Jellyflix',
|
||||
url: 'https://cdn.jsdelivr.net/gh/n00bcodr/Jellyfish/colors/jellyflix.css'
|
||||
},
|
||||
{
|
||||
name: 'Lavender',
|
||||
url: 'https://cdn.jsdelivr.net/gh/n00bcodr/Jellyfish/colors/lavender.css'
|
||||
},
|
||||
{
|
||||
name: 'Mint',
|
||||
url: 'https://cdn.jsdelivr.net/gh/n00bcodr/Jellyfish/colors/mint.css'
|
||||
},
|
||||
{
|
||||
name: 'Ocean',
|
||||
url: 'https://cdn.jsdelivr.net/gh/n00bcodr/Jellyfish/colors/ocean.css'
|
||||
},
|
||||
{
|
||||
name: 'Peach',
|
||||
url: 'https://cdn.jsdelivr.net/gh/n00bcodr/Jellyfish/colors/peach.css'
|
||||
},
|
||||
{
|
||||
name: 'Watermelon',
|
||||
url: 'https://cdn.jsdelivr.net/gh/n00bcodr/Jellyfish/colors/watermelon.css'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Glassmorphism',
|
||||
author: 'alexyle',
|
||||
url: [
|
||||
'https://cdn.jsdelivr.net/gh/alexyle/jellyfin-theme@main/glassmorphism/theme.css',
|
||||
],
|
||||
colorSchemes: []
|
||||
},
|
||||
{
|
||||
name: 'Zombie',
|
||||
author: 'MakD',
|
||||
url: 'https://cdn.jsdelivr.net/gh/MakD/zombie-release@latest/zombie_revived.css',
|
||||
colorSchemes: [
|
||||
{
|
||||
name: 'Default',
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
name: 'Amazon Prime',
|
||||
url: 'https://cdn.jsdelivr.net/gh/MakD/zombie-release@latest/color-schemes/amazon-prime.css'
|
||||
},
|
||||
{
|
||||
name: 'Apple TV',
|
||||
url: 'https://cdn.jsdelivr.net/gh/MakD/zombie-release@latest/color-schemes/apple-tv.css'
|
||||
},
|
||||
{
|
||||
name: 'Disney',
|
||||
url: 'https://cdn.jsdelivr.net/gh/MakD/zombie-release@latest/color-schemes/disney.css'
|
||||
},
|
||||
{
|
||||
name: 'HBO Max',
|
||||
url: 'https://cdn.jsdelivr.net/gh/MakD/zombie-release@latest/color-schemes/hbo-max.css'
|
||||
},
|
||||
{
|
||||
name: 'Hulu',
|
||||
url: 'https://cdn.jsdelivr.net/gh/MakD/zombie-release@latest/color-schemes/hulu.css'
|
||||
},
|
||||
{
|
||||
name: 'Netflix',
|
||||
url: 'https://cdn.jsdelivr.net/gh/MakD/zombie-release@latest/color-schemes/netflix.css'
|
||||
},
|
||||
{
|
||||
name: 'YouTube',
|
||||
url: 'https://cdn.jsdelivr.net/gh/MakD/zombie-release@latest/color-schemes/youtube.css'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Jamfin',
|
||||
author: 'JamsRepos',
|
||||
url: [
|
||||
'https://cdn.jsdelivr.net/gh/JamsRepos/Jamfin@latest/theme/complete.css',
|
||||
`${KefinTweaksConfig.scriptRoot}skins/jamfin-kefin.css`
|
||||
],
|
||||
colorSchemes: []
|
||||
},
|
||||
{
|
||||
name: 'Scyfin',
|
||||
author: 'loof2736',
|
||||
url: [
|
||||
'https://cdn.jsdelivr.net/gh/loof2736/scyfin@latest/CSS/scyfin-theme.css',
|
||||
`${KefinTweaksConfig.scriptRoot}skins/scyfin-kefin.css`
|
||||
],
|
||||
colorSchemes: [
|
||||
{
|
||||
name: 'Default',
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
name: 'Seafoam',
|
||||
url: 'https://cdn.jsdelivr.net/gh/loof2736/scyfin@latest/CSS/theme-seafoam.css'
|
||||
},
|
||||
{
|
||||
name: 'Coral',
|
||||
url: 'https://cdn.jsdelivr.net/gh/loof2736/scyfin@latest/CSS/theme-coral.css'
|
||||
},
|
||||
{
|
||||
name: 'Snow',
|
||||
url: 'https://cdn.jsdelivr.net/gh/loof2736/scyfin@latest/CSS/theme-snow.css'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Catppuccin',
|
||||
author: 'Catppuccin',
|
||||
url: 'https://jellyfin.catppuccin.com/theme.css',
|
||||
colorSchemes: [
|
||||
{
|
||||
name: 'Latte',
|
||||
url: 'https://jellyfin.catppuccin.com/catppuccin-latte.css'
|
||||
},
|
||||
{
|
||||
name: 'Frappé',
|
||||
url: 'https://jellyfin.catppuccin.com/catppuccin-frappe.css'
|
||||
},
|
||||
{
|
||||
name: 'Macchiato',
|
||||
url: 'https://jellyfin.catppuccin.com/catppuccin-macchiato.css'
|
||||
},
|
||||
{
|
||||
name: 'Mocha',
|
||||
url: 'https://jellyfin.catppuccin.com/catppuccin-mocha.css'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Flow',
|
||||
author: 'LitCastVlog',
|
||||
url: [
|
||||
'https://cdn.jsdelivr.net/gh/LitCastVlog/Flow@main/CSS/ScyFlow-main.css',
|
||||
`${KefinTweaksConfig.scriptRoot}skins/flow-kefin.css`
|
||||
],
|
||||
colorSchemes: [
|
||||
{
|
||||
name: 'Default',
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
name: 'Orange',
|
||||
url: 'https://cdn.jsdelivr.net/gh/LitCastVlog/Flow@main/CSS/Themes/ScyFlow-Orange.css'
|
||||
},
|
||||
{
|
||||
name: 'Pink',
|
||||
url: 'https://cdn.jsdelivr.net/gh/LitCastVlog/Flow@main/CSS/Themes/ScyFlow-Pink.css'
|
||||
},
|
||||
{
|
||||
name: 'Rainbow',
|
||||
url: 'https://cdn.jsdelivr.net/gh/LitCastVlog/Flow@main/CSS/Themes/ScyFlow-Rainbow.css'
|
||||
},
|
||||
{
|
||||
name: 'Red',
|
||||
url: 'https://cdn.jsdelivr.net/gh/LitCastVlog/Flow@main/CSS/Themes/ScyFlow-Red.css'
|
||||
},
|
||||
{
|
||||
name: 'White',
|
||||
url: 'https://cdn.jsdelivr.net/gh/LitCastVlog/Flow@main/CSS/Themes/ScyFlow-White.css'
|
||||
},
|
||||
{
|
||||
name: 'Blue',
|
||||
url: 'https://cdn.jsdelivr.net/gh/LitCastVlog/Flow@main/CSS/Themes/ScyFlow-Blue.css'
|
||||
},
|
||||
{
|
||||
name: 'Seafoam',
|
||||
url: 'https://cdn.jsdelivr.net/gh/loof2736/scyfin@latest/CSS/theme-seafoam.css'
|
||||
},
|
||||
{
|
||||
name: 'Coral',
|
||||
url: 'https://cdn.jsdelivr.net/gh/loof2736/scyfin@latest/CSS/theme-coral.css'
|
||||
},
|
||||
{
|
||||
name: 'Snow',
|
||||
url: 'https://cdn.jsdelivr.net/gh/loof2736/scyfin@latest/CSS/theme-snow.css'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Monochromic',
|
||||
author: 'CTalvio',
|
||||
url: [
|
||||
'https://cdn.jsdelivr.net/gh/CTalvio/Ultrachromic/presets/monochromic_preset.css',
|
||||
`${KefinTweaksConfig.scriptRoot}skins/chromic-kefin.css`
|
||||
],
|
||||
colorSchemes: []
|
||||
},
|
||||
{
|
||||
name: 'Kaleidochromic',
|
||||
author: 'CTalvio',
|
||||
url: [
|
||||
'https://cdn.jsdelivr.net/gh/CTalvio/Ultrachromic/presets/kaleidochromic_preset.css',
|
||||
`${KefinTweaksConfig.scriptRoot}skins/chromic-kefin.css`
|
||||
],
|
||||
colorSchemes: []
|
||||
},
|
||||
{
|
||||
name: 'Novachromic',
|
||||
author: 'CTalvio',
|
||||
url: [
|
||||
'https://cdn.jsdelivr.net/gh/CTalvio/Ultrachromic/presets/novachromic_preset.css',
|
||||
`${KefinTweaksConfig.scriptRoot}skins/chromic-kefin.css`
|
||||
],
|
||||
colorSchemes: []
|
||||
}
|
||||
// Add more skins here as needed
|
||||
],
|
||||
|
||||
// ============================================================================
|
||||
// THEME CONFIGURATION
|
||||
// ============================================================================
|
||||
// Configure available themes for color variations
|
||||
// These themes will be added as options to the existing select#selectTheme dropdown
|
||||
// When a custom theme is selected, the cssTheme link href will be updated to the custom URL
|
||||
// Each theme should have a name and URL pointing to a CSS file
|
||||
themes: [
|
||||
]
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// CONFIGURATION EXPOSURE
|
||||
// ============================================================================
|
||||
// Make user configuration available to scripts
|
||||
window.KefinTweaksUserConfig = USER_CONFIG;
|
||||
|
||||
console.log('[KefinTweaks UserConfig] User configuration loaded. Available at window.KefinTweaksUserConfig');
|
||||
|
||||
})();
|
||||
@@ -10,7 +10,7 @@
|
||||
const WARN = (...args) => console.warn('[KefinTweaks Utils]', ...args);
|
||||
const ERR = (...args) => console.error('[KefinTweaks Utils]', ...args);
|
||||
|
||||
LOG('Utils script loaded');
|
||||
LOG('Initializing');
|
||||
|
||||
// Store the original onViewShow function
|
||||
let originalOnViewShow = null;
|
||||
@@ -142,26 +142,14 @@
|
||||
* @returns {string|null} Current view name or null if not available
|
||||
*/
|
||||
function getCurrentView() {
|
||||
// Try to get view from URL hash (primary method)
|
||||
const hash = window.location.hash;
|
||||
if (hash) {
|
||||
// Extract page name from hash patterns like #/home.html, #/search.html, etc.
|
||||
const pageMatch = hash.match(/#\/([^\/\?]+)\.html/);
|
||||
// Extract page name from hash - match part after #/ and before ? or /
|
||||
// This handles both .html pages (old) and pages without .html (Jellyfin 10.11+)
|
||||
const pageMatch = hash.match(/#\/([^\/\?]+)/);
|
||||
if (pageMatch) {
|
||||
return pageMatch[1];
|
||||
}
|
||||
|
||||
// Fallback: look for /view/ pattern
|
||||
const viewMatch = hash.match(/\/view\/([^\/\?]+)/);
|
||||
if (viewMatch) {
|
||||
return viewMatch[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get view from active page element (fallback)
|
||||
const activePage = document.querySelector('.page:not(.hide)');
|
||||
if (activePage) {
|
||||
return activePage.getAttribute('data-page') || activePage.className.split(' ').find(cls => cls.includes('Page'));
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -287,12 +275,13 @@
|
||||
// Expose utilities to global scope
|
||||
window.KefinTweaksUtils = {
|
||||
onViewPage,
|
||||
notifyHandlers,
|
||||
getCurrentView,
|
||||
getHandlerCount,
|
||||
clearHandlers,
|
||||
addCustomMenuLink
|
||||
};
|
||||
|
||||
LOG('KefinTweaks Utils loaded and available at window.KefinTweaksUtils');
|
||||
|
||||
LOG('Initialized successfully');
|
||||
LOG('Available at window.KefinTweaksUtils');
|
||||
})();
|
||||
+127
-40
@@ -9,6 +9,7 @@
|
||||
/* Watchlist Tab Styling */
|
||||
.sections.watchlist {
|
||||
padding-bottom: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.sections.watchlist .watchlist-tabs {
|
||||
@@ -35,6 +36,7 @@
|
||||
}
|
||||
|
||||
.sections.watchlist .watchlist-tabs button:hover {
|
||||
color: rgba(200, 200, 200, 0.9);
|
||||
color: rgba(200, 200, 200, 0.9);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
@@ -43,6 +45,9 @@
|
||||
color: rgba(200, 200, 200, 1);
|
||||
border-bottom-color: #ffffff;
|
||||
background: rgba(200, 200, 200, 0.1);
|
||||
color: rgba(200, 200, 200, 1);
|
||||
border-bottom-color: #ffffff;
|
||||
background: rgba(200, 200, 200, 0.1);
|
||||
}
|
||||
|
||||
/* Tab content styling - hide content by default, show only active */
|
||||
@@ -133,12 +138,15 @@
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.progress-tab-header, .movie-history-header {
|
||||
.progress-tab-header, .movie-history-header, .watchlist-header {
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 10px 0 10px 10px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.progress-title {
|
||||
@@ -242,22 +250,50 @@
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.progress-header-stats-container, .movie-history-stats-container {
|
||||
.progress-header-stats-container, .movie-history-stats-container, .watchlist-header-stats-container {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.progress-header-stats {
|
||||
.progress-header-stats, .watchlist-header-stats, .movie-history-stats {
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 6px 12px;
|
||||
padding: 12px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.watchlist-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.layout-toggle-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.layout-toggle-btn:hover {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.layout-toggle-btn .material-icons {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.progress-stats-separator {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-weight: 300;
|
||||
@@ -485,8 +521,20 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.progress-tab-header h2 {
|
||||
display: none;
|
||||
.tab-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.progress-header-stats-container, .movie-history-stats-container, .watchlist-header-stats-container {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tab-header h2 {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -718,17 +766,6 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.movie-history-stats {
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 6px 12px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.movie-history-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
@@ -972,19 +1009,7 @@
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.movie-history-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.movie-history-header h2 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.movie-poster {
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
@@ -1623,15 +1648,6 @@ div[data-tab="watchlist"] .loading-message {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.episode-count-info {
|
||||
background: rgba(255, 193, 7, 0.1);
|
||||
border: 1px solid rgba(255, 193, 7, 0.3);
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
margin-bottom: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.episode-count-info .episode-count {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
@@ -1639,6 +1655,15 @@ div[data-tab="watchlist"] .loading-message {
|
||||
}
|
||||
|
||||
.episode-count-info {
|
||||
background: rgba(255, 193, 7, 0.1);
|
||||
border: 1px solid rgba(255, 193, 7, 0.3);
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
margin-bottom: 16px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 14px;
|
||||
}
|
||||
@@ -1809,4 +1834,66 @@ div[data-tab="watchlist"] .loading-message {
|
||||
|
||||
.watchlist-button[data-active="true"], .watchlist-icon[data-active="true"] {
|
||||
color: rgb(34, 197, 94) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.watchlist-button[data-active="true"] span, .watchlist-icon[data-active="true"] span {
|
||||
font-variation-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 48;
|
||||
}
|
||||
.slide .watchlist-button::before {
|
||||
content: 'bookmark';
|
||||
font-family: 'Material Icons';
|
||||
font-weight: 600;
|
||||
display: inline-block;
|
||||
font-size: 22px;
|
||||
vertical-align: middle;
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 48;
|
||||
}
|
||||
.slide .watchlist-button {
|
||||
border-radius: 50%;
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
-webkit-tap-highlight-color: #fff0;
|
||||
}
|
||||
|
||||
.card:has(button[data-played="true"]) .watchlist-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* List View Styling */
|
||||
[data-layout='List'] .cardScalable {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-layout='List'] .itemsContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
[data-layout='List'] .verticalSection.emby-scroller-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
[data-layout='List'] .cardBox {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
[data-layout='List'] .card {
|
||||
width: 100% !important;
|
||||
cursor: initial;
|
||||
}
|
||||
|
||||
[data-layout='List'] .cardText-first {
|
||||
flex: 1;
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
[data-layout='List'] .cardText-secondary {
|
||||
font-size: 100%;
|
||||
padding-top: .24em;
|
||||
}
|
||||
|
||||
+734
-216
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user