mirror of
https://github.com/ranaldsgift/KefinTweaks.git
synced 2026-11-05 15:52:47 +01:00
Jellyfin Watchlist functionality
This commit is contained in:
+308
@@ -0,0 +1,308 @@
|
||||
// Jellyfin Card Builder
|
||||
// This module provides a main entry point function to build Jellyfin cards
|
||||
// Usage: window.cardBuilder.buildCard(jellyfinItem)
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Main card builder object
|
||||
const cardBuilder = {
|
||||
/**
|
||||
* Main entry point function to build a Jellyfin card
|
||||
* @param {Object} item - The Jellyfin item object
|
||||
* @returns {HTMLElement} - The constructed card element
|
||||
*/
|
||||
buildCard: function(item) {
|
||||
return createJellyfinCardElement(item);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a Jellyfin card element from an item
|
||||
* @param {Object} item - The Jellyfin item object
|
||||
* @returns {HTMLElement} - The constructed card element
|
||||
*/
|
||||
function createJellyfinCardElement(item) {
|
||||
const serverId = ApiClient.serverId();
|
||||
const serverAddress = ApiClient.serverAddress();
|
||||
|
||||
// Determine card type based on 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';
|
||||
} else {
|
||||
// Default poster style for Movies, Series, etc.
|
||||
cardClass = 'overflowPortraitCard';
|
||||
padderClass = 'cardPadder-overflowPortrait';
|
||||
imageParams = 'fillHeight=446&fillWidth=297';
|
||||
}
|
||||
|
||||
// Create the main card container
|
||||
const card = document.createElement('div');
|
||||
card.className = `card ${cardClass} card-hoverable card-withuserdata`;
|
||||
card.setAttribute('data-index', '0');
|
||||
card.setAttribute('data-isfolder', item.Type === 'MusicAlbum' || item.Type === 'Artist' ? 'true' : 'false');
|
||||
card.setAttribute('data-serverid', serverId);
|
||||
card.setAttribute('data-id', item.Id);
|
||||
card.setAttribute('data-type', item.Type);
|
||||
card.setAttribute('data-mediatype', item.MediaType || 'Video');
|
||||
card.setAttribute('data-prefix', item.Name?.startsWith('The ') ? 'THE' : '');
|
||||
|
||||
// Card box container
|
||||
const cardBox = document.createElement('div');
|
||||
cardBox.className = 'cardBox cardBox-bottompadded';
|
||||
|
||||
// Card scalable container
|
||||
const cardScalable = document.createElement('div');
|
||||
cardScalable.className = 'cardScalable';
|
||||
|
||||
// Card padder with icon
|
||||
const cardPadder = document.createElement('div');
|
||||
cardPadder.className = `cardPadder ${padderClass} lazy-hidden-children`;
|
||||
|
||||
const cardIcon = document.createElement('span');
|
||||
cardIcon.className = 'cardImageIcon material-icons';
|
||||
cardIcon.setAttribute('aria-hidden', 'true');
|
||||
|
||||
// Set icon based on item type
|
||||
if (item.Type === 'Movie') {
|
||||
cardIcon.textContent = 'movie';
|
||||
} else if (item.Type === 'Series') {
|
||||
cardIcon.textContent = 'tv';
|
||||
} else if (item.Type === 'Episode') {
|
||||
cardIcon.textContent = 'play_circle';
|
||||
} else if (item.Type === 'MusicAlbum') {
|
||||
cardIcon.textContent = 'album';
|
||||
} else if (item.Type === 'Audio') {
|
||||
cardIcon.textContent = 'music_note';
|
||||
} else if (item.Type === 'Artist') {
|
||||
cardIcon.textContent = 'person';
|
||||
} else {
|
||||
cardIcon.textContent = 'folder';
|
||||
}
|
||||
|
||||
cardPadder.appendChild(cardIcon);
|
||||
|
||||
// Blurhash canvas (placeholder)
|
||||
const blurhashCanvas = document.createElement('canvas');
|
||||
blurhashCanvas.setAttribute('aria-hidden', 'true');
|
||||
blurhashCanvas.width = 20;
|
||||
blurhashCanvas.height = 20;
|
||||
blurhashCanvas.className = 'blurhash-canvas lazy-hidden';
|
||||
|
||||
// Card image container
|
||||
const cardImageContainer = document.createElement('a');
|
||||
cardImageContainer.href = `#/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) {
|
||||
let imageUrl;
|
||||
if (item.IsJellyseerr) {
|
||||
// Jellyseerr items use external image URLs
|
||||
imageUrl = item.ImageTags.Primary.startsWith('http')
|
||||
? item.ImageTags.Primary
|
||||
: `https://image.tmdb.org/t/p/w500${item.ImageTags.Primary}`;
|
||||
} else {
|
||||
// Regular Jellyfin items
|
||||
imageUrl = `${serverAddress}/Items/${item.Id}/Images/Primary?${imageParams}&quality=96&tag=${item.ImageTags.Primary}`;
|
||||
}
|
||||
cardImageContainer.style.backgroundImage = `url("${imageUrl}")`;
|
||||
} else {
|
||||
// No image - add icon as inner element
|
||||
const iconSpan = document.createElement('span');
|
||||
iconSpan.className = 'cardImageIcon material-icons';
|
||||
iconSpan.setAttribute('aria-hidden', 'true');
|
||||
|
||||
// Set icon based on item type
|
||||
if (item.Type === 'Movie') {
|
||||
iconSpan.textContent = 'movie';
|
||||
} else if (item.Type === 'Series') {
|
||||
iconSpan.textContent = 'tv';
|
||||
} else if (item.Type === 'Episode') {
|
||||
iconSpan.textContent = 'tv';
|
||||
} else if (item.Type === 'Person') {
|
||||
iconSpan.textContent = 'person';
|
||||
} else if (item.Type === 'MusicAlbum') {
|
||||
iconSpan.textContent = 'album';
|
||||
} else if (item.Type === 'Audio') {
|
||||
iconSpan.textContent = 'music_note';
|
||||
} else if (item.Type === 'Artist' || item.Type === 'MusicArtist') {
|
||||
iconSpan.textContent = 'person';
|
||||
} else {
|
||||
iconSpan.textContent = 'folder';
|
||||
}
|
||||
|
||||
cardImageContainer.appendChild(iconSpan);
|
||||
}
|
||||
|
||||
// Card overlay container
|
||||
const cardOverlayContainer = document.createElement('div');
|
||||
cardOverlayContainer.className = 'cardOverlayContainer itemAction';
|
||||
cardOverlayContainer.setAttribute('data-action', 'link');
|
||||
|
||||
// Overlay link
|
||||
const overlayLink = document.createElement('a');
|
||||
overlayLink.href = `#/details?id=${item.Id}&serverId=${serverId}`;
|
||||
overlayLink.className = 'cardImageContainer';
|
||||
|
||||
// Play button
|
||||
const playButton = document.createElement('button');
|
||||
playButton.setAttribute('is', 'paper-icon-button-light');
|
||||
playButton.className = 'cardOverlayButton cardOverlayButton-hover itemAction paper-icon-button-light cardOverlayFab-primary';
|
||||
playButton.setAttribute('data-action', 'resume');
|
||||
|
||||
const playIcon = document.createElement('span');
|
||||
playIcon.className = 'material-icons cardOverlayButtonIcon cardOverlayButtonIcon-hover play_arrow';
|
||||
playIcon.setAttribute('aria-hidden', 'true');
|
||||
playButton.appendChild(playIcon);
|
||||
|
||||
// Button container for additional overlay buttons (watchlist, etc.)
|
||||
const buttonContainer = document.createElement('div');
|
||||
buttonContainer.className = 'cardOverlayButton-br flex';
|
||||
|
||||
// Add play button to button container
|
||||
buttonContainer.appendChild(playButton);
|
||||
|
||||
// Assemble overlay
|
||||
cardOverlayContainer.appendChild(overlayLink);
|
||||
cardOverlayContainer.appendChild(buttonContainer);
|
||||
|
||||
// Card text container - different structure for episodes
|
||||
const cardTextContainer = document.createElement('div');
|
||||
cardTextContainer.className = 'cardText cardTextCentered cardText-first';
|
||||
|
||||
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.className = 'itemAction textActionButton';
|
||||
seriesLink.setAttribute('data-id', item.SeriesId || item.Id);
|
||||
seriesLink.setAttribute('data-serverid', serverId);
|
||||
seriesLink.setAttribute('data-type', 'Series');
|
||||
seriesLink.setAttribute('data-mediatype', 'undefined');
|
||||
seriesLink.setAttribute('data-channelid', 'undefined');
|
||||
seriesLink.setAttribute('data-isfolder', 'true');
|
||||
seriesLink.setAttribute('data-action', 'link');
|
||||
seriesLink.title = item.SeriesName || 'Unknown Series';
|
||||
seriesLink.textContent = item.SeriesName || 'Unknown Series';
|
||||
|
||||
const seriesBdi = document.createElement('bdi');
|
||||
seriesBdi.appendChild(seriesLink);
|
||||
cardTextContainer.appendChild(seriesBdi);
|
||||
|
||||
// Episode title as secondary
|
||||
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.className = 'itemAction textActionButton';
|
||||
episodeLink.setAttribute('data-id', item.Id);
|
||||
episodeLink.setAttribute('data-serverid', serverId);
|
||||
episodeLink.setAttribute('data-type', 'Episode');
|
||||
episodeLink.setAttribute('data-mediatype', 'undefined');
|
||||
episodeLink.setAttribute('data-channelid', 'undefined');
|
||||
episodeLink.setAttribute('data-isfolder', 'false');
|
||||
episodeLink.setAttribute('data-action', 'link');
|
||||
episodeLink.title = item.Name || 'Unknown Episode';
|
||||
episodeLink.textContent = item.Name || 'Unknown Episode';
|
||||
|
||||
const episodeBdi = document.createElement('bdi');
|
||||
episodeBdi.appendChild(episodeLink);
|
||||
secondaryText.appendChild(episodeBdi);
|
||||
} else {
|
||||
// Default: Item name as primary, year as secondary
|
||||
const titleLink = document.createElement('a');
|
||||
titleLink.href = `#/details?id=${item.Id}&serverId=${serverId}`;
|
||||
titleLink.className = 'itemAction textActionButton';
|
||||
titleLink.setAttribute('data-id', item.Id);
|
||||
titleLink.setAttribute('data-serverid', serverId);
|
||||
titleLink.setAttribute('data-type', item.Type);
|
||||
titleLink.setAttribute('data-mediatype', 'undefined');
|
||||
titleLink.setAttribute('data-channelid', 'undefined');
|
||||
titleLink.setAttribute('data-isfolder', item.Type === 'MusicAlbum' || item.Type === 'Artist' || item.Type === 'MusicArtist' ? 'true' : 'false');
|
||||
titleLink.setAttribute('data-action', 'link');
|
||||
titleLink.title = item.Name || 'Unknown';
|
||||
titleLink.textContent = item.Name || 'Unknown';
|
||||
|
||||
const titleBdi = document.createElement('bdi');
|
||||
titleBdi.appendChild(titleLink);
|
||||
cardTextContainer.appendChild(titleBdi);
|
||||
|
||||
// Secondary text (year)
|
||||
const secondaryText = document.createElement('div');
|
||||
secondaryText.className = 'cardText cardTextCentered cardText-secondary';
|
||||
const yearBdi = document.createElement('bdi');
|
||||
yearBdi.textContent = item.ProductionYear || item.PremiereDate?.substring(0, 4) || '';
|
||||
secondaryText.appendChild(yearBdi);
|
||||
}
|
||||
|
||||
// Assemble card
|
||||
cardScalable.appendChild(cardPadder);
|
||||
cardScalable.appendChild(blurhashCanvas);
|
||||
cardScalable.appendChild(cardImageContainer);
|
||||
cardScalable.appendChild(cardOverlayContainer);
|
||||
|
||||
cardBox.appendChild(cardScalable);
|
||||
cardBox.appendChild(cardTextContainer);
|
||||
|
||||
// Add secondary text after primary text
|
||||
if (item.Type === 'Episode') {
|
||||
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.className = 'itemAction textActionButton';
|
||||
episodeLink.setAttribute('data-id', item.Id);
|
||||
episodeLink.setAttribute('data-serverid', serverId);
|
||||
episodeLink.setAttribute('data-type', 'Episode');
|
||||
episodeLink.setAttribute('data-mediatype', 'undefined');
|
||||
episodeLink.setAttribute('data-channelid', 'undefined');
|
||||
episodeLink.setAttribute('data-isfolder', 'false');
|
||||
episodeLink.setAttribute('data-action', 'link');
|
||||
episodeLink.title = item.Name || 'Unknown Episode';
|
||||
episodeLink.textContent = item.Name || 'Unknown Episode';
|
||||
|
||||
const episodeBdi = document.createElement('bdi');
|
||||
episodeBdi.appendChild(episodeLink);
|
||||
secondaryText.appendChild(episodeBdi);
|
||||
cardBox.appendChild(secondaryText);
|
||||
} else {
|
||||
const secondaryText = document.createElement('div');
|
||||
secondaryText.className = 'cardText cardTextCentered cardText-secondary';
|
||||
const yearBdi = document.createElement('bdi');
|
||||
yearBdi.textContent = item.ProductionYear || item.PremiereDate?.substring(0, 4) || '';
|
||||
secondaryText.appendChild(yearBdi);
|
||||
cardBox.appendChild(secondaryText);
|
||||
}
|
||||
|
||||
card.appendChild(cardBox);
|
||||
|
||||
// Add click handler
|
||||
card.addEventListener('click', (e) => {
|
||||
if (!e.target.closest('button')) {
|
||||
console.log('[CardBuilder] item click', { id: item.Id, name: item.Name });
|
||||
try {
|
||||
Dashboard.navigate(`#!/details?id=${item.Id}&serverId=${serverId}`);
|
||||
} catch(e) {
|
||||
console.error('[CardBuilder]', e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
// Expose the cardBuilder to the global window object
|
||||
window.cardBuilder = cardBuilder;
|
||||
|
||||
console.log('[CardBuilder] Module loaded and available at window.cardBuilder');
|
||||
})();
|
||||
+775
@@ -0,0 +1,775 @@
|
||||
// Jellyfin Watchlist Enhancement Script
|
||||
// Adds watchlist functionality throughout Jellyfin interface
|
||||
// Requires: cardBuilder.js module to be loaded before this script
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Common logging function
|
||||
const LOG = (...args) => console.log('[Watchlist]', ...args);
|
||||
const WARN = (...args) => console.warn('[Watchlist]', ...args);
|
||||
const ERR = (...args) => console.error('[Watchlist]', ...args);
|
||||
|
||||
LOG('Script loaded - JS Injector mode');
|
||||
|
||||
/************ Helpers ************/
|
||||
|
||||
function formatPremiereDate(dateStr) {
|
||||
if (!dateStr) return "";
|
||||
return new Date(dateStr).toLocaleDateString(undefined, { weekday: "long", month: "long", day: "numeric" });
|
||||
}
|
||||
|
||||
function setCardData(items, options) {
|
||||
// Stub function for compatibility
|
||||
}
|
||||
|
||||
function buildCard(index, item, apiClient, options) {
|
||||
// Check if cardBuilder is available
|
||||
if (typeof window.cardBuilder !== 'undefined' && window.cardBuilder.buildCard) {
|
||||
// Use cardBuilder to create a proper Jellyfin card
|
||||
const cardElement = window.cardBuilder.buildCard(item);
|
||||
cardElement.setAttribute('data-index', index);
|
||||
cardElement.classList.add('discover-card');
|
||||
|
||||
// Convert DOM element to HTML string
|
||||
return cardElement.outerHTML;
|
||||
} else {
|
||||
// Fallback to simple card if cardBuilder is not available
|
||||
WARN("cardBuilder not available, using fallback card creation");
|
||||
return buildFallbackCard(index, item, apiClient, options);
|
||||
}
|
||||
}
|
||||
|
||||
function buildFallbackCard(index, item, apiClient, options) {
|
||||
var serverUrl = apiClient.serverAddress();
|
||||
var imgUrl = serverUrl + "/Items/" + item.Id + "/Images/Primary?maxWidth=200&tag=" + item.PrimaryImageTag;
|
||||
var itemUrl = getItemDetailUrl(item);
|
||||
|
||||
var html = '';
|
||||
html += '<div class="card overflowPortraitCard card-hoverable card-withuserdata discover-card" data-index="' + index + '">';
|
||||
html += ' <div class="cardBox cardBox-bottompadded">';
|
||||
html += ' <div class="cardScalable discoverCard-' + item.SourceType + '">';
|
||||
html += ' <div class="cardPadder cardPadder-overflowPortrait lazy-hidden-children"></div>';
|
||||
html += ' <canvas aria-hidden="true" width="20" height="20" class="blurhash-canvas lazy-hidden"></canvas>';
|
||||
html += ' <a href="' + itemUrl + '" class="cardImageContainer coveredImage cardContent itemAction lazy blurhashed lazy-image-fadein-fast" aria-label="" style="background-image: url(' + imgUrl + ');"></a>';
|
||||
html += ' <div class="cardOverlayContainer itemAction" data-action="link">';
|
||||
html += ' <div class="cardOverlayButton-br flex">';
|
||||
html += ' <button is="discover-requestbutton" type="button" data-action="none" class="discover-requestbutton cardOverlayButton cardOverlayButton-hover itemAction paper-icon-button-light emby-button" data-id="' + item.id + '" data-media-type="' + item.SourceType + '">';
|
||||
html += ' <span class="material-icons cardOverlayButtonIcon cardOverlayButtonIcon-hover add" aria-hidden="true"></span>';
|
||||
html += ' </button>';
|
||||
html += ' </div>';
|
||||
html += ' </div>';
|
||||
html += ' </div>';
|
||||
html += ' <div class="cardText cardTextCentered cardText-first">';
|
||||
html += ' <bdi>';
|
||||
html += ' <a target="_blank" href="' + item.id + '/' + item.SourceType + '/' + item.id + '" class="itemAction textActionButton" title="' + item.Name + '" data-action="link">' + item.Name + '</a>';
|
||||
html += ' </bdi>';
|
||||
html += ' </div>';
|
||||
html += ' <div class="cardText cardTextCentered cardText-secondary">';
|
||||
html += ' <bdi>';
|
||||
|
||||
var date = new Date(item.PremiereDate);
|
||||
|
||||
html += ' <a target="_blank" href="' + item.id + '/' + item.SourceType + '/' + item.id + '" class="itemAction textActionButton" title="' + date.getFullYear() + '" data-action="link">' + date.getFullYear() + '</a>';
|
||||
html += ' </bdi>';
|
||||
html += ' </div>';
|
||||
html += ' </div>';
|
||||
html += '</div>';
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
async function toggleFavorite(itemId, btn) {
|
||||
const apiClient = window.ApiClient;
|
||||
const userId = apiClient.getCurrentUserId();
|
||||
const serverUrl = apiClient.serverAddress();
|
||||
const token = apiClient.accessToken();
|
||||
|
||||
const isFav = btn.textContent === "♡";
|
||||
try {
|
||||
await fetch(`${serverUrl}/Users/${userId}/FavoriteItems/${itemId}?IsFavorite=${isFav}`, {
|
||||
method: "POST",
|
||||
headers: { "Authorization": `MediaBrowser Token=\"${token}\"` }
|
||||
});
|
||||
btn.textContent = isFav ? "♥" : "♡";
|
||||
} catch (err) {
|
||||
ERR("Favorite toggle failed", err);
|
||||
}
|
||||
}
|
||||
|
||||
function buildCardsHtmlInternal(items, options) {
|
||||
setCardData(items, options);
|
||||
let html = "";
|
||||
let itemsInRow = 0;
|
||||
let currentIndexValue;
|
||||
let hasOpenRow = false;
|
||||
let hasOpenSection = false;
|
||||
const sectionTitleTagName = options.sectionTitleTagName || "div";
|
||||
const apiClient = window.ApiClient;
|
||||
let lastServerId;
|
||||
|
||||
for (const [i, item] of items.entries()) {
|
||||
const serverId = item.ServerId || options.serverId;
|
||||
if (serverId !== lastServerId) lastServerId = serverId;
|
||||
|
||||
let newIndexValue = "";
|
||||
if (options.indexBy === "PremiereDate") {
|
||||
newIndexValue = formatPremiereDate(item.PremiereDate);
|
||||
} else if (options.indexBy === "ProductionYear") {
|
||||
newIndexValue = item.ProductionYear;
|
||||
} else if (options.indexBy === "CommunityRating") {
|
||||
const rounded = item.CommunityRating % 1 >= 0.5 ? 0.5 : 0;
|
||||
newIndexValue = item.CommunityRating ? (Math.floor(item.CommunityRating) + rounded) + "+" : "";
|
||||
}
|
||||
|
||||
// Handle section changes
|
||||
if (newIndexValue !== currentIndexValue) {
|
||||
|
||||
// Close previous row and section if open
|
||||
if (hasOpenRow) {
|
||||
html += "</div>";
|
||||
hasOpenRow = false;
|
||||
itemsInRow = 0;
|
||||
}
|
||||
if (hasOpenSection) {
|
||||
html += "</div>";
|
||||
hasOpenSection = false;
|
||||
}
|
||||
|
||||
// Start new section (always horizontal)
|
||||
html += "<div class=\"horizontalSection\">";
|
||||
html += `<${sectionTitleTagName} class="sectionTitle">${newIndexValue}</${sectionTitleTagName}>`;
|
||||
|
||||
currentIndexValue = newIndexValue;
|
||||
hasOpenSection = true;
|
||||
}
|
||||
|
||||
// Handle row creation for multi-row layouts
|
||||
if (options.rows && itemsInRow === 0) {
|
||||
html += "<div class=\"cardColumn\">";
|
||||
hasOpenRow = true;
|
||||
}
|
||||
|
||||
// Build the card with direct linking
|
||||
html += buildCard(i, item, apiClient, options);
|
||||
itemsInRow++;
|
||||
|
||||
// Close row if we've reached the row limit
|
||||
if (options.rows && itemsInRow >= options.rows) {
|
||||
html += "</div>";
|
||||
hasOpenRow = false;
|
||||
itemsInRow = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Close any remaining open elements
|
||||
if (hasOpenRow) html += "</div>";
|
||||
if (hasOpenSection) html += "</div>";
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function buildCardWithLink(index, item, apiClient, options) {
|
||||
// Generate the URL for the item detail page
|
||||
const itemUrl = getItemDetailUrl(item);
|
||||
|
||||
// Build the original card content
|
||||
const cardContent = buildCard(index, item, apiClient, options);
|
||||
|
||||
// Wrap the card in a clickable link
|
||||
return `<a href="${itemUrl}" class="card-link" style="text-decoration: none; color: inherit;">${cardContent}</a>`;
|
||||
}
|
||||
|
||||
function getItemDetailUrl(item) {
|
||||
// Construct the URL to the item's detail page
|
||||
// Adjust this based on your Jellyfin setup and routing
|
||||
const baseUrl = window.location.origin;
|
||||
const serverId = item.ServerId;
|
||||
|
||||
// Standard Jellyfin item detail URL format
|
||||
return `${baseUrl}/web/#/details?id=${item.Id}&serverId=${serverId}`;
|
||||
}
|
||||
|
||||
async function fetchLikedItems(type) {
|
||||
const apiClient = window.ApiClient;
|
||||
const userId = apiClient.getCurrentUserId();
|
||||
const serverUrl = apiClient.serverAddress();
|
||||
const token = apiClient.accessToken();
|
||||
|
||||
const url = `${serverUrl}/Items?Filters=Likes&IncludeItemTypes=${type}&UserId=${userId}&Recursive=true`;
|
||||
|
||||
try {
|
||||
const res = await fetch(url, { headers: { "Authorization": `MediaBrowser Token=\"${token}\"` } });
|
||||
const data = await res.json();
|
||||
return data.Items || [];
|
||||
} catch (err) {
|
||||
ERR("Failed to fetch liked items for type:", type, err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function renderCards(containerSelector, type) {
|
||||
const container = document.querySelector(containerSelector);
|
||||
if (!container) {
|
||||
WARN("Container not found:", containerSelector);
|
||||
return { type, itemCount: 0 };
|
||||
}
|
||||
const items = await fetchLikedItems(type);
|
||||
|
||||
// Hide section if no items
|
||||
if (!items || items.length === 0) {
|
||||
container.style.display = 'none';
|
||||
return { type, itemCount: 0 };
|
||||
}
|
||||
|
||||
// Show section and create proper Jellyfin section structure
|
||||
container.style.display = '';
|
||||
const sectionHtml = createWatchlistSection(type, items);
|
||||
container.innerHTML = sectionHtml;
|
||||
return { type, itemCount: items.length };
|
||||
}
|
||||
|
||||
function createWatchlistSection(type, items) {
|
||||
// This function assumes items array is not empty (checked at higher level)
|
||||
|
||||
// Create the main vertical section container
|
||||
const verticalSection = document.createElement('div');
|
||||
verticalSection.className = 'verticalSection emby-scroller-container';
|
||||
|
||||
// Create section title
|
||||
const sectionTitle = document.createElement('h2');
|
||||
sectionTitle.className = 'sectionTitle sectionTitle-cards focuscontainer-x padded-left padded-right';
|
||||
sectionTitle.textContent = getTypeDisplayName(type);
|
||||
|
||||
// Create scroll buttons container
|
||||
const scrollButtons = document.createElement('div');
|
||||
scrollButtons.setAttribute('is', 'emby-scrollbuttons');
|
||||
scrollButtons.className = 'emby-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');
|
||||
scroller.setAttribute('data-horizontal', 'true');
|
||||
scroller.setAttribute('data-centerfocus', 'card');
|
||||
scroller.className = 'padded-top-focusscale padded-bottom-focusscale emby-scroller';
|
||||
scroller.setAttribute('data-scroll-mode-x', 'custom');
|
||||
scroller.style.overflow = 'hidden';
|
||||
|
||||
// Create items container
|
||||
const itemsContainer = document.createElement('div');
|
||||
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 using cardBuilder
|
||||
items.forEach((item, index) => {
|
||||
if (typeof window.cardBuilder !== 'undefined' && window.cardBuilder.buildCard) {
|
||||
const card = window.cardBuilder.buildCard(item);
|
||||
card.setAttribute('data-index', index);
|
||||
card.classList.add('discover-card');
|
||||
itemsContainer.appendChild(card);
|
||||
} else {
|
||||
// Fallback to HTML string
|
||||
const cardHtml = buildFallbackCard(index, item, window.ApiClient, {});
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = cardHtml;
|
||||
itemsContainer.appendChild(tempDiv.firstElementChild);
|
||||
}
|
||||
});
|
||||
|
||||
scroller.appendChild(itemsContainer);
|
||||
|
||||
// Add scroll functionality
|
||||
let scrollPosition = 0;
|
||||
const cardWidth = 212; // 200px card + 12px gap
|
||||
const visibleCards = Math.floor(scroller.offsetWidth / cardWidth);
|
||||
const scrollStep = Math.max(1, Math.floor(visibleCards * 0.9));
|
||||
const maxScroll = Math.max(0, items.length - visibleCards);
|
||||
|
||||
const updateScrollButtons = () => {
|
||||
prevButton.disabled = scrollPosition <= 0;
|
||||
nextButton.disabled = scrollPosition >= maxScroll;
|
||||
};
|
||||
|
||||
const scrollTo = (position, smooth = true) => {
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
prevButton.addEventListener('click', () => {
|
||||
const newPosition = Math.max(0, scrollPosition - scrollStep);
|
||||
scrollTo(newPosition, true);
|
||||
});
|
||||
|
||||
nextButton.addEventListener('click', () => {
|
||||
const newPosition = Math.min(maxScroll, scrollPosition + scrollStep);
|
||||
scrollTo(newPosition, true);
|
||||
});
|
||||
|
||||
// Initial state
|
||||
updateScrollButtons();
|
||||
|
||||
// Assemble the section
|
||||
verticalSection.appendChild(sectionTitle);
|
||||
verticalSection.appendChild(scrollButtons);
|
||||
verticalSection.appendChild(scroller);
|
||||
|
||||
return verticalSection.outerHTML;
|
||||
}
|
||||
|
||||
function getTypeDisplayName(itemType) {
|
||||
const typeMap = {
|
||||
'Movie': 'Movies',
|
||||
'Series': 'TV Shows',
|
||||
'Episode': 'Episodes',
|
||||
'Person': 'People',
|
||||
'MusicAlbum': 'Albums',
|
||||
'Audio': 'Songs',
|
||||
'Artist': 'Artists',
|
||||
'Playlist': 'Playlists',
|
||||
'Book': 'Books',
|
||||
'AudioBook': 'Audiobooks',
|
||||
'Photo': 'Photos',
|
||||
'PhotoAlbum': 'Photo Albums',
|
||||
'TvChannel': 'TV Channels',
|
||||
'LiveTvProgram': 'Live TV',
|
||||
'BoxSet': 'Collections'
|
||||
};
|
||||
return typeMap[itemType] || itemType;
|
||||
}
|
||||
|
||||
/************ Watchlist Section Observer ************/
|
||||
|
||||
// Function to render watchlist content when .sections.watchlist is found
|
||||
async function renderWatchlistContent() {
|
||||
try {
|
||||
const results = await Promise.all([
|
||||
renderCards(".sections.watchlist > .watchlist-movies", "Movie"),
|
||||
renderCards(".sections.watchlist > .watchlist-series", "Series"),
|
||||
renderCards(".sections.watchlist > .watchlist-episodes", "Episode")
|
||||
]);
|
||||
|
||||
// Check if all sections are empty
|
||||
const totalItems = results.reduce((sum, result) => sum + result.itemCount, 0);
|
||||
|
||||
if (totalItems === 0) {
|
||||
// Show empty state message
|
||||
showEmptyWatchlistMessage();
|
||||
} else {
|
||||
// Hide empty state message if it exists
|
||||
hideEmptyWatchlistMessage();
|
||||
}
|
||||
} catch (err) {
|
||||
ERR('Error rendering watchlist cards:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to check if watchlist section exists and render content
|
||||
function checkAndRenderWatchlist() {
|
||||
const watchlistSection = document.querySelector('.sections.watchlist');
|
||||
if (watchlistSection && !watchlistSection.dataset.watchlistRendered) {
|
||||
watchlistSection.dataset.watchlistRendered = 'true';
|
||||
renderWatchlistContent();
|
||||
}
|
||||
}
|
||||
|
||||
// Set up observer to watch for .sections.watchlist element
|
||||
function setupWatchlistSectionObserver() {
|
||||
// Check immediately in case the element already exists
|
||||
checkAndRenderWatchlist();
|
||||
|
||||
// Set up MutationObserver to watch for .sections.watchlist
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === 'childList') {
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
// Check if the added node is the watchlist section
|
||||
if (node.classList && node.classList.contains('sections') && node.classList.contains('watchlist')) {
|
||||
checkAndRenderWatchlist();
|
||||
}
|
||||
|
||||
// Check for watchlist section within the added node
|
||||
const watchlistSection = node.querySelector && node.querySelector('.sections.watchlist');
|
||||
if (watchlistSection) {
|
||||
checkAndRenderWatchlist();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Start observing
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize the watchlist section observer
|
||||
setupWatchlistSectionObserver();
|
||||
|
||||
function showEmptyWatchlistMessage() {
|
||||
// Check if message already exists
|
||||
let emptyMessage = document.querySelector('.watchlist-empty-message');
|
||||
if (emptyMessage) {
|
||||
emptyMessage.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
// Create empty state message
|
||||
emptyMessage = document.createElement('div');
|
||||
emptyMessage.className = 'watchlist-empty-message';
|
||||
emptyMessage.style.cssText = `
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: #999;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
`;
|
||||
|
||||
emptyMessage.innerHTML = `
|
||||
<div style="margin-bottom: 20px;">
|
||||
<span class="material-icons" style="font-size: 48px; color: #666;">bookmark_border</span>
|
||||
</div>
|
||||
<h3 style="color: #fff; margin-bottom: 16px; font-size: 20px;">Your Watchlist is Empty</h3>
|
||||
<p style="margin: 0; max-width: 400px; margin-left: auto; margin-right: auto;">
|
||||
Press the <span class="material-icons" style="font-size: 18px; vertical-align: middle; color: #00a4dc;">bookmark_border</span>
|
||||
button to add an item to your Watchlist
|
||||
</p>
|
||||
`;
|
||||
|
||||
// Find the watchlist container and add the message
|
||||
const watchlistContainer = document.querySelector('.sections.watchlist');
|
||||
if (watchlistContainer) {
|
||||
watchlistContainer.appendChild(emptyMessage);
|
||||
} else {
|
||||
// Fallback to body if watchlist container not found
|
||||
document.body.appendChild(emptyMessage);
|
||||
}
|
||||
}
|
||||
|
||||
function hideEmptyWatchlistMessage() {
|
||||
const emptyMessage = document.querySelector('.watchlist-empty-message');
|
||||
if (emptyMessage) {
|
||||
emptyMessage.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
LOG('Watchlist functionality initialized');
|
||||
|
||||
/************ Watchlist Button Observer ************/
|
||||
|
||||
// Function to add watchlist button to a card overlay container
|
||||
function addWatchlistButton(overlayContainer) {
|
||||
// Check if watchlist button already exists
|
||||
if (overlayContainer && overlayContainer.querySelector('.watchlist-button')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the card parent to get the item ID
|
||||
const card = overlayContainer.closest('.card');
|
||||
if (!card) {
|
||||
WARN('Could not find card parent for overlay container');
|
||||
return;
|
||||
}
|
||||
|
||||
const itemId = card.getAttribute('data-id');
|
||||
if (!itemId) {
|
||||
WARN('Could not find data-id on card element');
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the .cardOverlayButton-br container
|
||||
const buttonContainer = overlayContainer.querySelector('.cardOverlayButton-br');
|
||||
if (!buttonContainer) {
|
||||
WARN('Could not find .cardOverlayButton-br container');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create watchlist button
|
||||
const watchlistButton = document.createElement('button');
|
||||
watchlistButton.type = 'button';
|
||||
watchlistButton.className = 'watchlist-button cardOverlayButton cardOverlayButton-hover itemAction paper-icon-button-light emby-button';
|
||||
watchlistButton.setAttribute('data-action', 'none');
|
||||
watchlistButton.setAttribute('data-id', itemId);
|
||||
watchlistButton.setAttribute('data-active', 'false');
|
||||
watchlistButton.title = 'Add to Watchlist';
|
||||
|
||||
// Create the bookmark icon
|
||||
const watchlistIcon = document.createElement('span');
|
||||
watchlistIcon.className = 'material-icons cardOverlayButtonIcon cardOverlayButtonIcon-hover';
|
||||
watchlistIcon.textContent = 'bookmark_border';
|
||||
watchlistIcon.setAttribute('aria-hidden', 'true');
|
||||
|
||||
watchlistButton.appendChild(watchlistIcon);
|
||||
|
||||
// Add click event listener
|
||||
watchlistButton.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Toggle watchlist status
|
||||
const newRating = watchlistButton.dataset.active === 'false' ? 'true' : 'false';
|
||||
ApiClient.updateUserItemRating(ApiClient.getCurrentUserId(), itemId, newRating);
|
||||
watchlistButton.dataset.active = newRating;
|
||||
|
||||
// Update icon and title based on state
|
||||
const isActive = watchlistButton.dataset.active === 'true';
|
||||
watchlistIcon.textContent = isActive ? 'bookmark' : 'bookmark_border';
|
||||
watchlistButton.title = isActive ? 'Remove from Watchlist' : 'Add to Watchlist';
|
||||
});
|
||||
|
||||
// Check item's current watchlist status and set initial state
|
||||
ApiClient.getItem(ApiClient.getCurrentUserId(), itemId).then((item) => {
|
||||
if (item.UserData && item.UserData.Likes) {
|
||||
watchlistButton.dataset.active = 'true';
|
||||
watchlistIcon.textContent = 'bookmark';
|
||||
watchlistButton.title = 'Remove from Watchlist';
|
||||
}
|
||||
}).catch(err => {
|
||||
ERR('Error fetching item data for watchlist button:', err);
|
||||
});
|
||||
|
||||
// Add the watchlist button to the button container
|
||||
buttonContainer.appendChild(watchlistButton);
|
||||
}
|
||||
|
||||
// Function to process all existing overlay containers
|
||||
function processExistingOverlayContainers() {
|
||||
const overlayContainers = document.querySelectorAll('.cardOverlayContainer');
|
||||
|
||||
overlayContainers.forEach((overlayContainer) => {
|
||||
const buttonContainer = overlayContainer.querySelector('.cardOverlayButton-br');
|
||||
if (buttonContainer) {
|
||||
addWatchlistButton(overlayContainer);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Set up MutationObserver to watch for new overlay containers
|
||||
function setupWatchlistButtonObserver() {
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === 'childList') {
|
||||
// Check for added nodes
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
// Check if the added node is an overlay container
|
||||
if (node.classList && node.classList.contains('cardOverlayContainer')) {
|
||||
const buttonContainer = node.querySelector('.cardOverlayButton-br');
|
||||
if (buttonContainer) {
|
||||
addWatchlistButton(node);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for overlay containers within the added node
|
||||
const overlayContainers = node.querySelectorAll && node.querySelectorAll('.cardOverlayContainer');
|
||||
if (overlayContainers && overlayContainers.length > 0) {
|
||||
overlayContainers.forEach((overlayContainer) => {
|
||||
const buttonContainer = overlayContainer.querySelector('.cardOverlayButton-br');
|
||||
if (buttonContainer) {
|
||||
addWatchlistButton(overlayContainer);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Start observing
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
|
||||
// Process any existing overlay containers
|
||||
processExistingOverlayContainers();
|
||||
}
|
||||
|
||||
// Initialize the observer
|
||||
setupWatchlistButtonObserver();
|
||||
|
||||
/************ Item Detail Page Observer ************/
|
||||
|
||||
// Function to add watchlist button to item detail page
|
||||
function addDetailPageWatchlistButton() {
|
||||
const trailerButton = document.querySelector('.itemDetailPage:not(.hide) .btnPlayTrailer');
|
||||
|
||||
if (!trailerButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if watchlist button already exists
|
||||
const existingButton = document.querySelector('.itemDetailPage:not(.hide) .watchlist-icon');
|
||||
if (existingButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
const watchlistIcon = document.createElement('button');
|
||||
watchlistIcon.setAttribute("is", "emby-button");
|
||||
watchlistIcon.className = 'watchlist-icon detailButton emby-button hide';
|
||||
watchlistIcon.title = "Add to Watchlist";
|
||||
watchlistIcon.dataset.active = 'false';
|
||||
|
||||
// Get item ID from URL
|
||||
const itemId = window.location.href.substring(window.location.href.indexOf("id=") + 3, window.location.href.indexOf("id=") + 35);
|
||||
|
||||
watchlistIcon.addEventListener('click', () => {
|
||||
const newRating = watchlistIcon.dataset.active === 'false' ? 'true' : 'false';
|
||||
ApiClient.updateUserItemRating(ApiClient.getCurrentUserId(), itemId, newRating);
|
||||
watchlistIcon.dataset.active = newRating;
|
||||
|
||||
// Update title based on state
|
||||
const isActive = watchlistIcon.dataset.active === 'true';
|
||||
watchlistIcon.title = isActive ? 'Remove from Watchlist' : 'Add to Watchlist';
|
||||
});
|
||||
|
||||
// Get item data to check if it should be shown and current state
|
||||
ApiClient.getItem(ApiClient.getCurrentUserId(), itemId).then((item) => {
|
||||
// Only show for Movies, Series, Seasons, and Episodes
|
||||
if (item.Type !== "Movie" && item.Type !== "Series" && item.Type !== "Season" && item.Type !== "Episode") {
|
||||
watchlistIcon.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
// Set initial state based on current watchlist status
|
||||
if (item.UserData && item.UserData.Likes) {
|
||||
watchlistIcon.dataset.active = 'true';
|
||||
watchlistIcon.title = 'Remove from Watchlist';
|
||||
}
|
||||
}).catch(err => {
|
||||
ERR('Error fetching item data:', err);
|
||||
});
|
||||
|
||||
// Add button after trailer button
|
||||
trailerButton.after(watchlistIcon);
|
||||
|
||||
// Set up observer to show button when other buttons become visible
|
||||
const target = document.querySelector('.mainDetailButtons');
|
||||
if (target) {
|
||||
const observer = new MutationObserver(mutations => {
|
||||
mutations.forEach(mutation => {
|
||||
if (mutation.type === "attributes" &&
|
||||
mutation.attributeName === "class" &&
|
||||
mutation.target.classList.contains("emby-button") &&
|
||||
!mutation.target.classList.contains("watchlist-icon") &&
|
||||
!mutation.target.classList.contains("hide")) {
|
||||
|
||||
if (watchlistIcon && watchlistIcon.classList.contains("hide")) {
|
||||
watchlistIcon.classList.remove('hide');
|
||||
observer.disconnect();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(target, {
|
||||
attributes: true,
|
||||
subtree: true,
|
||||
attributeFilter: ["class"]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Function to monitor item detail pages
|
||||
function monitorItemDetailPage() {
|
||||
const observer = new MutationObserver(() => {
|
||||
const visibleItemDetailPage = document.querySelector('.itemDetailPage:not(.hide)');
|
||||
if (!visibleItemDetailPage) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if watchlist button already exists
|
||||
const existingButton = document.querySelector('.itemDetailPage:not(.hide) .watchlist-icon');
|
||||
if (existingButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
addDetailPageWatchlistButton();
|
||||
});
|
||||
|
||||
// Start observing the entire document body
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ['class']
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize the item detail page observer
|
||||
monitorItemDetailPage();
|
||||
|
||||
// Debug functions for troubleshooting (available in console)
|
||||
window.debugWatchlistButtons = function() {
|
||||
LOG('Manual debug trigger called');
|
||||
LOG('Current overlay containers:', document.querySelectorAll('.cardOverlayContainer').length);
|
||||
LOG('Current watchlist buttons:', document.querySelectorAll('.watchlist-button').length);
|
||||
processExistingOverlayContainers();
|
||||
};
|
||||
|
||||
window.debugWatchlistRendering = function() {
|
||||
LOG('Manual watchlist rendering trigger called');
|
||||
checkAndRenderWatchlist();
|
||||
};
|
||||
|
||||
window.debugDetailPageWatchlist = function() {
|
||||
LOG('Manual detail page watchlist trigger called');
|
||||
addDetailPageWatchlistButton();
|
||||
};
|
||||
|
||||
LOG('Debug functions available: window.debugWatchlistButtons(), window.debugWatchlistRendering(), window.debugDetailPageWatchlist()');
|
||||
})();
|
||||
Reference in New Issue
Block a user