diff --git a/configuration/skinManager-configuration.js b/configuration/skinManager-configuration.js index 4fd5727..2e4d7e5 100644 --- a/configuration/skinManager-configuration.js +++ b/configuration/skinManager-configuration.js @@ -6,12 +6,26 @@ const LOG = (...args) => console.log('[KefinTweaks SkinManager Config]', ...args); const ERR = (...args) => console.error('[KefinTweaks SkinManager Config]', ...args); + const WARN = (...args) => console.warn('[KefinTweaks SkinManager Config]', ...args); const MODAL_ID = 'kefin-skinmanager-config'; // Store loaded config for use in listeners let currentLoadedConfig = null; + function hasDefaultSkinsCatalog() { + const skins = window.KefinTweaksDefaultSkinsConfig?.skins; + return Array.isArray(skins) && skins.length > 0; + } + + function notifySkinManagerConfig(message) { + if (window.KefinTweaksToaster?.toast) { + window.KefinTweaksToaster.toast(message); + } else { + alert(message); + } + } + // Helper function to build Jellyfin checkbox function buildJellyfinCheckbox(id, checked, label, dataAttributes = {}) { let className = 'emby-checkbox'; @@ -298,6 +312,22 @@ */ async function saveConfig(skinManagerConfig) { try { + if (!hasDefaultSkinsCatalog()) { + WARN('Refusing to save Skin Manager config: default skins catalog not loaded'); + notifySkinManagerConfig('Default skins are not loaded yet. Try again in a moment.'); + return false; + } + + const existingSkins = window.KefinTweaksConfig?.skins; + const existingNonEmpty = Array.isArray(existingSkins) && existingSkins.length > 0; + const nextSkins = skinManagerConfig?.skins; + const nextEmpty = !Array.isArray(nextSkins) || nextSkins.length === 0; + if (nextEmpty && existingNonEmpty) { + WARN('Refusing to save Skin Manager config: would wipe non-empty skins array'); + notifySkinManagerConfig('Save blocked: collected skins list is empty but existing config has skins. Reload and try again.'); + return false; + } + if (!window.KefinTweaksConfig) { window.KefinTweaksConfig = {}; } @@ -437,10 +467,17 @@ /** * Collect configuration from form + * @returns {Object|null} Collected config, or null if unsafe to save */ function collectConfig() { + if (!hasDefaultSkinsCatalog()) { + WARN('Cannot collect Skin Manager config: default skins catalog not loaded'); + notifySkinManagerConfig('Default skins are not loaded yet. Try again in a moment.'); + return null; + } + // Get default skin - const defaultSkin = document.getElementById('defaultSkin')?.value || null; + let defaultSkin = document.getElementById('defaultSkin')?.value || null; if (defaultSkin === '') { defaultSkin = null; } @@ -532,6 +569,14 @@ // Add the current category's optional includes optionalIncludes.push(...currentCategoryOptionalIncludes); + const existingSkins = window.KefinTweaksConfig?.skins; + const existingNonEmpty = Array.isArray(existingSkins) && existingSkins.length > 0; + if (skins.length === 0 && existingNonEmpty) { + WARN('Cannot collect Skin Manager config: would wipe non-empty skins array'); + notifySkinManagerConfig('Save blocked: collected skins list is empty but existing config has skins. Reload and try again.'); + return null; + } + return { skins: skins, themes: themes, @@ -577,6 +622,11 @@ */ async function openConfigModal() { try { + if (!hasDefaultSkinsCatalog()) { + notifySkinManagerConfig('Default skins are not loaded yet. Try again in a moment.'); + return; + } + // Load config const config = await loadConfig(); @@ -617,6 +667,7 @@ if (saveBtn) { saveBtn.addEventListener('click', async () => { const skinManagerConfig = collectConfig(); + if (!skinManagerConfig) return; const success = await saveConfig(skinManagerConfig); if (success) { if (window.KefinTweaksToaster && window.KefinTweaksToaster.toast) { diff --git a/scripts/kefinTweaks-loader.js b/scripts/kefinTweaks-loader.js index 05a5b0b..ceaca16 100644 --- a/scripts/kefinTweaks-loader.js +++ b/scripts/kefinTweaks-loader.js @@ -198,7 +198,7 @@ name: 'skinManager-configuration', script: '../configuration/skinManager-configuration.js', css: null, - dependencies: ['modal', 'toaster', 'utils'], + dependencies: ['modal', 'toaster', 'utils', 'skinConfigLegacyDefaults', 'skinConfig'], description: 'Skin manager configuration for KefinTweaks' }, { diff --git a/scripts/skinManager.js b/scripts/skinManager.js index 69dcc54..587af7d 100644 --- a/scripts/skinManager.js +++ b/scripts/skinManager.js @@ -408,6 +408,10 @@ window.KefinTweaksConfig = ${JSON.stringify(configToBackup, null, 2)};`; } } + // Legacy: strip exact default duplicates / old Jellypane URLs from admin skins and persist. + // Disabled — can wipe enable/disable stubs. Keep helpers + body for possible restore. + const RUN_LEGACY_SKIN_DEDUP_CLEANUP = false; + // Load and merge skin configurations async function loadSkinConfig() { // Load default skins from skinConfig.js (for merging) @@ -418,73 +422,75 @@ window.KefinTweaksConfig = ${JSON.stringify(configToBackup, null, 2)};`; // Load admin-configured skins from window.KefinTweaksConfig (from JS Injector) let adminSkins = window.KefinTweaksConfig?.skins || []; - - // Clean up admin skins: remove exact duplicates of legacy defaults and old Jellypane format - const cleanedAdminSkins = []; - const skinsToRemove = new Set(); - - adminSkins.forEach(adminSkin => { - // Check for exact match with any legacy default skin (canonical comparison) - const matchingLegacyDefault = legacyDefaults.find(legacySkin => { - if (adminSkin.name !== legacySkin.name) { - return false; + + if (RUN_LEGACY_SKIN_DEDUP_CLEANUP) { + // Clean up admin skins: remove exact duplicates of legacy defaults and old Jellypane format + const cleanedAdminSkins = []; + const skinsToRemove = new Set(); + + adminSkins.forEach(adminSkin => { + // Check for exact match with any legacy default skin (canonical comparison) + const matchingLegacyDefault = legacyDefaults.find(legacySkin => { + if (adminSkin.name !== legacySkin.name) { + return false; + } + const matches = skinsMatchExactly(adminSkin, legacySkin); + if (!matches) { + // Debug: Log why skins with same name don't match + LOG(`[Skin Match Debug] Skin '${adminSkin.name}' found in both configs but URLs/colorSchemes differ. Admin: ${JSON.stringify({url: adminSkin.url, colorSchemes: adminSkin.colorSchemes})}, Legacy Default: ${JSON.stringify({url: legacySkin.url, colorSchemes: legacySkin.colorSchemes})}`); + } + return matches; + }); + + if (matchingLegacyDefault) { + // Exact match found - remove from admin config + LOG(`Removing exact duplicate of legacy default skin '${adminSkin.name}' from admin config`); + skinsToRemove.add(adminSkin.name); + return; // Skip adding to cleanedAdminSkins } - const matches = skinsMatchExactly(adminSkin, legacySkin); - if (!matches) { - // Debug: Log why skins with same name don't match - LOG(`[Skin Match Debug] Skin '${adminSkin.name}' found in both configs but URLs/colorSchemes differ. Admin: ${JSON.stringify({url: adminSkin.url, colorSchemes: adminSkin.colorSchemes})}, Legacy Default: ${JSON.stringify({url: legacySkin.url, colorSchemes: legacySkin.colorSchemes})}`); + + // Special case: Jellypane with old URL format + if (isJellypaneWithOldUrlFormat(adminSkin)) { + LOG(`Removing Jellypane with old URL format from admin config`); + skinsToRemove.add(adminSkin.name); + return; // Skip adding to cleanedAdminSkins } - return matches; + + // Keep this admin skin + cleanedAdminSkins.push(adminSkin); }); - - if (matchingLegacyDefault) { - // Exact match found - remove from admin config - LOG(`Removing exact duplicate of legacy default skin '${adminSkin.name}' from admin config`); - skinsToRemove.add(adminSkin.name); - return; // Skip adding to cleanedAdminSkins - } - - // Special case: Jellypane with old URL format - if (isJellypaneWithOldUrlFormat(adminSkin)) { - LOG(`Removing Jellypane with old URL format from admin config`); - skinsToRemove.add(adminSkin.name); - return; // Skip adding to cleanedAdminSkins - } - - // Keep this admin skin - cleanedAdminSkins.push(adminSkin); - }); - - // If we removed any skins, create backup and save cleaned config - if (skinsToRemove.size > 0 && window.KefinTweaksConfig) { - // Create a deep copy of the original config for backup - const originalConfig = JSON.parse(JSON.stringify(window.KefinTweaksConfig)); - - // Create backup before modifying - const backupCreated = await createConfigBackup(originalConfig); - if (!backupCreated) { - WARN('Failed to create backup, but continuing with cleanup'); - } - - // Update KefinTweaksConfig with cleaned skins - window.KefinTweaksConfig.skins = cleanedAdminSkins; - - // Save the cleaned config back to JS Injector - if (window.KefinTweaksUtils && window.KefinTweaksUtils.saveConfigToJavaScriptInjector) { - const saveSuccess = await window.KefinTweaksUtils.saveConfigToJavaScriptInjector(window.KefinTweaksConfig, { waitForLogin: false }); - if (saveSuccess) { - LOG(`Successfully saved cleaned config (removed ${skinsToRemove.size} duplicate/old-format skin(s): ${Array.from(skinsToRemove).join(', ')})`); - } else { - WARN('Failed to save cleaned config to JS Injector'); + + // If we removed any skins, create backup and save cleaned config + if (skinsToRemove.size > 0 && window.KefinTweaksConfig) { + // Create a deep copy of the original config for backup + const originalConfig = JSON.parse(JSON.stringify(window.KefinTweaksConfig)); + + // Create backup before modifying + const backupCreated = await createConfigBackup(originalConfig); + if (!backupCreated) { + WARN('Failed to create backup, but continuing with cleanup'); } - } else { - WARN('KefinTweaksUtils.saveConfigToJavaScriptInjector not available, cannot save cleaned config'); + + // Update KefinTweaksConfig with cleaned skins + window.KefinTweaksConfig.skins = cleanedAdminSkins; + + // Save the cleaned config back to JS Injector + if (window.KefinTweaksUtils && window.KefinTweaksUtils.saveConfigToJavaScriptInjector) { + const saveSuccess = await window.KefinTweaksUtils.saveConfigToJavaScriptInjector(window.KefinTweaksConfig, { waitForLogin: false }); + if (saveSuccess) { + LOG(`Successfully saved cleaned config (removed ${skinsToRemove.size} duplicate/old-format skin(s): ${Array.from(skinsToRemove).join(', ')})`); + } else { + WARN('Failed to save cleaned config to JS Injector'); + } + } else { + WARN('KefinTweaksUtils.saveConfigToJavaScriptInjector not available, cannot save cleaned config'); + } + + LOG(`Cleaned up ${skinsToRemove.size} duplicate/old-format skin(s) from admin config: ${Array.from(skinsToRemove).join(', ')}`); } - - LOG(`Cleaned up ${skinsToRemove.size} duplicate/old-format skin(s) from admin config: ${Array.from(skinsToRemove).join(', ')}`); + + adminSkins = cleanedAdminSkins; } - - adminSkins = cleanedAdminSkins; // Merge: Start with defaults, then override/add from admin config // Admin config takes precedence for duplicate names