Improve Watchlist page handling items being watchlisted/unwatchlisted within a given session

This commit is contained in:
HighImKevin committed 2026-10-17 07:03:45 -04:00
1 parent a9b055e0e7
commit 0ac2c6415e
3 files changed
+103 -18

No files matched your search

+12
View File
@@ -344,9 +344,17 @@
cachedServerVersion: null
}
/**
* Fields used by progressive Watchlist sections. Must match getWatchlistQuery /
* invalidateWatchlistQueries so cache clear hits the same URL as fetch.
*/
const WATCHLIST_PROGRESSIVE_FIELDS =
'PrimaryImageAspectRatio,DateCreated,ProductionYear,PremiereDate,ProviderIds,UserData';
/**
* Build a single top-level Likes query def (shared by getWatchlistItems / getWatchlistQuery).
* Callers pass IncludeItemTypes (and other options) to narrow results.
* Default Fields match progressive sections so invalidate clears the fetched URL.
* @param {Object} options
* @returns {{ queryOptions: Object }}
*/
@@ -356,6 +364,7 @@
Recursive: true,
ImageTypeLimit: 1,
EnableImageTypes: 'Primary,Backdrop,Thumb',
Fields: WATCHLIST_PROGRESSIVE_FIELDS,
...options,
Filters: 'Likes'
}
@@ -1501,6 +1510,9 @@
return genreSyncScheduledPromise;
}
// Shared with watchlist progressive sections / invalidate (same Fields → same cache URL)
apiHelper.WATCHLIST_PROGRESSIVE_FIELDS = WATCHLIST_PROGRESSIVE_FIELDS;
// Expose apiHelper to global window object
window.apiHelper = apiHelper;
+16
View File
@@ -2226,12 +2226,28 @@
});
}
function updateCardFavoriteButtons(itemId, userData) {
if (!itemId || !userData || !('IsFavorite' in userData)) return;
const isFavorite = userData.IsFavorite === true;
itemIdDomVariants(itemId).forEach((variant) => {
const escapedId = escapeCssIdentForCardSync(variant);
document.querySelectorAll(
`[is="emby-ratingbutton"][data-id="${escapedId}"], button[data-isfavorite][data-id="${escapedId}"]`
).forEach((button) => {
button.setAttribute('data-isfavorite', isFavorite ? 'true' : 'false');
button.title = isFavorite ? 'Remove from favorites' : 'Add to favorites';
});
});
}
function applyUserDataEntriesToCards(entries, userId) {
if (!isPrimaryUserDataChange(userId)) return;
entries.forEach((entry) => {
if (entry?.ItemId) {
updateCardResumeAttributes(entry.ItemId, entry);
updateCardWatchlistButtons(entry.ItemId, entry);
updateCardFavoriteButtons(entry.ItemId, entry);
}
});
}
+75 -18
View File
@@ -3297,6 +3297,37 @@ In the Custom Tabs plugin, add a new tab with the following HTML content:
});
}
/**
* Keep paint-snapshot membership + header counts in sync on like/unlike without re-rendering sections.
* Ghost cards may remain in the DOM; counts reflect liked membership.
*/
function patchWatchlistItemsByTypeMembership(itemId, itemType, isAdded) {
if (!itemId) return;
const removeFromType = (type) => {
const list = watchlistItemsByType[type];
if (!Array.isArray(list) || !list.length) return false;
const next = list.filter((item) => item?.Id !== itemId);
if (next.length === list.length) return false;
watchlistItemsByType[type] = next;
return true;
};
if (isAdded) {
const type = itemType && isWatchlistSupportedType(itemType) ? itemType : null;
if (!type) return;
const list = watchlistItemsByType[type] || (watchlistItemsByType[type] = []);
if (list.some((item) => item?.Id === itemId)) return;
list.push({ Id: itemId });
} else if (itemType && isWatchlistSupportedType(itemType)) {
removeFromType(itemType);
} else {
WATCHLIST_SECTION_TYPES.forEach(({ type }) => removeFromType(type));
}
updateWatchlistHeaderStats();
}
function getWatchlistProgressiveHost() {
const watchlistSection = getWatchlistSection();
if (!watchlistSection) return null;
@@ -3330,9 +3361,11 @@ In the Custom Tabs plugin, add a new tab with the following HTML content:
throw new Error('apiHelper.getWatchlistQuery unavailable');
}
// Same Fields as apiHelper.buildWatchlistQueryDef so invalidate hits this fetch URL
const queryOptions = {
IncludeItemTypes: itemType,
Fields: 'PrimaryImageAspectRatio,DateCreated,ProductionYear,PremiereDate,ProviderIds,UserData'
Fields: apiHelper.WATCHLIST_PROGRESSIVE_FIELDS
|| 'PrimaryImageAspectRatio,DateCreated,ProductionYear,PremiereDate,ProviderIds,UserData'
};
const queryResult = await apiHelper.getWatchlistQuery(queryOptions, {
ttl: WATCHLIST_CACHE_TTL,
@@ -4974,31 +5007,52 @@ In the Custom Tabs plugin, add a new tab with the following HTML content:
alert(`Watchlist cleared: ${cleared} item(s) removed.`);
}
// Invalidate progressive watchlist cache and re-render when membership changes
/**
* Membership changed: update liked-Id cache + invalidate Likes query URL.
* Do not full-rebuild the Watchlist tab (avoids scroll jump / stale re-paint races).
* Cards stay as ghosts until next visit; cardBuilder owns overlay button UI via UserDataChanged.
*/
async function updateWatchlistCacheOnToggle(itemId, itemType, isAdded) {
try {
LOG(`Watchlist membership changed for ${itemType} ${itemId} (added=${isAdded}); refreshing progressive sections`);
LOG(`Watchlist membership changed for ${itemType || 'unknown'} ${itemId} (added=${isAdded}); invalidating query cache`);
setLikedIdInCache(itemId, isAdded === true);
syncOverlayWatchlistButtons();
patchWatchlistItemsByTypeMembership(itemId, itemType, isAdded === true);
if (itemType) {
if (itemType && isWatchlistSupportedType(itemType)) {
await window.apiHelper?.invalidateWatchlistQueries?.({ IncludeItemTypes: itemType });
} else {
await invalidateAllWatchlistQueries();
}
const watchlistSection = getWatchlistSection();
const watchlistVisible = !!watchlistSection
&& !watchlistSection.closest('.libraryPage.hide')
&& !!watchlistSection.querySelector('div[data-tab="watchlist"]:not([style*="display: none"])');
if (watchlistVisible || tabStates.watchlist.isDataFetched) {
await initWatchlistTab(true);
} else {
tabStates.watchlist.isDataFetched = false;
}
tabStates.watchlist.isDataFetched = false;
} catch (err) {
ERR('Error refreshing watchlist after toggle:', err);
ERR('Error refreshing watchlist cache after toggle:', err);
}
}
/**
* Sync liked-Id cache + invalidate progressive Likes queries from UserData.Likes.
* Idempotent for the Set when cardBuilder already applied Likes; always invalidate
* so a remote/Library like is visible on the next Watchlist visit.
*/
async function handleItemWatchlistChange(itemId, isLiked, itemType) {
if (!itemId || typeof isLiked !== 'boolean') return;
if (isItemInLikedIdCache(itemId) !== isLiked) {
setLikedIdInCache(itemId, isLiked);
}
// Keep header counts aligned even if liked-Id Set was already updated by cardBuilder
patchWatchlistItemsByTypeMembership(itemId, itemType, isLiked);
try {
if (itemType && isWatchlistSupportedType(itemType)) {
await window.apiHelper?.invalidateWatchlistQueries?.({ IncludeItemTypes: itemType });
} else {
await invalidateAllWatchlistQueries();
}
tabStates.watchlist.isDataFetched = false;
} catch (err) {
ERR('Error invalidating watchlist after Likes change:', err);
}
}
@@ -5022,8 +5076,11 @@ In the Custom Tabs plugin, add a new tab with the following HTML content:
for (const userData of entries) {
if (!userData?.ItemId) continue;
LOG(`Detected UserDataChanged for item: ${userData.ItemId}, Played: ${userData.Played}`);
handleItemWatchedStatusChange(userData.ItemId, userData.Played, userData.Likes).catch(err => {
LOG(`Detected UserDataChanged for item: ${userData.ItemId}, Played: ${userData.Played}, Likes: ${userData.Likes}`);
(async () => {
await handleItemWatchlistChange(userData.ItemId, userData.Likes);
await handleItemWatchedStatusChange(userData.ItemId, userData.Played, userData.Likes);
})().catch(err => {
ERR('Error handling UserDataChanged event:', err);
});
}