diff --git a/Jellyfin.Plugin.Seasonals/Web/test-site-new.html b/Jellyfin.Plugin.Seasonals/Web/test-site-new.html index 627ce68..88d9db7 100644 --- a/Jellyfin.Plugin.Seasonals/Web/test-site-new.html +++ b/Jellyfin.Plugin.Seasonals/Web/test-site-new.html @@ -370,9 +370,69 @@ console.log('[Test Site] Theme cleared.'); } + // Track active animation frames and observers for cleanup + let activeAnimationFrames = []; + let activeBlobUrls = []; + + // Patch requestAnimationFrame and MutationObserver to track them + const origRAF = window.requestAnimationFrame; + const origCAF = window.cancelAnimationFrame; + let trackingEnabled = false; + + window.requestAnimationFrame = function(cb) { + const id = origRAF.call(window, cb); + if (trackingEnabled) activeAnimationFrames.push(id); + return id; + }; + + window.cancelAnimationFrame = function(id) { + origCAF.call(window, id); + activeAnimationFrames = activeAnimationFrames.filter(f => f !== id); + }; + + // Track MutationObservers created by themes + let activeObservers = []; + const OrigMO = window.MutationObserver; + window.MutationObserver = class extends OrigMO { + constructor(cb) { + super(cb); + if (trackingEnabled) activeObservers.push(this); + } + }; + + // Track intervals created by themes + let activeIntervals = []; + const origSetInterval = window.setInterval; + const origClearInterval = window.clearInterval; + window.setInterval = function(...args) { + const id = origSetInterval.apply(window, args); + if (trackingEnabled) activeIntervals.push(id); + return id; + }; + window.clearInterval = function(id) { + origClearInterval.call(window, id); + activeIntervals = activeIntervals.filter(i => i !== id); + }; + function loadTheme() { clearTheme(); + // Cancel all tracked animation frames + activeAnimationFrames.forEach(id => origCAF.call(window, id)); + activeAnimationFrames = []; + + // Disconnect all tracked MutationObservers + activeObservers.forEach(obs => obs.disconnect()); + activeObservers = []; + + // Clear all tracked intervals + activeIntervals.forEach(id => origClearInterval.call(window, id)); + activeIntervals = []; + + // Revoke old blob URLs + activeBlobUrls.forEach(url => URL.revokeObjectURL(url)); + activeBlobUrls = []; + const value = select.value; if (!value || value === '') return; @@ -403,16 +463,31 @@ document.head.appendChild(link); } - // Inject JS (after a short delay to let CSS load) + // Inject JS wrapped in IIFE to avoid const redeclaration errors if (jsFile) { - setTimeout(() => { - const script = document.createElement('script'); - script.src = jsFile; - script.setAttribute('data-seasonal', 'true'); - script.onerror = () => console.error(`[Test Site] Failed to load JS: ${jsFile}`); - document.body.appendChild(script); - console.log(`[Test Site] Loaded theme: ${value} (${jsFile})`); - }, 100); + setTimeout(async () => { + try { + const response = await fetch(jsFile); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + const code = await response.text(); + + // Wrap in IIFE so each theme has its own scope + const wrappedCode = `(function() {\n${code}\n})();`; + const blob = new Blob([wrappedCode], { type: 'application/javascript' }); + const blobUrl = URL.createObjectURL(blob); + activeBlobUrls.push(blobUrl); + + trackingEnabled = true; + const script = document.createElement('script'); + script.src = blobUrl; + script.setAttribute('data-seasonal', 'true'); + script.onerror = () => console.error(`[Test Site] Failed to execute JS: ${jsFile}`); + document.body.appendChild(script); + console.log(`[Test Site] Loaded theme: ${value} (${jsFile}) [IIFE-wrapped]`); + } catch (err) { + console.error(`[Test Site] Failed to load JS: ${jsFile}`, err); + } + }, 150); } }