mirror of
https://github.com/ranaldsgift/KefinTweaks.git
synced 2026-11-05 15:52:47 +01:00
Improve onViewPage util, improve top person caching for home page, improve watchlist caching, fixes for mobile UI and Flatten Shows improvements
This commit is contained in:
1 parent
a4c492728e
commit
f95c9abd8c
18 files changed
+1055
-478
No files matched your search
@@ -6,6 +6,8 @@ While working on these scripts to meet my personal needs, I noticed that there w
|
||||
|
||||
There is no configuration required, but if you wish to customize some of the features, there are options to do so.
|
||||
|
||||
I think it's also worth mentioning that before the 1.0 release, you can expect to potentially encounter some issues or bugs. I try to address these as quickly as I can, and I appreciate your time for raising the issues in the first place!
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Installation](#-installation)
|
||||
@@ -46,17 +48,18 @@ There is no configuration required, but if you wish to customize some of the fea
|
||||
- [Planned Features](#planned-features)
|
||||
- [Version History](#version-history)
|
||||
|
||||
## 📦 Installation
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
- Jellyfin 10.10.7 or later
|
||||
- Jellyfin 10.10.7 or earlier (10.11.X is untested and "unsupported" but may be mostly functional)
|
||||
- [JS Injector](https://github.com/n00bcodr/Jellyfin-JavaScript-Injector) plugin installed and configured
|
||||
- [Custom Tabs](https://github.com/IAmParadox27/jellyfin-plugin-custom-tabs) plugin for watchlist functionality
|
||||
- [JellyfinEnhanced](https://github.com/n00bcodr/Jellyfin-Enhanced) plugin for Jellyseerr search functionality and ExclusiveElsewhere
|
||||
|
||||
### Setup Instructions
|
||||
|
||||
0. **Install Prerequisites listed above**
|
||||
0a. **Install Prerequisites listed above**
|
||||
0b. **If you are using JellyfinEnhanced, you MUST disable the "Watchlist" feature from the plugin settings**
|
||||
1. **Add a new script to your JS Injector Plugin**
|
||||
2. **Copy this entire contents of kefinTweaks.js into the new script**
|
||||
|
||||
@@ -98,8 +101,8 @@ The features in KefinTweaks which use local data caching are listed below, along
|
||||
- **Top People** [24h]:
|
||||
In order to populate the "Top People" in your movie library, we fetch the Person data for every movie in your library. We build a list of all the People who appear in at least X number of items based on their type (Actor/Director/Writer). This is an expensive operation, and is longer as the size of your library grows.
|
||||
- Watchlist
|
||||
- **Watchlist Items** [24h]:
|
||||
This is mostly done to provide a more responsive UX, as typically the API call to retreive these items should not be very expensive. The Watchlist cache is also updated any time there is a change to an item's Play State or Watchlist status. Keep in mind that the cache can only be updated locally, so if you watch a movie from your watchlist on Device X, and then you browse your Watchlist on Device Y, the Watchlist will still show the watched item until the cache is manually refreshed or it expires.
|
||||
- **Watchlist Items** [5m]:
|
||||
This is mostly done to provide a more responsive UX, as typically the API call to retreive these items should not be very expensive. The Watchlist cache is also updated any time there is a change to an item's Play State or Watchlist status.
|
||||
- **Series Progress and Movie History** [24h]:
|
||||
These are both more expensive operations, especially if you have watched a very high number of Movies or Shows. It's not practical to fetch this data on demand so we cache it to improve UX.
|
||||
- Collections on Details Page
|
||||
|
||||
+88
-89
@@ -1,104 +1,103 @@
|
||||
/* KefinTweaks Breadcrumbs Styles */
|
||||
|
||||
.kefinTweaks-breadcrumbs-wrapper {
|
||||
margin-left: -.5em;
|
||||
transition: background 0.2s ease;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.kefinTweaks-breadcrumbs-wrapper:hover {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 0.5em;
|
||||
}
|
||||
|
||||
.kefinTweaks-breadcrumbs {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 1.17em;
|
||||
font-family: inherit;
|
||||
pointer-events: auto;
|
||||
position: relative;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
word-wrap: break-word;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.kefinTweaks-popover-container {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.kefinTweaks-breadcrumbs span {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.kefinTweaks-breadcrumbs span:hover {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
}
|
||||
|
||||
/* Hover effects for clickable breadcrumbs only */
|
||||
.kefinTweaks-breadcrumbs span[style*="cursor: pointer"]:hover {
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
|
||||
/* Responsive behavior */
|
||||
@media (max-width: 768px) {
|
||||
@media (min-width: 768px) {
|
||||
.kefinTweaks-breadcrumbs-wrapper {
|
||||
display: none !important;
|
||||
display: block;
|
||||
margin-left: -.5em;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
/* Popover styles for seasons and albums */
|
||||
.kefinTweaks-popover {
|
||||
position: relative;
|
||||
background-color: rgba(29, 38, 53, 0.95);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
padding: 0;
|
||||
min-width: 200px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
z-index: 1001;
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.kefinTweaks-breadcrumbs-wrapper:hover {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 0.5em;
|
||||
}
|
||||
|
||||
.kefinTweaks-popover-item {
|
||||
padding: 8px 16px 8px 24px;
|
||||
cursor: pointer;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
position: relative;
|
||||
}
|
||||
.kefinTweaks-breadcrumbs {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 1.17em;
|
||||
font-family: inherit;
|
||||
pointer-events: auto;
|
||||
position: relative;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
word-wrap: break-word;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.kefinTweaks-popover-item:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
color: rgba(255, 255, 255, 1);
|
||||
}
|
||||
.kefinTweaks-popover-container {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.kefinTweaks-popover-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.kefinTweaks-breadcrumbs span {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.kefinTweaks-popover-item.selected {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
font-weight: bold;
|
||||
}
|
||||
.kefinTweaks-breadcrumbs span:hover {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
}
|
||||
|
||||
.kefinTweaks-popover-item.selected::before {
|
||||
content: '>';
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: calc(50% - 1px);
|
||||
transform: translateY(-50%);
|
||||
font-weight: bold;
|
||||
}
|
||||
/* Hover effects for clickable breadcrumbs only */
|
||||
.kefinTweaks-breadcrumbs span[style*="cursor: pointer"]:hover {
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
|
||||
.skinHeader {
|
||||
contain: none;
|
||||
}
|
||||
/* Popover styles for seasons and albums */
|
||||
.kefinTweaks-popover {
|
||||
position: relative;
|
||||
background-color: rgba(29, 38, 53, 0.95);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
padding: 0;
|
||||
min-width: 200px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
z-index: 1001;
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.headerLeft {
|
||||
overflow: visible;
|
||||
.kefinTweaks-popover-item {
|
||||
padding: 8px 16px 8px 24px;
|
||||
cursor: pointer;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.kefinTweaks-popover-item:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
color: rgba(255, 255, 255, 1);
|
||||
}
|
||||
|
||||
.kefinTweaks-popover-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.kefinTweaks-popover-item.selected {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.kefinTweaks-popover-item.selected::before {
|
||||
content: '>';
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: calc(50% - 1px);
|
||||
transform: translateY(-50%);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.skinHeader {
|
||||
contain: none;
|
||||
}
|
||||
|
||||
.headerLeft {
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
+1
-4
@@ -515,8 +515,6 @@
|
||||
const currentPath = Emby.Page.lastPath || '';
|
||||
const isSupportedPage = currentPath.startsWith('/details?');
|
||||
|
||||
log('showBreadcrumbs called - wrapper exists:', !!wrapper, 'isWideScreen:', wideScreen, 'isSupportedPage:', isSupportedPage, 'currentPath:', currentPath);
|
||||
|
||||
if (wrapper && wideScreen && isSupportedPage) {
|
||||
wrapper.style.display = 'block';
|
||||
log('Breadcrumbs shown successfully');
|
||||
@@ -878,7 +876,7 @@
|
||||
log('Registering breadcrumb handler with KefinTweaksUtils');
|
||||
|
||||
// Register handler for all pages (breadcrumbs can appear on any detail page)
|
||||
window.KefinTweaksUtils.onViewPage(async (view, element, itemPromise) => {
|
||||
window.KefinTweaksUtils.onViewPage(async (view, element, hash, itemPromise) => {
|
||||
try {
|
||||
// Await the item promise to get the actual item data
|
||||
const item = await itemPromise;
|
||||
@@ -888,7 +886,6 @@
|
||||
error('Breadcrumb page change handler failed:', err);
|
||||
}
|
||||
}, {
|
||||
immediate: true,
|
||||
pages: [] // Empty array means all pages
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -23,4 +23,12 @@ div#seriesScheduleSection {
|
||||
|
||||
.commentsSection {
|
||||
order: 3;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.headerRight .btnRecommendations,
|
||||
.headerRight .btnAdmin,
|
||||
.headerRight .headerAppearanceButton {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
+156
-7
@@ -9,6 +9,51 @@
|
||||
const WARN = (...args) => console.warn('[KefinTweaks FlattenSingleSeasonShows]', ...args);
|
||||
const ERR = (...args) => console.error('[KefinTweaks FlattenSingleSeasonShows]', ...args);
|
||||
|
||||
/**
|
||||
* Fetches the Next Up episode for a given series
|
||||
* @param {string} seriesId - The series ID
|
||||
* @returns {Promise<Object|null>} - Episode number object with season and episode, or null
|
||||
*/
|
||||
async function fetchNextUpEpisode(seriesId) {
|
||||
try {
|
||||
const apiClient = window.ApiClient;
|
||||
const userId = apiClient.getCurrentUserId();
|
||||
const serverUrl = apiClient.serverAddress();
|
||||
const token = apiClient.accessToken();
|
||||
|
||||
LOG(`Fetching Next Up episode for series: ${seriesId}`);
|
||||
const nextUpUrl = `${serverUrl}/Shows/NextUp?SeriesId=${seriesId}&UserId=${userId}&Fields=MediaSourceCount`;
|
||||
const nextUpRes = await fetch(nextUpUrl, {
|
||||
headers: { "Authorization": `MediaBrowser Token="${token}"` }
|
||||
});
|
||||
|
||||
if (!nextUpRes.ok) {
|
||||
throw new Error(`HTTP ${nextUpRes.status}: ${nextUpRes.statusText}`);
|
||||
}
|
||||
|
||||
const nextUpData = await nextUpRes.json();
|
||||
const items = nextUpData.Items || [];
|
||||
|
||||
if (items.length > 0) {
|
||||
const item = items[0];
|
||||
if (item.IndexNumber && item.ParentIndexNumber) {
|
||||
const episodeNumber = {
|
||||
season: item.ParentIndexNumber,
|
||||
episode: item.IndexNumber
|
||||
};
|
||||
LOG(`Fetched Next Up episode from API: S${episodeNumber.season}:E${episodeNumber.episode}`);
|
||||
return episodeNumber;
|
||||
}
|
||||
}
|
||||
|
||||
LOG('No Next Up episode found in API response');
|
||||
return null;
|
||||
} catch (error) {
|
||||
ERR(`Failed to fetch Next Up episode for series ${seriesId}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all episodes for a given series
|
||||
* @param {string} seriesId - The series ID
|
||||
@@ -22,7 +67,7 @@
|
||||
const token = apiClient.accessToken();
|
||||
|
||||
LOG(`Fetching episodes for series: ${seriesId}`);
|
||||
const episodesUrl = `${serverUrl}/Shows/${seriesId}/Episodes?UserId=${userId}&Fields=UserData&EnableImageTypes=Primary`;
|
||||
const episodesUrl = `${serverUrl}/Shows/${seriesId}/Episodes?UserId=${userId}&Fields=UserData`;
|
||||
const episodesRes = await fetch(episodesUrl, {
|
||||
headers: { "Authorization": `MediaBrowser Token="${token}"` }
|
||||
});
|
||||
@@ -104,12 +149,51 @@
|
||||
WARN('Could not determine season ID from episodes');
|
||||
}
|
||||
|
||||
// Render Season 1 section using renderCards
|
||||
// Check if Next Up section exists
|
||||
const nextUpSection = activePage?.querySelector('.nextUpSection');
|
||||
const nextUpItemsContainer = nextUpSection?.querySelector('.itemsContainer');
|
||||
|
||||
// Extract episode info from Next Up if it exists
|
||||
let targetEpisodeNumber = null;
|
||||
if (nextUpSection && nextUpItemsContainer) {
|
||||
LOG('Next Up section found, hiding nextUpSection');
|
||||
|
||||
// Get the existing card from Next Up and extract episode info
|
||||
const existingCard = nextUpItemsContainer.querySelector('.card');
|
||||
if (existingCard) {
|
||||
// Find the .cardText a element that contains the episode number
|
||||
const cardTextLinks = existingCard.querySelectorAll('.cardText a');
|
||||
for (const link of cardTextLinks) {
|
||||
const linkText = link.innerText || link.textContent;
|
||||
// Match SX:EY pattern (may be part of longer text like "S1:E5 - Episode Name")
|
||||
const match = linkText.match(/S(\d+):E(\d+)/);
|
||||
if (match) {
|
||||
targetEpisodeNumber = {
|
||||
season: parseInt(match[1], 10),
|
||||
episode: parseInt(match[2], 10)
|
||||
};
|
||||
LOG(`Found Next Up episode from DOM: S${targetEpisodeNumber.season}:E${targetEpisodeNumber.episode}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we couldn't find it from the DOM, fetch from API
|
||||
if (!targetEpisodeNumber) {
|
||||
LOG('Could not find episode number from DOM, fetching from API');
|
||||
targetEpisodeNumber = await fetchNextUpEpisode(seriesId);
|
||||
}
|
||||
|
||||
// Hide the entire Next Up section
|
||||
nextUpSection.style.display = 'none';
|
||||
}
|
||||
|
||||
// Render Season 1 section using renderCards (default behavior)
|
||||
const seasonSection = window.cardBuilder.renderCards(
|
||||
episodes,
|
||||
'Season 1',
|
||||
viewMoreUrl,
|
||||
false, // overflowCard
|
||||
true, // overflowCard
|
||||
null // cardFormat (use default)
|
||||
);
|
||||
|
||||
@@ -130,6 +214,72 @@
|
||||
// Prepend the Season 1 section right before #childrenCollapsible
|
||||
childrenCollapsible.parentNode.insertBefore(seasonSection, childrenCollapsible);
|
||||
|
||||
// Check if childrenCollapsible has only one child in its itemsContainer, and hide it if so
|
||||
const childrenItemsContainer = childrenCollapsible.querySelector('.itemsContainer');
|
||||
if (childrenItemsContainer) {
|
||||
LOG('childrenCollapsible contains only one child, hiding it');
|
||||
childrenCollapsible.style.display = 'none';
|
||||
}
|
||||
|
||||
// If we found a target episode from Next Up, scroll to it
|
||||
if (targetEpisodeNumber && scrollerContainer) {
|
||||
// Wait for cards to be rendered and positioned
|
||||
setTimeout(() => {
|
||||
const scrollerItemsContainer = scrollerContainer.querySelector('.itemsContainer');
|
||||
if (!scrollerItemsContainer) {
|
||||
WARN('Could not find itemsContainer in scroller');
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the card that matches the target episode number
|
||||
const cards = scrollerItemsContainer.querySelectorAll('.card');
|
||||
let targetCard = null;
|
||||
|
||||
for (const card of cards) {
|
||||
const cardTextLinks = card.querySelectorAll('.cardText a');
|
||||
for (const link of cardTextLinks) {
|
||||
const linkText = link.innerText || link.textContent;
|
||||
const match = linkText.match(/S(\d+):E(\d+)/);
|
||||
if (match &&
|
||||
parseInt(match[1], 10) === targetEpisodeNumber.season &&
|
||||
parseInt(match[2], 10) === targetEpisodeNumber.episode) {
|
||||
targetCard = card;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (targetCard) break;
|
||||
}
|
||||
|
||||
// Scroll to make the target card the leftmost visible element
|
||||
if (targetCard) {
|
||||
const itemsContainer = scrollerItemsContainer;
|
||||
const cardOffset = targetCard.offsetLeft;
|
||||
const scrollerPadding = parseInt(window.getComputedStyle(scrollerContainer).paddingLeft, 10) || 0;
|
||||
|
||||
// Calculate the translateX value (negative to move left)
|
||||
// The card offset minus padding gives us how much to translate
|
||||
const translateX = -(cardOffset - scrollerPadding);
|
||||
|
||||
// Update the transform property on the itemsContainer
|
||||
itemsContainer.style.transform = `translateX(${translateX}px)`;
|
||||
|
||||
// Enable the left scroll button so users can scroll back
|
||||
const scrollButtons = scrollerContainer.closest('.emby-scroller-container')?.querySelector('.emby-scrollbuttons');
|
||||
if (scrollButtons) {
|
||||
const leftButton = scrollButtons.querySelector('button[data-direction="left"]');
|
||||
if (leftButton) {
|
||||
leftButton.removeAttribute('disabled');
|
||||
LOG('Enabled left scroll button');
|
||||
}
|
||||
}
|
||||
|
||||
LOG(`Scrolled to target episode S${targetEpisodeNumber.season}:E${targetEpisodeNumber.episode} using translateX(${translateX}px)`);
|
||||
} else {
|
||||
WARN(`Could not find target episode card S${targetEpisodeNumber.season}:E${targetEpisodeNumber.episode}`);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Mark as flattened for this series to avoid duplicates
|
||||
if (activePage) {
|
||||
activePage.dataset.flattenedSeriesId = seriesId;
|
||||
@@ -154,7 +304,7 @@
|
||||
LOG('Registering flatten single season shows handler with KefinTweaksUtils');
|
||||
|
||||
window.KefinTweaksUtils.onViewPage(
|
||||
async (view, element, itemPromise) => {
|
||||
async (view, element, hash, itemPromise) => {
|
||||
// Only handle details pages
|
||||
const activePage = document.querySelector('.libraryPage:not(.hide)');
|
||||
if (!activePage) return;
|
||||
@@ -162,9 +312,9 @@
|
||||
// Remove any existing flattened section and reset flag when page changes
|
||||
const existingSection = activePage.querySelector('.flattened-season-section');
|
||||
if (existingSection) {
|
||||
existingSection.remove();
|
||||
LOG('Flattened section already exists, skipping');
|
||||
return;
|
||||
}
|
||||
delete activePage.dataset.flattenedSeriesId;
|
||||
|
||||
// Await the item promise to get the actual item data
|
||||
const item = await itemPromise;
|
||||
@@ -180,7 +330,6 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
pages: ['details']
|
||||
}
|
||||
);
|
||||
|
||||
+2
-3
@@ -89,7 +89,7 @@
|
||||
}
|
||||
|
||||
// Manually notify handlers for Header Tab navigation because it doesn't trigger normal Jellyfin navigation
|
||||
window.KefinTweaksUtils.notifyHandlers(currentPage, document);
|
||||
window.KefinTweaksUtils.notifyHandlers(currentPage, document, newHash);
|
||||
|
||||
// Replace history entry instead of adding new one
|
||||
const newUrl = window.location.origin + window.location.pathname + '#' + newHash.substring(1);
|
||||
@@ -343,8 +343,7 @@
|
||||
// Sync active tab state when page view changes
|
||||
syncActiveTabState();
|
||||
}, {
|
||||
pages: SUPPORTED_PAGES, // Only trigger for supported pages
|
||||
immediate: false
|
||||
pages: SUPPORTED_PAGES // Only trigger for supported pages
|
||||
});
|
||||
LOG('Registered onViewPage handler for syncActiveTabState');
|
||||
} else {
|
||||
|
||||
+6
-1
@@ -16,7 +16,7 @@
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.sections.homeSectionsContainer>div:has(div[data-positionticks]) {
|
||||
.sections.homeSectionsContainer>div:has(>.emby-scroller div[data-positionticks]) {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
@@ -86,4 +86,9 @@ div#discovery-loading-indicator:not(.show)::after {
|
||||
|
||||
#indexPage:not([data-discovery-ready]) div#discovery-loading-indicator:not(.show)::after {
|
||||
content: '';
|
||||
}
|
||||
|
||||
/* Ensure Editor's Choice Banner appears at top of home screen if enabled */
|
||||
.editorsChoiceContainer {
|
||||
order: 1 !important;
|
||||
}
|
||||
+227
-175
@@ -177,7 +177,6 @@
|
||||
// Set up page view monitoring
|
||||
if (window.KefinTweaksUtils) {
|
||||
window.KefinTweaksUtils.onViewPage(manageBodyClasses, {
|
||||
immediate: true,
|
||||
pages: []
|
||||
});
|
||||
LOG('Registered body class management for home page');
|
||||
@@ -771,22 +770,25 @@
|
||||
const token = apiClient.accessToken();
|
||||
const userId = apiClient.getCurrentUserId();
|
||||
const cache = new window.LocalStorageCache();
|
||||
const indexedDBCache = new window.IndexedDBCache();
|
||||
|
||||
try {
|
||||
LOG('Starting paginated fetch of movies for people data processing...');
|
||||
|
||||
let allMovies = [];
|
||||
let startIndex = 0;
|
||||
const limit = 500;
|
||||
let hasMoreData = true;
|
||||
let peopleMap = null; // Will hold raw (unfiltered) people data
|
||||
|
||||
// Load existing partial data if available
|
||||
const existingData = cache.get('movies_top_people');
|
||||
if (existingData && !existingData.isComplete) {
|
||||
const moviesProcessed = countMoviesFromPeopleData(existingData);
|
||||
// Load existing raw people data if available (for resuming) - use IndexedDB
|
||||
const existingRawData = await indexedDBCache.get('movies_top_people_raw', userId);
|
||||
if (existingRawData && !existingRawData.isComplete) {
|
||||
const moviesProcessed = existingRawData.moviesProcessedCount;
|
||||
if (moviesProcessed > 0) {
|
||||
startIndex = moviesProcessed;
|
||||
LOG(`Resuming from existing data: ${moviesProcessed} movies already processed`);
|
||||
// Reconstruct Map from cached array
|
||||
peopleMap = new Map(existingRawData.peopleData || []);
|
||||
LOG(`Resuming from existing data: ${moviesProcessed} movies already processed, ${peopleMap.size} people in cache`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -813,26 +815,29 @@
|
||||
hasMoreData = false;
|
||||
LOG('No more movies to fetch');
|
||||
} else {
|
||||
allMovies = allMovies.concat(movies);
|
||||
// Process new movies and merge with existing people data
|
||||
peopleMap = processPeopleDataRaw(movies, peopleMap);
|
||||
startIndex += movies.length;
|
||||
|
||||
LOG(`Fetched ${movies.length} movies, total: ${allMovies.length}`);
|
||||
LOG(`Fetched ${movies.length} movies, total processed: ${startIndex}, people tracked: ${peopleMap ? peopleMap.size : 0}`);
|
||||
|
||||
// Process and cache partial data after each chunk
|
||||
const partialPeopleData = processPeopleData(allMovies);
|
||||
if (partialPeopleData) {
|
||||
// Mark as incomplete and store movie count for resuming
|
||||
partialPeopleData.isComplete = movies.length < limit; // Complete if we got less than limit
|
||||
partialPeopleData.moviesProcessedCount = allMovies.length;
|
||||
|
||||
// Filter for display (but cache raw data)
|
||||
const filteredData = filterPeopleData(peopleMap);
|
||||
if (filteredData) {
|
||||
// Update the global moviesTopPeople immediately so discovery sections can use it
|
||||
moviesTopPeople = partialPeopleData;
|
||||
|
||||
// Cache partial data
|
||||
cache.set('movies_top_people', partialPeopleData, userId, 7 * 24 * 60 * 60 * 1000);
|
||||
LOG(`Updated moviesTopPeople with partial data: ${partialPeopleData.actors.length} actors, ${partialPeopleData.directors.length} directors, ${partialPeopleData.writers.length} writers (${allMovies.length} movies processed)`);
|
||||
moviesTopPeople = filteredData;
|
||||
LOG(`Updated moviesTopPeople with partial data: ${filteredData.actors.length} actors, ${filteredData.directors.length} directors, ${filteredData.writers.length} writers (${startIndex} movies processed)`);
|
||||
}
|
||||
|
||||
// Cache RAW (unfiltered) data for resuming - use IndexedDB for large data
|
||||
// Convert Map to array for JSON serialization
|
||||
const rawDataToCache = {
|
||||
isComplete: movies.length < limit,
|
||||
moviesProcessedCount: startIndex,
|
||||
peopleData: Array.from(peopleMap.entries())
|
||||
};
|
||||
await indexedDBCache.set('movies_top_people_raw', rawDataToCache, userId, 7 * 24 * 60 * 60 * 1000);
|
||||
|
||||
// If we got less than the limit, we've reached the end
|
||||
if (movies.length < limit) {
|
||||
hasMoreData = false;
|
||||
@@ -842,20 +847,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Final processing of all data
|
||||
LOG(`Final processing of ${allMovies.length} movies...`);
|
||||
const finalPeopleData = processPeopleData(allMovies);
|
||||
// Final processing: filter the complete raw data
|
||||
LOG(`Final filtering of people data from ${startIndex} movies...`);
|
||||
const finalPeopleData = filterPeopleData(peopleMap);
|
||||
|
||||
if (finalPeopleData) {
|
||||
// Mark as complete and remove movies count to save space
|
||||
// Mark as complete
|
||||
finalPeopleData.isComplete = true;
|
||||
delete finalPeopleData.moviesProcessedCount;
|
||||
|
||||
// Update global variable
|
||||
moviesTopPeople = finalPeopleData;
|
||||
|
||||
// Cache final data
|
||||
cache.set('movies_top_people', finalPeopleData, userId, 7 * 24 * 60 * 60 * 1000);
|
||||
// Cache final filtered data (for quick loading) - use IndexedDBCache for larger data with items
|
||||
await indexedDBCache.set('movies_top_people', finalPeopleData, userId, 7 * 24 * 60 * 60 * 1000);
|
||||
|
||||
// Also remove raw cache since we're complete (optional, saves space)
|
||||
await indexedDBCache.clear('movies_top_people_raw', userId);
|
||||
|
||||
LOG(`Final cached people data: ${finalPeopleData.actors.length} actors, ${finalPeopleData.directors.length} directors, ${finalPeopleData.writers.length} writers`);
|
||||
}
|
||||
|
||||
@@ -868,53 +876,109 @@
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes people data from movies array
|
||||
* @param {Array} movies - Array of movie objects with People data
|
||||
* @returns {Object|null} - Processed people data or null if failed
|
||||
* Extracts necessary item data for caching (similar to watchlist format)
|
||||
* @param {Object} item - Full Jellyfin item object
|
||||
* @returns {Object} - Optimized item data for storage
|
||||
*/
|
||||
function processPeopleData(movies) {
|
||||
try {
|
||||
const peopleMap = new Map();
|
||||
|
||||
movies.forEach(movie => {
|
||||
if (movie.People) {
|
||||
movie.People.forEach(person => {
|
||||
const key = person.Id;
|
||||
if (!peopleMap.has(key)) {
|
||||
peopleMap.set(key, {
|
||||
id: person.Id,
|
||||
name: person.Name,
|
||||
directorCount: 0,
|
||||
writerCount: 0,
|
||||
actorCount: 0,
|
||||
directorItemIds: [],
|
||||
writerItemIds: [],
|
||||
actorItemIds: []
|
||||
});
|
||||
}
|
||||
|
||||
const personData = peopleMap.get(key);
|
||||
if (person.Type === 'Director') {
|
||||
personData.directorCount++;
|
||||
if (!personData.directorItemIds.includes(movie.Id)) {
|
||||
personData.directorItemIds.push(movie.Id);
|
||||
}
|
||||
} else if (person.Type === 'Writer') {
|
||||
personData.writerCount++;
|
||||
if (!personData.writerItemIds.includes(movie.Id)) {
|
||||
personData.writerItemIds.push(movie.Id);
|
||||
}
|
||||
} else if (person.Type === 'Actor') {
|
||||
personData.actorCount++;
|
||||
if (!personData.actorItemIds.includes(movie.Id)) {
|
||||
personData.actorItemIds.push(movie.Id);
|
||||
}
|
||||
}
|
||||
});
|
||||
function extractItemDataForCache(item) {
|
||||
return {
|
||||
Id: item.Id,
|
||||
Type: item.Type,
|
||||
MediaType: item.MediaType,
|
||||
Name: item.Name,
|
||||
SeriesId: item.SeriesId,
|
||||
SeriesName: item.SeriesName,
|
||||
SeriesPrimaryImageTag: item.SeriesPrimaryImageTag,
|
||||
ParentBackdropItemId: item.ParentBackdropItemId,
|
||||
ParentBackdropImageTags: item.ParentBackdropImageTags,
|
||||
ParentThumbImageTag: item.ParentPrimaryImageTags,
|
||||
IndexNumber: item.IndexNumber,
|
||||
ParentIndexNumber: item.ParentIndexNumber,
|
||||
ImageTags: item.ImageTags,
|
||||
BackdropImageTags: item.BackdropImageTags,
|
||||
PremiereDate: item.PremiereDate,
|
||||
ProductionYear: item.ProductionYear,
|
||||
UserData: {
|
||||
IsFavorite: item.UserData?.IsFavorite || false,
|
||||
Played: item.UserData?.Played || false,
|
||||
LastPlayedDate: item.UserData?.LastPlayedDate || null,
|
||||
PlayCount: item.UserData?.PlayCount || 0,
|
||||
Likes: item.UserData?.Likes || false
|
||||
}
|
||||
});
|
||||
|
||||
// Filter and sort people by count, taking top 100 per category
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes people data from movies array and returns RAW data (all people, unfiltered)
|
||||
* @param {Array} movies - Array of movie objects with People data
|
||||
* @param {Map} existingPeopleMap - Optional existing people map to merge with
|
||||
* @returns {Map|null} - Map of all people data (unfiltered) or null if failed
|
||||
*/
|
||||
function processPeopleDataRaw(movies, existingPeopleMap = null) {
|
||||
try {
|
||||
const peopleMap = existingPeopleMap ? new Map(existingPeopleMap) : new Map();
|
||||
|
||||
movies.forEach(movie => {
|
||||
// Extract item data for caching
|
||||
const itemData = extractItemDataForCache(movie);
|
||||
|
||||
if (movie.People) {
|
||||
movie.People.forEach(person => {
|
||||
const key = person.Id;
|
||||
if (!peopleMap.has(key)) {
|
||||
peopleMap.set(key, {
|
||||
id: person.Id,
|
||||
name: person.Name,
|
||||
directorCount: 0,
|
||||
writerCount: 0,
|
||||
actorCount: 0,
|
||||
directorItems: [],
|
||||
writerItems: [],
|
||||
actorItems: []
|
||||
});
|
||||
}
|
||||
|
||||
const personData = peopleMap.get(key);
|
||||
if (person.Type === 'Director') {
|
||||
personData.directorCount++;
|
||||
// Check if item already exists by Id
|
||||
if (!personData.directorItems.some(item => item.Id === movie.Id)) {
|
||||
personData.directorItems.push(itemData);
|
||||
}
|
||||
} else if (person.Type === 'Writer') {
|
||||
personData.writerCount++;
|
||||
if (!personData.writerItems.some(item => item.Id === movie.Id)) {
|
||||
personData.writerItems.push(itemData);
|
||||
}
|
||||
} else if (person.Type === 'Actor') {
|
||||
personData.actorCount++;
|
||||
if (!personData.actorItems.some(item => item.Id === movie.Id)) {
|
||||
personData.actorItems.push(itemData);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return peopleMap;
|
||||
|
||||
} catch (err) {
|
||||
ERR('Failed to process people data:', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters and sorts raw people data by minPeopleAppearances, returning top 100 per category
|
||||
* @param {Map} peopleMap - Map of all people data (unfiltered)
|
||||
* @returns {Object|null} - Filtered and sorted people data or null if failed
|
||||
*/
|
||||
function filterPeopleData(peopleMap) {
|
||||
try {
|
||||
if (!peopleMap) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const allPeople = Array.from(peopleMap.values());
|
||||
|
||||
const topDirectors = allPeople
|
||||
@@ -925,7 +989,7 @@
|
||||
id: person.id,
|
||||
name: person.name,
|
||||
count: person.directorCount,
|
||||
itemIds: person.directorItemIds
|
||||
items: person.directorItems || []
|
||||
}));
|
||||
|
||||
const topWriters = allPeople
|
||||
@@ -936,7 +1000,7 @@
|
||||
id: person.id,
|
||||
name: person.name,
|
||||
count: person.writerCount,
|
||||
itemIds: person.writerItemIds
|
||||
items: person.writerItems || []
|
||||
}));
|
||||
|
||||
const topActors = allPeople
|
||||
@@ -947,7 +1011,7 @@
|
||||
id: person.id,
|
||||
name: person.name,
|
||||
count: person.actorCount,
|
||||
itemIds: person.actorItemIds
|
||||
items: person.actorItems || []
|
||||
}));
|
||||
|
||||
return {
|
||||
@@ -957,11 +1021,21 @@
|
||||
};
|
||||
|
||||
} catch (err) {
|
||||
ERR('Failed to process people data:', err);
|
||||
ERR('Failed to filter people data:', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes people data from movies array (legacy function, now uses raw processing + filtering)
|
||||
* @param {Array} movies - Array of movie objects with People data
|
||||
* @returns {Object|null} - Processed people data or null if failed
|
||||
*/
|
||||
function processPeopleData(movies) {
|
||||
const peopleMap = processPeopleDataRaw(movies);
|
||||
return filterPeopleData(peopleMap);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initializes the people cache in the background
|
||||
@@ -979,43 +1053,48 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const cache = new window.LocalStorageCache();
|
||||
const indexedDBCache = new window.IndexedDBCache();
|
||||
const apiClient = window.ApiClient;
|
||||
const userId = apiClient.getCurrentUserId();
|
||||
|
||||
// Check if we have valid cached data
|
||||
if (cache.isCacheValid('movies_top_people')) {
|
||||
const cachedData = cache.get('movies_top_people');
|
||||
// First, check if we have valid complete filtered data (in IndexedDBCache)
|
||||
if (await indexedDBCache.isCacheValid('movies_top_people', userId)) {
|
||||
const cachedData = await indexedDBCache.get('movies_top_people', userId);
|
||||
|
||||
// Check if data is complete
|
||||
if (cachedData.isComplete) {
|
||||
if (cachedData && cachedData.isComplete) {
|
||||
moviesTopPeople = cachedData;
|
||||
isPeopleCacheComplete = true;
|
||||
LOG('Loaded complete top people data from cache');
|
||||
LOG('Loaded complete top people data from IndexedDB cache');
|
||||
return;
|
||||
} else if (cachedData.moviesProcessedCount) {
|
||||
// Use the stored count for resuming
|
||||
const moviesProcessed = cachedData.moviesProcessedCount;
|
||||
if (moviesProcessed > 0 && moviesProcessed % 500 === 0) {
|
||||
// We have a complete set of 500-item chunks, continue loading
|
||||
moviesTopPeople = cachedData;
|
||||
isPeopleCacheComplete = false;
|
||||
LOG(`Loaded partial top people data from cache (${moviesProcessed} movies), will continue fetching...`);
|
||||
} else if (moviesProcessed > 0) {
|
||||
// We have incomplete data, use what we have but don't continue loading
|
||||
moviesTopPeople = cachedData;
|
||||
isPeopleCacheComplete = true;
|
||||
LOG(`Loaded incomplete top people data from cache (${moviesProcessed} movies), not divisible by 500, using as-is`);
|
||||
return;
|
||||
} else {
|
||||
// We have partial data without count info, load it and continue fetching
|
||||
moviesTopPeople = cachedData;
|
||||
isPeopleCacheComplete = false;
|
||||
LOG('Loaded partial top people data from cache, will continue fetching...');
|
||||
}
|
||||
}
|
||||
|
||||
// If no complete data, check for raw partial data (in IndexedDBCache)
|
||||
if (await indexedDBCache.isCacheValid('movies_top_people_raw', userId)) {
|
||||
const rawCachedData = await indexedDBCache.get('movies_top_people_raw', userId);
|
||||
|
||||
if (rawCachedData && rawCachedData.peopleData) {
|
||||
// Reconstruct Map and filter for immediate display
|
||||
const peopleMap = new Map(rawCachedData.peopleData);
|
||||
const filteredData = filterPeopleData(peopleMap);
|
||||
|
||||
if (filteredData) {
|
||||
moviesTopPeople = filteredData;
|
||||
isPeopleCacheComplete = rawCachedData.isComplete || false;
|
||||
LOG(`Loaded partial top people data from raw cache (${rawCachedData.moviesProcessedCount || 0} movies), ${filteredData.actors.length} actors, ${filteredData.directors.length} directors, ${filteredData.writers.length} writers`);
|
||||
|
||||
// If incomplete, we'll continue fetching in background
|
||||
if (!isPeopleCacheComplete) {
|
||||
// Continue in background
|
||||
} else {
|
||||
// It's marked complete but wasn't in filtered cache, update filtered cache
|
||||
filteredData.isComplete = true;
|
||||
await indexedDBCache.set('movies_top_people', filteredData, userId, 7 * 24 * 60 * 60 * 1000);
|
||||
await indexedDBCache.clear('movies_top_people_raw', userId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No moviesProcessedCount stored, treat as incomplete and continue fetching
|
||||
moviesTopPeople = cachedData;
|
||||
isPeopleCacheComplete = false;
|
||||
LOG('Loaded partial top people data from cache (no count stored), will continue fetching...');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1745,7 +1824,7 @@
|
||||
// Render the scrollable container
|
||||
const scrollableContainer = window.cardBuilder.renderCards(
|
||||
movies,
|
||||
'New Movies',
|
||||
'Recently Released Movies',
|
||||
null,
|
||||
true
|
||||
);
|
||||
@@ -1788,7 +1867,7 @@
|
||||
// Render the scrollable container
|
||||
const scrollableContainer = window.cardBuilder.renderCards(
|
||||
episodes,
|
||||
'New Episodes',
|
||||
'Recently Aired Episodes',
|
||||
null,
|
||||
true
|
||||
);
|
||||
@@ -1908,7 +1987,7 @@
|
||||
// Render the scrollable container with the network/studio items
|
||||
const scrollableContainer = window.cardBuilder.renderCards(
|
||||
limitedNetworks,
|
||||
'Popular TV Networks',
|
||||
'Popular TV Studios',
|
||||
null,
|
||||
true,
|
||||
'backdrop'
|
||||
@@ -1984,78 +2063,63 @@
|
||||
return null;
|
||||
})(),
|
||||
|
||||
// 2. Directed by [Director] (async - fetch actual items)
|
||||
// 2. Directed by [Director] (use cached items directly)
|
||||
(async () => {
|
||||
if (moviesTopPeople && moviesTopPeople.directors.length > 0) {
|
||||
const randomDirector = getRandomItem(moviesTopPeople.directors, renderedDirectors);
|
||||
if (randomDirector && randomDirector.itemIds && randomDirector.itemIds.length > 0) {
|
||||
// Validate that itemIds contain actual valid IDs (not undefined/null)
|
||||
const validItemIds = randomDirector.itemIds.filter(id => id && typeof id === 'string' && id.trim() !== '');
|
||||
if (validItemIds.length > 0) {
|
||||
// Fetch actual items from API
|
||||
const shuffledIds = shuffleArray(validItemIds).slice(0, 16);
|
||||
const items = await fetchItemsByIds(shuffledIds);
|
||||
|
||||
if (items && items.length > 0) {
|
||||
return {
|
||||
type: 'director',
|
||||
data: randomDirector,
|
||||
items: items,
|
||||
viewMoreUrl: `#/details?id=${randomDirector.id}&serverId=${ApiClient.serverId()}`
|
||||
};
|
||||
}
|
||||
if (randomDirector && randomDirector.items && randomDirector.items.length > 0) {
|
||||
// Use cached items directly - no need to fetch from API
|
||||
const shuffledItems = shuffleArray(randomDirector.items).slice(0, 16);
|
||||
|
||||
if (shuffledItems && shuffledItems.length > 0) {
|
||||
return {
|
||||
type: 'director',
|
||||
data: randomDirector,
|
||||
items: shuffledItems,
|
||||
viewMoreUrl: `#/details?id=${randomDirector.id}&serverId=${ApiClient.serverId()}`
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
})(),
|
||||
|
||||
// 3. Written by [Writer] (async - fetch actual items)
|
||||
// 3. Written by [Writer] (use cached items directly)
|
||||
(async () => {
|
||||
if (moviesTopPeople && moviesTopPeople.writers.length > 0) {
|
||||
const randomWriter = getRandomItem(moviesTopPeople.writers, renderedWriters);
|
||||
if (randomWriter && randomWriter.itemIds && randomWriter.itemIds.length > 0) {
|
||||
// Validate that itemIds contain actual valid IDs (not undefined/null)
|
||||
const validItemIds = randomWriter.itemIds.filter(id => id && typeof id === 'string' && id.trim() !== '');
|
||||
if (validItemIds.length > 0) {
|
||||
// Fetch actual items from API
|
||||
const shuffledIds = shuffleArray(validItemIds).slice(0, 16);
|
||||
const items = await fetchItemsByIds(shuffledIds);
|
||||
|
||||
if (items && items.length > 0) {
|
||||
return {
|
||||
type: 'writer',
|
||||
data: randomWriter,
|
||||
items: items,
|
||||
viewMoreUrl: `#/details?id=${randomWriter.id}&serverId=${ApiClient.serverId()}`
|
||||
};
|
||||
}
|
||||
if (randomWriter && randomWriter.items && randomWriter.items.length > 0) {
|
||||
// Use cached items directly - no need to fetch from API
|
||||
const shuffledItems = shuffleArray(randomWriter.items).slice(0, 16);
|
||||
|
||||
if (shuffledItems && shuffledItems.length > 0) {
|
||||
return {
|
||||
type: 'writer',
|
||||
data: randomWriter,
|
||||
items: shuffledItems,
|
||||
viewMoreUrl: `#/details?id=${randomWriter.id}&serverId=${ApiClient.serverId()}`
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
})(),
|
||||
|
||||
// 4. Starring [Actor] (async - fetch actual items)
|
||||
// 4. Starring [Actor] (use cached items directly)
|
||||
(async () => {
|
||||
if (moviesTopPeople && moviesTopPeople.actors.length > 0) {
|
||||
const randomActor = getRandomItem(moviesTopPeople.actors, renderedActors);
|
||||
if (randomActor && randomActor.itemIds && randomActor.itemIds.length > 0) {
|
||||
// Validate that itemIds contain actual valid IDs (not undefined/null)
|
||||
const validItemIds = randomActor.itemIds.filter(id => id && typeof id === 'string' && id.trim() !== '');
|
||||
if (validItemIds.length > 0) {
|
||||
// Fetch actual items from API
|
||||
const shuffledIds = shuffleArray(validItemIds).slice(0, 16);
|
||||
const items = await fetchItemsByIds(shuffledIds);
|
||||
|
||||
if (items && items.length > 0) {
|
||||
return {
|
||||
type: 'actor',
|
||||
data: randomActor,
|
||||
items: items,
|
||||
viewMoreUrl: `#/details?id=${randomActor.id}&serverId=${ApiClient.serverId()}`
|
||||
};
|
||||
}
|
||||
if (randomActor && randomActor.items && randomActor.items.length > 0) {
|
||||
// Use cached items directly - no need to fetch from API
|
||||
const shuffledItems = shuffleArray(randomActor.items).slice(0, 16);
|
||||
|
||||
if (shuffledItems && shuffledItems.length > 0) {
|
||||
return {
|
||||
type: 'actor',
|
||||
data: randomActor,
|
||||
items: shuffledItems,
|
||||
viewMoreUrl: `#/details?id=${randomActor.id}&serverId=${ApiClient.serverId()}`
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3526,19 +3590,7 @@
|
||||
}
|
||||
|
||||
// Check if sections are already rendered
|
||||
const hasRenderedCustomSections = customHomeSections.some(section =>
|
||||
homeSectionsContainer.querySelector(`[data-custom-section-id="${section.id}"]`)
|
||||
);
|
||||
|
||||
const hasRenderedNewAndTrendingSections = (enableNewMovies && homeSectionsContainer.querySelector('[data-custom-section-id="new-movies"]')) ||
|
||||
(enableNewEpisodes && homeSectionsContainer.querySelector('[data-custom-section-id="new-episodes"]')) ||
|
||||
(enableTrending && homeSectionsContainer.querySelector('[data-custom-section-id="trending-movies"]'));
|
||||
|
||||
const hasRenderedDiscoverySections = (enableWatchlist && homeSectionsContainer.querySelector('[data-custom-section-id="watchlist"]')) ||
|
||||
(enableDiscovery && homeSectionsContainer.querySelector('[data-custom-section-id="watched-movie"]')) ||
|
||||
(enableSeasonal && homeSectionsContainer.querySelector('[data-custom-section-id="halloween-movies"]'));
|
||||
|
||||
if (hasRenderedCustomSections || hasRenderedNewAndTrendingSections || hasRenderedDiscoverySections) {
|
||||
if (homeSectionsContainer.dataset.customSectionsRendered === 'true') {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3546,6 +3598,7 @@
|
||||
|
||||
// Set processing flag to prevent parallel execution
|
||||
isProcessing = true;
|
||||
homeSectionsContainer.dataset.customSectionsRendered = 'true';
|
||||
|
||||
try {
|
||||
LOG('Starting parallel initialization of home screen sections...');
|
||||
@@ -3605,7 +3658,6 @@
|
||||
ERR('Home screen page change handler failed:', err);
|
||||
}
|
||||
}, {
|
||||
immediate: true,
|
||||
pages: ['home', 'home.html']
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -194,7 +194,7 @@
|
||||
LOG('Registering collections handler with KefinTweaksUtils');
|
||||
|
||||
window.KefinTweaksUtils.onViewPage(
|
||||
async (view, element, itemPromise) => {
|
||||
async (view, element, hash, itemPromise) => {
|
||||
// Only handle details pages
|
||||
const activePage = document.querySelector('.libraryPage:not(.hide)');
|
||||
if (!activePage) return;
|
||||
@@ -210,7 +210,6 @@
|
||||
}, 100);
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
pages: ['details']
|
||||
}
|
||||
);
|
||||
|
||||
+3
-1
@@ -18,7 +18,8 @@
|
||||
//
|
||||
// INSTALLATION:
|
||||
// ============
|
||||
// 0. Install required prerequisites listed above
|
||||
// 0a. Install required prerequisites listed above
|
||||
// 0b. If you are using JellyfinEnhanced, you MUST disable the "Watchlist" feature from the plugin settings
|
||||
// 1. Add a new script to your JS Injector plugin
|
||||
// 2. Copy this entire file into the new script
|
||||
// (Optional) Enable/disable features, or customize their configuration by editing the script below
|
||||
@@ -61,6 +62,7 @@
|
||||
homeScreen: true, // Custom home screen sections and discovery engine
|
||||
search: true, // Enhanced search with better UI and Jellyseerr integration
|
||||
// REQUIREMENTS: Watchlist requires Custom Tabs plugin https://github.com/IAmParadox27/jellyfin-plugin-custom-tabs
|
||||
// If you are using JellyfinEnhanced, you MUST disable the "Watchlist" feature from the plugin settings
|
||||
watchlist: true, // Complete watchlist management with progress tracking
|
||||
|
||||
// UI and navigation improvements
|
||||
|
||||
+1
-2
@@ -227,7 +227,7 @@
|
||||
LOG('Registering playlist handler with KefinTweaksUtils');
|
||||
|
||||
// Register handler for details pages
|
||||
window.KefinTweaksUtils.onViewPage(async (view, element, itemPromise) => {
|
||||
window.KefinTweaksUtils.onViewPage(async (view, element, hash, itemPromise) => {
|
||||
// Await the item promise to get the actual item data
|
||||
const item = await itemPromise;
|
||||
if (item && item.Type === 'Playlist') {
|
||||
@@ -235,7 +235,6 @@
|
||||
return;
|
||||
}
|
||||
}, {
|
||||
immediate: true,
|
||||
pages: ['details']
|
||||
});
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
|
||||
// config
|
||||
const CORE_TYPES = ['Movie', 'Series', 'Episode', 'Person'];
|
||||
const MUSIC_TYPES = ['MusicAlbum', 'Audio', 'Artist'];
|
||||
const MUSIC_TYPES = ['MusicAlbum', 'Audio', 'Artist', 'MusicVideo'];
|
||||
const BOOKS_TYPES = ['Book', 'AudioBook'];
|
||||
const OTHER_TYPES = ['Playlist', 'Photo', 'PhotoAlbum', 'TvChannel', 'LiveTvProgram', 'BoxSet'];
|
||||
|
||||
@@ -677,7 +677,6 @@
|
||||
}
|
||||
}, 100);
|
||||
}, {
|
||||
immediate: true,
|
||||
pages: ['search']
|
||||
});
|
||||
|
||||
|
||||
+8
-4
@@ -282,15 +282,19 @@
|
||||
|
||||
// Register for the display preferences page
|
||||
unregisterDisplayPreferencesHandler = window.KefinTweaksUtils.onViewPage(handleDisplayPreferencesPage, {
|
||||
pages: ['mypreferencesdisplay'],
|
||||
immediate: true
|
||||
pages: ['mypreferencesdisplay']
|
||||
});
|
||||
|
||||
// Register for all pages to add the appearance button
|
||||
unregisterAnyPageHandler = window.KefinTweaksUtils.onViewPage(handleAnyPage, {
|
||||
pages: [],
|
||||
immediate: true
|
||||
pages: []
|
||||
});
|
||||
|
||||
/* if (window.location.hash.includes('mypreferencesdisplay')) {
|
||||
handleDisplayPreferencesPage();
|
||||
}
|
||||
|
||||
handleAnyPage(); */
|
||||
|
||||
LOG('SkinManager handlers registered');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
.material-icons.book:before {
|
||||
content: "\e062";
|
||||
}
|
||||
.material-icons.newspaper:before {
|
||||
content: "\e34c";
|
||||
}
|
||||
.material-icons.quiz:before {
|
||||
content: "\e34b";
|
||||
}
|
||||
.material-icons.person_add:before {
|
||||
content: '\e1a2';
|
||||
}
|
||||
.material-icons.help:before {
|
||||
content: '\e0ff';
|
||||
}
|
||||
.material-icons.bookmark:before, .material-icons.bookmark_border:before, .material-icons.watchlist:before {
|
||||
content: '\e064';
|
||||
}
|
||||
.material-icons.bookmark_border:before {
|
||||
content: '\e064';
|
||||
}
|
||||
.material-icons.thumb_up:before {
|
||||
content: '\e18a';
|
||||
}
|
||||
.material-icons.palette:before {
|
||||
content: '\e1dd';
|
||||
}
|
||||
.material-icons.star:before {
|
||||
content: '\e17a';
|
||||
}
|
||||
.material-icons.sort:before {
|
||||
content: '\e04b';
|
||||
}
|
||||
.material-icons.arrow_drop_down:before {
|
||||
content: '\e071';
|
||||
}
|
||||
.material-icons.play_circle:before {
|
||||
content: '\e084';
|
||||
}
|
||||
.material-icons.check_circle_outline:before {
|
||||
content: '\e226';
|
||||
}
|
||||
.material-icons.favorite_border:before {
|
||||
content: '\e0f6';
|
||||
}
|
||||
|
||||
/* Lazy workaround for the pause button on the media bar plugin */
|
||||
.pause-button .material-icons {
|
||||
font-family: var(--iconPack) !important;
|
||||
}
|
||||
|
||||
/* Jellyfin Enhanced Random Item Button */
|
||||
button#randomItemButton>i {
|
||||
display: none;
|
||||
}
|
||||
|
||||
button#randomItemButton:before {
|
||||
content: '\e28c';
|
||||
font-family: lucide !important;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
word-wrap: normal;
|
||||
white-space: nowrap;
|
||||
direction: ltr;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
font-feature-settings: "liga";
|
||||
}
|
||||
@@ -38,6 +38,16 @@
|
||||
],
|
||||
colorSchemes: []
|
||||
},
|
||||
{
|
||||
name: 'NeutralFin',
|
||||
author: 'KartoffelChipss',
|
||||
url: [
|
||||
'https://cdn.jsdelivr.net/gh/KartoffelChipss/NeutralFin@latest/theme/neutralfin-minified.css',
|
||||
'https://cdn.jsdelivr.net/gh/KartoffelChipss/Jellyfin-Lucide@main/theme/jellyfin-lucide.css',
|
||||
`${KefinTweaksConfig.scriptRoot}skins/neutralfin-kefin.css`
|
||||
],
|
||||
colorSchemes: []
|
||||
},
|
||||
{
|
||||
name: 'Jellypane',
|
||||
author: 'tedhinklater',
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
}
|
||||
|
||||
// Call our registered handlers
|
||||
notifyHandlers(args[0], args[1]);
|
||||
notifyHandlers(args[0], args[1], window.location.hash);
|
||||
};
|
||||
|
||||
LOG('Hooked into Emby.Page.onViewShow');
|
||||
@@ -68,7 +68,6 @@
|
||||
const handlerConfig = {
|
||||
callback,
|
||||
options: {
|
||||
immediate: false, // Call immediately if on matching page
|
||||
pages: [], // Specific pages to watch (empty = all pages)
|
||||
...options
|
||||
}
|
||||
@@ -77,18 +76,15 @@
|
||||
handlers.push(handlerConfig);
|
||||
LOG(`Registered onViewPage handler (total: ${handlers.length})`);
|
||||
|
||||
// Call immediately if requested and we're on a matching page
|
||||
// Test if this is causing issues
|
||||
if (handlerConfig.options.immediate && false) {
|
||||
const currentView = getCurrentView();
|
||||
if (currentView && shouldCallHandler(handlerConfig, currentView)) {
|
||||
try {
|
||||
// Pass the promise (not awaited) for consistency with notifyHandlers
|
||||
const itemPromise = getCurrentItem();
|
||||
callback(currentView, document, itemPromise);
|
||||
} catch (err) {
|
||||
ERR('Error in immediate handler call:', err);
|
||||
}
|
||||
const currentView = getCurrentView();
|
||||
if (currentView && shouldCallHandler(handlerConfig, currentView)) {
|
||||
try {
|
||||
// Pass the promise (not awaited) for consistency with notifyHandlers
|
||||
const itemPromise = getCurrentItem();
|
||||
callback(currentView, document, window.location.hash, itemPromise);
|
||||
} catch (err) {
|
||||
ERR('Error in immediate handler call:', err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,7 +118,7 @@
|
||||
* @returns {Promise<Object|null>} The item object or null if not found
|
||||
*/
|
||||
async function fetchItemById(itemId) {
|
||||
if (!itemId || !window.ApiClient || !window.ApiClient.getItem || !window.ApiClient.getCurrentUserId) {
|
||||
if (!itemId || !window.ApiClient || !window.ApiClient.getItem || !window.ApiClient.getCurrentUserId || !window.ApiClient._loggedIn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -186,7 +182,7 @@
|
||||
* @param {string} view - The view name
|
||||
* @param {Element} element - The view element
|
||||
*/
|
||||
function notifyHandlers(view, element) {
|
||||
function notifyHandlers(view, element, hash) {
|
||||
// Clear cache when view changes to ensure fresh data
|
||||
const currentItemId = getItemIdFromUrl();
|
||||
if (cachedItemId !== currentItemId) {
|
||||
@@ -206,7 +202,7 @@
|
||||
if (shouldCallHandler(config, view)) {
|
||||
try {
|
||||
// Pass the promise as third parameter - handlers can await if needed
|
||||
config.callback(view, element, itemPromise);
|
||||
config.callback(view, element, hash, itemPromise);
|
||||
} catch (err) {
|
||||
ERR('Error in onViewPage handler:', err);
|
||||
}
|
||||
|
||||
@@ -1858,10 +1858,6 @@ div[data-tab="watchlist"] .loading-message {
|
||||
-webkit-tap-highlight-color: #fff0;
|
||||
}
|
||||
|
||||
.mainDetailButtons:has(.btnPlay.hide) .watchlist-icon, .mainDetailButtons:has(.btnPlay.hide) .btnRecommend {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.card:has(button[data-played="true"]) .watchlist-button,
|
||||
.card:not(:has(button[data-played])) .watchlist-button,
|
||||
.mainDetailButtons:has(button[data-played="true"]) .watchlist-icon,
|
||||
|
||||
+450
-163
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user