// KefinTweaks Script Injector // Dynamically loads scripts and CSS files based on user configuration // Usage: Modify the ENABLED_SCRIPTS array below to enable/disable features (function() { 'use strict'; // Configuration: Set which scripts to load // Set to true to enable, false to disable const ENABLED_SCRIPTS = { // Core functionality (required for other scripts) cardBuilder: true, // Required by watchlist and homeScreen // UI Enhancements watchlist: true, // Watchlist functionality homeScreen: true, // Custom home screen sections search: true, // Enhanced search functionality headerTabs: true, // Header tab enhancements customMenu: true, // Custom menu functionality exclusiveElsewhere: true, // Exclusive elsewhere branding updoot: true, // Upvote functionality // Utility scripts backdropLeakFix: true, // Memory leak fixes }; // Script definitions with dependencies and metadata const SCRIPT_DEFINITIONS = [ { name: 'cardBuilder', script: 'cardBuilder.js', css: null, dependencies: [], description: 'Core card building functionality (required by other scripts)', required: true }, { name: 'watchlist', script: 'watchlist.js', css: 'watchlist.css', dependencies: ['cardBuilder'], description: 'Adds watchlist functionality throughout Jellyfin interface' }, { name: 'homeScreen', script: 'homeScreen.js', css: 'homeScreen.css', dependencies: ['cardBuilder'], description: 'Adds custom home screen sections' }, { name: 'search', script: 'search.js', css: 'search.css', dependencies: [], description: 'Enhanced search functionality' }, { name: 'headerTabs', script: 'headerTabs.js', css: null, dependencies: [], description: 'Header tab improvements' }, { name: 'customMenu', script: 'custom-menu.js', css: null, dependencies: [], description: 'Remove target="_blank" from custom menu links' }, { name: 'exclusiveElsewhere', script: 'exclusiveElsewhere.js', css: null, dependencies: [], description: 'Modifies the behavior of the Jellyfin Enhanced Elsewhere functionality to add custom branding when items are not available on streaming services' }, { name: 'backdropLeakFix', script: 'backdrop-leak-fix.js', css: null, dependencies: [], description: 'Fixes issue that causes backdrop images to be continuously added to the page if the tab isn\'t focused.' }, { name: 'updoot', script: 'updoot.js', css: null, dependencies: [], description: 'Upvote functionality' } ]; // Configuration validation function validateConfiguration() { const errors = []; // Check if required scripts are enabled SCRIPT_DEFINITIONS.forEach(script => { if (script.required && !ENABLED_SCRIPTS[script.name]) { errors.push(`Required script '${script.name}' is disabled`); } }); // Check dependencies SCRIPT_DEFINITIONS.forEach(script => { if (ENABLED_SCRIPTS[script.name]) { script.dependencies.forEach(dep => { if (!ENABLED_SCRIPTS[dep]) { errors.push(`Script '${script.name}' requires '${dep}' to be enabled`); } }); } }); if (errors.length > 0) { console.error('[KefinTweaks Injector] Configuration errors:', errors); return false; } return true; } // Get the root path for scripts (adjust this based on your setup) function getScriptRoot() { // Fallback: assume scripts are in the same directory return 'https://cdn.jsdelivr.net/gh/ranaldsgift/KefinTweaks/'; } // Load a CSS file function loadCSS(filename) { return new Promise((resolve, reject) => { // Check if CSS is already loaded const existingLink = document.querySelector(`link[href*="${filename}"]`); if (existingLink) { console.log(`[KefinTweaks Injector] CSS already loaded: ${filename}`); resolve(); return; } const link = document.createElement('link'); link.rel = 'stylesheet'; link.type = 'text/css'; link.href = `${getScriptRoot()}${filename}`; link.onload = () => { console.log(`[KefinTweaks Injector] CSS loaded: ${filename}`); resolve(); }; link.onerror = () => { console.warn(`[KefinTweaks Injector] Failed to load CSS: ${filename}`); reject(new Error(`Failed to load CSS: ${filename}`)); }; document.head.appendChild(link); }); } // Load a JavaScript file function loadScript(filename) { return new Promise((resolve, reject) => { // Check if script is already loaded const existingScript = document.querySelector(`script[src*="${filename}"]`); if (existingScript) { console.log(`[KefinTweaks Injector] Script already loaded: ${filename}`); resolve(); return; } const script = document.createElement('script'); script.src = `${getScriptRoot()}${filename}`; script.async = true; script.onload = () => { console.log(`[KefinTweaks Injector] Script loaded: ${filename}`); resolve(); }; script.onerror = () => { console.error(`[KefinTweaks Injector] Failed to load script: ${filename}`); reject(new Error(`Failed to load script: ${filename}`)); }; document.head.appendChild(script); }); } // Load a script and its dependencies async function loadScriptWithDependencies(scriptDef) { try { // Load dependencies first for (const depName of scriptDef.dependencies) { const depScript = SCRIPT_DEFINITIONS.find(s => s.name === depName); if (depScript && ENABLED_SCRIPTS[depName]) { await loadScriptWithDependencies(depScript); } } // Load CSS if specified if (scriptDef.css) { await loadCSS(scriptDef.css); } // Load the script await loadScript(scriptDef.script); console.log(`[KefinTweaks Injector] Successfully loaded: ${scriptDef.name}`); } catch (error) { console.error(`[KefinTweaks Injector] Error loading ${scriptDef.name}:`, error); throw error; } } // Main initialization function async function initialize() { console.log('[KefinTweaks Injector] Starting initialization...'); // Validate configuration if (!validateConfiguration()) { console.error('[KefinTweaks Injector] Configuration validation failed. Aborting.'); return; } // Get enabled scripts in dependency order const enabledScripts = SCRIPT_DEFINITIONS.filter(script => ENABLED_SCRIPTS[script.name]); console.log(`[KefinTweaks Injector] Loading ${enabledScripts.length} scripts:`, enabledScripts.map(s => s.name)); // Load all enabled scripts const loadPromises = enabledScripts.map(script => loadScriptWithDependencies(script)); try { await Promise.all(loadPromises); console.log('[KefinTweaks Injector] All scripts loaded successfully!'); // Dispatch custom event when all scripts are loaded const event = new CustomEvent('kefinTweaksLoaded', { detail: { loadedScripts: enabledScripts.map(s => s.name), timestamp: new Date().toISOString() } }); document.dispatchEvent(event); } catch (error) { console.error('[KefinTweaks Injector] Error during initialization:', error); } } // Utility functions for debugging and configuration window.KefinTweaks = { // Get current configuration getConfig: () => ({ ...ENABLED_SCRIPTS }), // Get script definitions getScripts: () => [...SCRIPT_DEFINITIONS], // Check if a script is loaded isScriptLoaded: (scriptName) => { const scriptDef = SCRIPT_DEFINITIONS.find(s => s.name === scriptName); if (!scriptDef) return false; const scriptElement = document.querySelector(`script[src*="${scriptDef.script}"]`); return !!scriptElement; }, // Reload all scripts (useful for development) reload: () => { console.log('[KefinTweaks Injector] Reloading all scripts...'); initialize(); }, // Load a specific script (useful for dynamic loading) loadScript: async (scriptName) => { const scriptDef = SCRIPT_DEFINITIONS.find(s => s.name === scriptName); if (!scriptDef) { throw new Error(`Script '${scriptName}' not found`); } if (!ENABLED_SCRIPTS[scriptName]) { throw new Error(`Script '${scriptName}' is disabled in configuration`); } await loadScriptWithDependencies(scriptDef); } }; // Start initialization when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initialize); } else { initialize(); } console.log('[KefinTweaks Injector] Injector script loaded. Available at window.KefinTweaks'); })();