From b58264998af554a1dd9b78b80e7ff8de582a05d2 Mon Sep 17 00:00:00 2001 From: CodeDevMLH <145071728+CodeDevMLH@users.noreply.github.com> Date: Tue, 17 Feb 2026 02:01:05 +0100 Subject: [PATCH] Enhance theme loading by tracking animation frames, MutationObservers, and intervals; wrap JS in IIFE for scope isolation --- .../Web/test-site-new.html | 93 +++++++++++++++++-- 1 file changed, 84 insertions(+), 9 deletions(-) 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); } }