// KefinTweaks Installer // Entry point script for KefinTweaks installation and configuration // Users paste this script into JS Injector instead of injector.js (function() { 'use strict'; console.log('[KefinTweaks Installer] Initializing...'); const MODAL_ID = 'kefinTweaksSourceModal'; let versionsCache = null; function getAuthHeader() { const token = ApiClient.accessToken(); const client = typeof ApiClient.applicationName === 'function' ? ApiClient.applicationName() : 'Jellyfin Web'; const device = typeof ApiClient.deviceName === 'function' ? ApiClient.deviceName() : (navigator.userAgent.includes('Chrome') ? 'Chrome' : 'Browser'); const deviceId = typeof ApiClient.deviceId === 'function' ? ApiClient.deviceId() : ''; const version = ApiClient._appVersion || ApiClient._serverVersion || ''; const parts = [ `Client="${encodeURIComponent(client)}"`, `Device="${encodeURIComponent(device)}"`, `DeviceId="${encodeURIComponent(deviceId)}"`, `Version="${encodeURIComponent(version)}"`, `Token="${encodeURIComponent(token)}"` ]; return `MediaBrowser ${parts.join(', ')}`; } // jsDelivr URL patterns const JSDELIVR_BASE = 'https://cdn.jsdelivr.net/gh/ranaldsgift/KefinTweaks'; const GITHUB_REPO = 'ranaldsgift/KefinTweaks'; const GITHUB_API_RELEASES = `https://api.github.com/repos/${GITHUB_REPO}/releases`; // Check if user is admin async function isAdmin() { try { if (window.ApiClient && window.ApiClient.getCurrentUser) { const user = await window.ApiClient.getCurrentUser(); return user && user.Policy && user.Policy.IsAdministrator === true; } } catch (error) { console.warn('[KefinTweaks Installer] Could not check admin status:', error); } return false; } // Find JavaScript Injector plugin async function findJavaScriptInjectorPlugin() { try { if (!window.ApiClient || !window.ApiClient._serverAddress || window.ApiClient.accessToken() === null) { return null; } const server = ApiClient._serverAddress; const response = await fetch(`${server}/Plugins`, { headers: { 'Authorization': getAuthHeader() } }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const plugins = await response.json(); const pluginsList = Array.isArray(plugins) ? plugins : (plugins.Items || []); const injectorPlugin = pluginsList.find(plugin => plugin.Name === 'JavaScript Injector' || plugin.Name === 'JS Injector' ); if (!injectorPlugin) { throw new Error('JavaScript Injector plugin not found'); } return injectorPlugin.Id; } catch (error) { console.error('[KefinTweaks Installer] Error finding JavaScript Injector plugin:', error); throw error; } } // Get current JavaScript Injector configuration async function getJavaScriptInjectorConfig(pluginId) { try { if (!window.ApiClient || !window.ApiClient._serverAddress || !window.ApiClient.accessToken) { return null; } const server = ApiClient._serverAddress; const response = await fetch(`${server}/Plugins/${pluginId}/Configuration`, { headers: { 'Authorization': getAuthHeader() } }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return await response.json(); } catch (error) { console.error('[KefinTweaks Installer] Error getting JS Injector config:', error); throw error; } } // Get KefinTweaks config from JS Injector async function getKefinTweaksConfig() { try { if (window.KefinTweaksConfig) { return window.KefinTweaksConfig; } const pluginId = await findJavaScriptInjectorPlugin(); if (!pluginId) { return null; } const injectorConfig = await getJavaScriptInjectorConfig(pluginId); if (!injectorConfig) { return null; } const kefinTweaksScript = injectorConfig.CustomJavaScripts?.find( script => script.Name === 'KefinTweaks-Config' ); if (kefinTweaksScript && kefinTweaksScript.Script) { const scriptMatch = kefinTweaksScript.Script.match(/window\.KefinTweaksConfig\s*=\s*({[\s\S]*});/); if (scriptMatch && scriptMatch[1]) { try { return JSON.parse(scriptMatch[1]); } catch (parseError) { console.error('[KefinTweaks Installer] Error parsing config:', parseError); } } } } catch (error) { console.warn('[KefinTweaks Installer] Error getting config:', error); } return null; } // Load default config from URL async function loadDefaultConfig(rootUrl) { return new Promise((resolve, reject) => { try { const defaultConfigUrl = `${rootUrl}kefinTweaks-default-config.js`; if (window.KefinTweaksDefaultConfig) { resolve(window.KefinTweaksDefaultConfig); return; } const script = document.createElement('script'); script.src = defaultConfigUrl; script.async = true; script.onload = () => { if (window.KefinTweaksDefaultConfig) { resolve(window.KefinTweaksDefaultConfig); } else { reject(new Error('Default config file loaded but window.KefinTweaksDefaultConfig is not defined')); } }; script.onerror = () => { reject(new Error(`Failed to load default config file: ${defaultConfigUrl}`)); }; document.head.appendChild(script); } catch (error) { reject(error); } }); } // Save configuration to JavaScript Injector async function saveConfigToJavaScriptInjector(config) { try { const pluginId = await findJavaScriptInjectorPlugin(); const injectorConfig = await getJavaScriptInjectorConfig(pluginId); if (!injectorConfig.CustomJavaScripts) { injectorConfig.CustomJavaScripts = []; } const scriptContent = `// KefinTweaks Configuration // This file is automatically generated by KefinTweaks Installer // Do not edit manually unless you know what you're doing window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`; const existingScriptIndex = injectorConfig.CustomJavaScripts.findIndex( script => script.Name === 'KefinTweaks-Config' ); if (existingScriptIndex !== -1) { injectorConfig.CustomJavaScripts[existingScriptIndex].Script = scriptContent; } else { injectorConfig.CustomJavaScripts.push({ Name: 'KefinTweaks-Config', Script: scriptContent, Enabled: true, RequiresAuthentication: false }); } const server = ApiClient._serverAddress; const response = await fetch(`${server}/Plugins/${pluginId}/Configuration`, { method: 'POST', headers: { 'Authorization': getAuthHeader(), 'Content-Type': 'application/json' }, body: JSON.stringify(injectorConfig) }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return true; } catch (error) { console.error('[KefinTweaks Installer] Error saving config:', error); throw error; } } const LATEST_RELEASE_NAME = 'Latest'; const DEVELOPMENT_NAME = 'Development'; const EXPERIMENTAL_NAME = 'Experimental'; const SELF_HOSTED_NAME = 'Self Hosted'; // Parse source URL to determine source type and version function parseKefinTweaksSource(url) { if (!url || url === '') { return { sourceType: 'github', version: LATEST_RELEASE_NAME }; } const match = url.match(/cdn\.jsdelivr\.net\/gh\/ranaldsgift\/KefinTweaks@([^\/]+)\//); if (!match) { return { sourceType: 'custom', version: null }; } const version = match[1]; if (version === 'latest') { return { sourceType: 'github', version: LATEST_RELEASE_NAME }; } else if (version === 'main' || version === 'master') { return { sourceType: 'github', version: DEVELOPMENT_NAME }; } else if (version === 'experimental') { return { sourceType: 'github', version: EXPERIMENTAL_NAME }; } else if (version && version.length === 40 && /^[0-9a-f]+$/.test(version)) { // Commit hash URL (e.g. from a previous Experimental selection) return { sourceType: 'github', version: EXPERIMENTAL_NAME }; } else { return { sourceType: 'github', version: version.replace(/^v/, '') }; } } // Fetch available versions from GitHub async function fetchVersions() { if (versionsCache) { return versionsCache; } try { const response = await fetch(GITHUB_API_RELEASES); if (!response.ok) { throw new Error('Failed to fetch releases'); } const releases = await response.json(); const versions = releases .map(release => release.tag_name.replace(/^v/, '')) .filter(tag => tag.match(/^\d+\.\d+\.\d+/)); // Only semantic versions versionsCache = [LATEST_RELEASE_NAME, DEVELOPMENT_NAME, EXPERIMENTAL_NAME, ...versions]; return versionsCache; } catch (error) { console.warn('[KefinTweaks Installer] Error fetching versions:', error); return [LATEST_RELEASE_NAME, DEVELOPMENT_NAME, EXPERIMENTAL_NAME]; } } // Build kefinTweaksRoot URL from selections function buildKefinTweaksRootUrl(sourceType, source) { if (sourceType === 'custom') { // Ensure URL ends with / return source.endsWith('/') ? source : source + '/'; } // GitHub source (branch names like main/experimental are resolved by jsDelivr to latest commit at load time) let versionTag = 'latest'; if (source === DEVELOPMENT_NAME) { versionTag = 'main'; } else if (source === EXPERIMENTAL_NAME) { versionTag = 'experimental'; } else if (source !== LATEST_RELEASE_NAME) { versionTag = source.startsWith('v') ? source : 'v' + source; } return `${JSDELIVR_BASE}@${versionTag}/`; } // Simple modal creation (doesn't depend on modal.js) function createSimpleModal(id, title, content, footer) { // Remove existing modal const existing = document.getElementById(id); if (existing) { existing.remove(); } const backdrop = document.createElement('div'); backdrop.className = 'dialogBackdrop dialogBackdropOpened'; backdrop.id = id + '-backdrop'; const dialogContainer = document.createElement('div'); dialogContainer.className = 'dialogContainer'; dialogContainer.id = id; const dialog = document.createElement('div'); dialog.className = 'focuscontainer dialog smoothScrollY ui-body-a background-theme-a formDialog centeredDialog opened'; dialog.style.display = 'flex'; dialog.style.flexDirection = 'column'; dialog.style.maxHeight = '90vh'; dialog.style.maxWidth = '600px'; dialog.style.width = '90vw'; if (title) { const header = document.createElement('div'); header.className = 'formDialogHeader'; header.style.display = 'flex'; header.style.justifyContent = 'space-between'; header.style.alignItems = 'center'; header.style.padding = '1.25em 1.5em'; header.style.borderBottom = '1px solid rgba(255,255,255,0.1)'; const titleEl = document.createElement('h2'); titleEl.style.margin = '0'; titleEl.textContent = title; header.appendChild(titleEl); const closeBtn = document.createElement('button'); closeBtn.setAttribute('is', 'paper-icon-button-light'); closeBtn.className = 'btnCancel btnClose autoSize paper-icon-button-light'; closeBtn.onclick = () => closeSimpleModal(id); const closeIcon = document.createElement('span'); closeIcon.className = 'material-icons close'; closeBtn.appendChild(closeIcon); header.appendChild(closeBtn); dialog.appendChild(header); } const contentDiv = document.createElement('div'); contentDiv.style.padding = '1.25em 1.5em 8em'; contentDiv.style.overflowY = 'auto'; contentDiv.style.flex = '1'; if (typeof content === 'string') { contentDiv.innerHTML = content; } else { contentDiv.appendChild(content); } dialog.appendChild(contentDiv); if (footer) { const footerDiv = document.createElement('div'); footerDiv.className = 'formDialogFooter'; footerDiv.style.padding = '1.25em 1.5em'; footerDiv.style.borderTop = '1px solid rgba(255,255,255,0.1)'; if (typeof footer === 'string') { footerDiv.innerHTML = footer; } else { footerDiv.appendChild(footer); } dialog.appendChild(footerDiv); } dialogContainer.appendChild(dialog); document.body.appendChild(backdrop); document.body.appendChild(dialogContainer); backdrop.onclick = (e) => { if (e.target === backdrop) { closeSimpleModal(id); } }; // Close modal when clicking outside (on dialogContainer) dialogContainer.onclick = (e) => { if (e.target === dialogContainer) { closeSimpleModal(id); } }; return { id, close: () => closeSimpleModal(id) }; } function closeSimpleModal(id) { const backdrop = document.getElementById(id + '-backdrop'); const dialog = document.getElementById(id); if (backdrop) backdrop.remove(); if (dialog) dialog.remove(); } /** * Show a confirmation modal * @param {string} message - Confirmation message * @param {Function} onConfirm - Callback when user confirms * @param {Function} onCancel - Optional callback when user cancels */ function showConfirmationModal(message, onConfirm, onCancel = null) { const CONFIRM_MODAL_ID = 'kefinTweaksConfirmModal'; const content = document.createElement('div'); content.style.display = 'flex'; content.style.flexDirection = 'column'; content.style.gap = '1em'; content.style.lineHeight = '1.6'; content.innerHTML = `
${message}
`; const footer = document.createElement('div'); footer.style.display = 'flex'; footer.style.justifyContent = 'flex-end'; footer.style.gap = '1em'; const cancelBtn = document.createElement('button'); cancelBtn.className = 'btnCancel raised emby-button button-cancel'; cancelBtn.textContent = 'Cancel'; cancelBtn.onclick = () => { closeSimpleModal(CONFIRM_MODAL_ID); if (onCancel) onCancel(); }; const confirmBtn = document.createElement('button'); confirmBtn.className = 'btnSubmit raised emby-button button-submit'; confirmBtn.textContent = 'Confirm'; confirmBtn.onclick = () => { closeSimpleModal(CONFIRM_MODAL_ID); if (onConfirm) onConfirm(); }; footer.appendChild(cancelBtn); footer.appendChild(confirmBtn); createSimpleModal(CONFIRM_MODAL_ID, 'Confirm', content, footer); } /** * Show an alert/info modal * @param {string} title - Modal title * @param {string} message - Message content (can include HTML) */ function showAlertModal(title, message) { const ALERT_MODAL_ID = 'kefinTweaksAlertModal'; const content = document.createElement('div'); content.style.display = 'flex'; content.style.flexDirection = 'column'; content.style.gap = '1em'; content.style.lineHeight = '1.6'; content.innerHTML = message; const footer = document.createElement('div'); footer.style.display = 'flex'; footer.style.justifyContent = 'flex-end'; footer.style.gap = '1em'; const okBtn = document.createElement('button'); okBtn.className = 'btnSubmit raised emby-button button-submit'; okBtn.textContent = 'OK'; okBtn.onclick = () => { closeSimpleModal(ALERT_MODAL_ID); }; footer.appendChild(okBtn); createSimpleModal(ALERT_MODAL_ID, title, content, footer); } /** * Show success modal after saving configuration * @param {boolean} isInitialInstall - Whether this is the initial installation * @param {string} version - Version number or source type */ async function showSuccessModal(isInitialInstall, version) { const SUCCESS_MODAL_ID = 'kefinTweaksSuccessModal'; const content = document.createElement('div'); content.style.display = 'flex'; content.style.flexDirection = 'column'; content.style.lineHeight = '1.6'; if (isInitialInstall) { // Initial installation message with support information content.innerHTML = `Thanks so much for installing KefinTweaks!
KefinTweaks is not maintained by the Jellyfin team, and as a result you are encouraged to seek support from me directly.
Sadly, there is currently no place suitable for discussion of plugins built by community members or fan-made projects in the official Jellyfin Discord, so please visit the Jellyfin Community Discord to find me (username: HighImKevin) and other users who would be happy to help you out.
Please also feel free to report bugs and request features from the Issues page. The strength of this plugin relies on awesome community members like you, so thanks for using KefinTweaks!
`; } else { // Update message with refresh instruction content.innerHTML = `KefinTweaks configuration has been successfully updated. Please refresh your page for the changes to take effect!
`; } // Add discord icon link to the bottom of the content use a background img on an element const kefinRoot = window.KefinTweaksConfig?.kefinTweaksRoot; if (kefinRoot) { // Wrapped in a link to the discord server const discordLinkWrapper = document.createElement('div'); discordLinkWrapper.style.display = 'flex'; discordLinkWrapper.style.justifyContent = 'center'; const discordLink = document.createElement('a'); discordLink.href = 'https://discord.gg/v7P9CAvCKZ'; discordLink.target = '_blank'; discordLink.style.display = 'inline-block'; discordLink.style.width = '50px'; const discordIcon = document.createElement('div'); discordIcon.style.backgroundImage = `url(${kefinRoot}/pages/images/discord.png)`; discordIcon.style.backgroundSize = 'contain'; discordIcon.style.backgroundPosition = 'center'; discordIcon.style.backgroundRepeat = 'no-repeat'; discordIcon.style.height = '35px'; discordLink.appendChild(discordIcon); discordLinkWrapper.appendChild(discordLink); content.appendChild(discordLinkWrapper); } const footer = document.createElement('div'); footer.style.display = 'flex'; footer.style.justifyContent = 'flex-end'; footer.style.gap = '1em'; const okBtn = document.createElement('button'); okBtn.className = 'btnSubmit raised emby-button button-submit'; okBtn.textContent = 'OK'; okBtn.onclick = () => { closeSimpleModal(SUCCESS_MODAL_ID); if (!isInitialInstall) { // Optionally auto-refresh on update (or let user do it manually) // window.location.reload(); } }; footer.appendChild(okBtn); // Close original KefinTweaksConfiguration Modal if it is open const kefinTweaksConfigurationModals = document.querySelectorAll('[data-modal-id="kefinTweaksConfigModal"]'); if (kefinTweaksConfigurationModals.length > 0) { kefinTweaksConfigurationModals.forEach(modal => { modal.remove(); }); } createSimpleModal(SUCCESS_MODAL_ID, `${isInitialInstall ? 'Plugin Installed' : 'Plugin Updated'}`, content, footer); } // Open source configuration modal async function openKefinTweaksSourceModal() { const userIsAdmin = await isAdmin(); if (!userIsAdmin) { showAlertModal('Access Denied', 'You must be an administrator to configure KefinTweaks.
'); return; } const currentConfig = await getKefinTweaksConfig(); const currentRoot = currentConfig?.kefinTweaksRoot || ''; const sourceInfo = parseKefinTweaksSource(currentRoot); const versions = await fetchVersions(); const content = document.createElement('div'); content.style.display = 'flex'; content.style.flexDirection = 'column'; content.style.gap = '1.5em'; // Enabled checkbox const isEnabled = currentConfig?.enabled !== false; const enabledContainer = document.createElement('div'); enabledContainer.style.display = 'flex'; enabledContainer.style.alignItems = 'center'; enabledContainer.style.gap = '0.75em'; const enabledCheckbox = document.createElement('input'); enabledCheckbox.type = 'checkbox'; enabledCheckbox.id = 'kefinTweaksEnabledModal'; enabledCheckbox.className = 'checkbox'; enabledCheckbox.checked = isEnabled; const enabledLabel = document.createElement('label'); enabledLabel.setAttribute('for', 'kefinTweaksEnabledModal'); enabledLabel.style.cursor = 'pointer'; enabledLabel.style.margin = '0'; enabledLabel.style.display = 'flex'; enabledLabel.style.alignItems = 'center'; enabledLabel.style.gap = '0.75em'; const enabledText = document.createElement('span'); enabledText.textContent = isEnabled ? 'Enabled' : 'Disabled'; enabledText.id = 'kefinTweaksEnabledModalLabel'; enabledLabel.appendChild(enabledCheckbox); enabledLabel.appendChild(enabledText); enabledContainer.appendChild(enabledLabel); // Update label text when checkbox changes enabledCheckbox.addEventListener('change', () => { enabledText.textContent = enabledCheckbox.checked ? 'Enabled' : 'Disabled'; }); content.appendChild(enabledContainer); // Source Type dropdown const sourceTypeLabel = document.createElement('label'); sourceTypeLabel.textContent = 'Install From:'; sourceTypeLabel.style.display = 'block'; sourceTypeLabel.style.marginBottom = '0.5em'; const sourceTypeSelect = document.createElement('select'); sourceTypeSelect.className = 'fld emby-select emby-select-withcolor'; sourceTypeSelect.id = 'kefinTweaksSourceType'; sourceTypeSelect.innerHTML = ` `; // Source dropdown (for GitHub) const sourceLabel = document.createElement('label'); sourceLabel.textContent = 'Version:'; sourceLabel.style.display = 'block'; sourceLabel.style.marginBottom = '0.5em'; const sourceSelect = document.createElement('select'); sourceSelect.className = 'fld emby-select emby-select-withcolor'; sourceSelect.id = 'kefinTweaksSource'; sourceSelect.innerHTML = versions.map(v => `` ).join(''); // Custom URL input const customUrlInput = document.createElement('input'); customUrlInput.type = 'text'; customUrlInput.className = 'fld emby-input'; customUrlInput.id = 'kefinTweaksCustomUrl'; customUrlInput.placeholder = 'https://example.com/KefinTweaks/'; customUrlInput.value = sourceInfo.sourceType === 'custom' ? currentRoot : ''; customUrlInput.style.display = sourceInfo.sourceType === 'custom' ? 'block' : 'none'; // Set initial visibility based on source type if (sourceInfo.sourceType === 'custom') { sourceSelect.style.display = 'none'; sourceLabel.style.display = 'none'; customUrlInput.style.display = 'block'; } else { sourceSelect.style.display = 'block'; sourceLabel.style.display = 'block'; customUrlInput.style.display = 'none'; } // Toggle visibility based on source type sourceTypeSelect.onchange = () => { if (sourceTypeSelect.value === 'custom') { sourceSelect.style.display = 'none'; sourceLabel.style.display = 'none'; customUrlInput.style.display = 'block'; } else { sourceSelect.style.display = 'block'; sourceLabel.style.display = 'block'; customUrlInput.style.display = 'none'; } }; content.appendChild(sourceTypeLabel); content.appendChild(sourceTypeSelect); content.appendChild(sourceLabel); content.appendChild(sourceSelect); content.appendChild(customUrlInput); // Footer with buttons const footer = document.createElement('div'); footer.style.display = 'flex'; footer.style.justifyContent = 'flex-end'; footer.style.gap = '1em'; const cancelBtn = document.createElement('button'); cancelBtn.className = 'btnCancel raised emby-button button-cancel'; cancelBtn.textContent = 'Cancel'; cancelBtn.onclick = () => closeSimpleModal(MODAL_ID); const saveBtn = document.createElement('button'); saveBtn.className = 'btnSubmit raised emby-button button-submit'; saveBtn.textContent = (!currentRoot || currentRoot.trim() === '') ? 'Install' : 'Save'; saveBtn.onclick = async () => { try { const sourceType = sourceTypeSelect.value; const source = sourceType === 'custom' ? customUrlInput.value : sourceSelect.value; if (sourceType === 'custom' && (!source || source.trim() === '')) { showAlertModal('Invalid URL', 'Please enter a valid URL for your self-hosted location.
'); return; } const kefinTweaksRoot = buildKefinTweaksRootUrl(sourceType, source); // Load default config from the selected root let defaultConfig; try { defaultConfig = await loadDefaultConfig(kefinTweaksRoot); } catch (error) { console.error('[KefinTweaks Installer] Error loading default config:', error); // Create minimal config if loading fails defaultConfig = { kefinTweaksRoot: kefinTweaksRoot, scripts: {}, homeScreen: {}, exclusiveElsewhere: {}, search: {}, skins: [], defaultSkin: null, themes: [], customMenuLinks: [] }; } // Get enabled state from checkbox const enabledState = enabledCheckbox.checked; // Merge with existing config to preserve user settings const mergedConfig = { ...defaultConfig, kefinTweaksRoot: kefinTweaksRoot, ...(currentConfig || {}), kefinTweaksRoot: kefinTweaksRoot, // Ensure root is updated enabled: enabledState // Update enabled state }; // Remove scriptRoot if it exists (legacy field) if (mergedConfig.scriptRoot) { delete mergedConfig.scriptRoot; } await saveConfigToJavaScriptInjector(mergedConfig); // Update window.KefinTweaksConfig immediately so injector can use it window.KefinTweaksConfig = mergedConfig; window.KefinTweaksConfigEnabled = enabledState; closeSimpleModal(MODAL_ID); // Update the plugin card with new version/status await updateKefinTweaksPluginCard(); // Determine if this is initial installation const isInitialInstall = !currentConfig || !currentConfig.kefinTweaksRoot || currentConfig.kefinTweaksRoot === ''; // Get version for display const sourceInfo = parseKefinTweaksSource(kefinTweaksRoot); let versionDisplay = ''; if (sourceInfo.sourceType === 'custom') { versionDisplay = SELF_HOSTED_NAME; } else if (sourceInfo.version === DEVELOPMENT_NAME) { versionDisplay = DEVELOPMENT_NAME; } else if (sourceInfo.version === LATEST_RELEASE_NAME) { // Fetch the actual latest version number try { const response = await fetch('https://api.github.com/repos/ranaldsgift/KefinTweaks/releases/latest'); if (response.ok) { const data = await response.json(); const versionNumber = data.tag_name.replace(/^v/, ''); versionDisplay = `${versionNumber} (${LATEST_RELEASE_NAME})`; } else { versionDisplay = LATEST_RELEASE_NAME; } } catch (error) { console.warn('[KefinTweaks Installer] Error fetching latest version:', error); versionDisplay = LATEST_RELEASE_NAME; } } else { versionDisplay = sourceInfo.version; } // Load injector.js from the configured root await loadInjectorFromRoot(kefinTweaksRoot); // Show success modal await showSuccessModal(isInitialInstall, versionDisplay); } catch (error) { console.error('[KefinTweaks Installer] Error saving configuration:', error); showAlertModal('Configuration Error', `Error saving configuration: ${error.message}
`); } }; const uninstallBtn = document.createElement('button'); uninstallBtn.className = 'btnCancel raised emby-button button-cancel'; uninstallBtn.textContent = 'Uninstall'; uninstallBtn.style.marginRight = 'auto'; // Hide uninstall button if KefinTweaks isn't installed yet (no root URL) if (!currentRoot || currentRoot.trim() === '') { uninstallBtn.style.display = 'none'; } uninstallBtn.onclick = () => { closeSimpleModal(MODAL_ID); showConfirmationModal( 'Are you sure you want to uninstall KefinTweaks?KefinTweaks has been uninstalled. The installer has been disabled and your configuration has been backed up.
Please refresh your page.
You can re-enable KefinTweaks at any time from the JS Injector Plugin.'
);
} catch (error) {
console.error('[KefinTweaks Installer] Error uninstalling:', error);
showAlertModal(
'Uninstall Error',
`
Error uninstalling KefinTweaks: ${error.message}
` ); } }, () => { // User cancelled, reopen the source modal openKefinTweaksSourceModal(); } ); }; footer.appendChild(saveBtn); footer.appendChild(cancelBtn); footer.appendChild(uninstallBtn); createSimpleModal(MODAL_ID, 'KefinTweaks Plugin Settings', content, footer); } /** * Uninstall KefinTweaks * 1. Renames KefinTweaks-Config to KefinTweaks-Config-Backup-Timestamp * 2. Disables the installer script (script containing kefinTweaks-plugin.js) */ async function uninstallKefinTweaks() { try { const pluginId = await findJavaScriptInjectorPlugin(); const injectorConfig = await getJavaScriptInjectorConfig(pluginId); if (!injectorConfig.CustomJavaScripts) { throw new Error('No custom scripts found in JS Injector configuration'); } // Step 1: Rename KefinTweaks-Config to backup const configScriptIndex = injectorConfig.CustomJavaScripts.findIndex( script => script.Name === 'KefinTweaks-Config' ); if (configScriptIndex !== -1) { const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5); injectorConfig.CustomJavaScripts[configScriptIndex].Name = `KefinTweaks-Config-Backup-${timestamp}`; injectorConfig.CustomJavaScripts[configScriptIndex].Enabled = false; console.log('[KefinTweaks Installer] Renamed KefinTweaks-Config to backup and disabled it'); } // Step 2: Find and disable installer script // Look for script containing kefinTweaks-plugin.js (flexible pattern to match script.src*kefinTweaks-plugin.js) const installerScriptIndex = injectorConfig.CustomJavaScripts.findIndex( script => script.Script && /script\.src.*kefinTweaks-plugin\.js/i.test(script.Script) ); if (installerScriptIndex !== -1) { injectorConfig.CustomJavaScripts[installerScriptIndex].Enabled = false; console.log('[KefinTweaks Installer] Disabled installer script'); } else { console.warn('[KefinTweaks Installer] Installer script not found in CustomJavaScripts'); } // Save the updated configuration const server = ApiClient._serverAddress; const response = await fetch(`${server}/Plugins/${pluginId}/Configuration`, { method: 'POST', headers: { 'Authorization': getAuthHeader(), 'Content-Type': 'application/json' }, body: JSON.stringify(injectorConfig) }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } console.log('[KefinTweaks Installer] Uninstall completed successfully'); return true; } catch (error) { console.error('[KefinTweaks Installer] Error uninstalling:', error); throw error; } } /** * Update the plugin card with new version and status information */ async function updateKefinTweaksPluginCard() { const card = document.querySelector('[data-id="kefinTweaksPlugin"]'); if (!card) { // Card doesn't exist yet, try to add it const pluginsPage = document.querySelector('#pluginsPage:not(.hide)'); if (pluginsPage) { let installedPlugins = pluginsPage.querySelector('.installedPlugins'); if (!installedPlugins) { installedPlugins = document.querySelector('#pluginsPage:not(.hide)>div>div>div:last-child>div'); } if (installedPlugins) { addKefinTweaksPluginCard(installedPlugins); } } return; } try { const config = await getKefinTweaksConfig(); const root = config?.kefinTweaksRoot || ''; const sourceInfo = parseKefinTweaksSource(root); // Determine version display text let versionDisplay = 'Installation Pending'; if (root) { if (sourceInfo.sourceType === 'custom') { versionDisplay = SELF_HOSTED_NAME; } else if (sourceInfo.version === DEVELOPMENT_NAME) { versionDisplay = DEVELOPMENT_NAME; } else if (sourceInfo.version === LATEST_RELEASE_NAME) { // Fetch the actual latest version number try { const response = await fetch('https://api.github.com/repos/ranaldsgift/KefinTweaks/releases/latest'); if (response.ok) { const data = await response.json(); const versionNumber = data.tag_name.replace(/^v/, ''); versionDisplay = `${versionNumber} (${LATEST_RELEASE_NAME})`; } else { versionDisplay = LATEST_RELEASE_NAME; } } catch (error) { console.warn('[KefinTweaks Installer] Error fetching latest version:', error); versionDisplay = LATEST_RELEASE_NAME; } } else { // Specific version versionDisplay = sourceInfo.version; } } // Update data attributes card.setAttribute('data-version', versionDisplay); card.setAttribute('data-status', root ? 'Active' : 'Pending'); // Check if this is a Material-UI card const isMuiCard = card.classList.contains('MuiGrid-root'); if (isMuiCard) { // Handle Material-UI card structure const versionElement = card.querySelector('.MuiTypography-root.MuiTypography-body2'); if (versionElement) { versionElement.textContent = versionDisplay; } } else { // Handle standard card structure const cardTextElements = card.querySelectorAll('.cardText'); if (cardTextElements.length > 0) { // Update first cardText (plugin name with version) const nameElement = cardTextElements[0]; const versionSpan = nameElement.querySelector('.cardText-secondary'); if (versionSpan) { versionSpan.textContent = versionDisplay; } else { // If no secondary span exists, create one nameElement.innerHTML = `KefinTweaks${versionDisplay}`; } } // Determine status text const statusText = root ? 'Status Active' : 'Click to Install'; // Update status text (usually last cardText) if (cardTextElements.length > 0) { cardTextElements[cardTextElements.length - 1].textContent = statusText; } } } catch (error) { console.error('[KefinTweaks Installer] Error updating plugin card:', error); } } /** * Remove the plugin card from the DOM */ function removeKefinTweaksPluginCard() { const card = document.querySelector('[data-id="kefinTweaksPlugin"]'); if (card) { card.remove(); console.log('[KefinTweaks Installer] Plugin card removed'); } } // Make function globally accessible window.openKefinTweaksSourceModal = openKefinTweaksSourceModal; // Add plugin card to plugins page function addKefinTweaksPluginCard(installedPlugins) { if (!installedPlugins) { return; } // Check if card already exists in either container if (document.querySelector('[data-id="kefinTweaksPlugin"]')) { return; } const pluginsPage = document.querySelector('#pluginsPage:not(.hide)'); if (!pluginsPage) { return; } // Find or create frontEndPlugins container let frontEndPlugins = pluginsPage.querySelector('.frontEndPlugins'); if (!frontEndPlugins) { // Create the heading const heading = document.createElement('div'); heading.className = 'sectionTitleContainer flex align-items-center'; heading.innerHTML = '