// KefinTweaks Custom Menu Links Configuration UI // Modular configuration modal for managing custom menu links (function() { 'use strict'; const LOG = (...args) => console.log('[KefinTweaks CustomMenuLinks Config]', ...args); const ERR = (...args) => console.error('[KefinTweaks CustomMenuLinks Config]', ...args); const MODAL_ID = 'kefin-custommenulinks-config'; /** * Load configuration from window.KefinTweaksConfig */ async function loadConfig() { const config = window.KefinTweaksConfig || {}; return { customMenuLinks: Array.isArray(config.customMenuLinks) ? config.customMenuLinks : [] }; } /** * Save configuration to JS Injector */ async function saveConfig(customMenuLinks) { try { if (!window.KefinTweaksConfig) { window.KefinTweaksConfig = {}; } window.KefinTweaksConfig.customMenuLinks = customMenuLinks; // Placement is per-link; do not keep the legacy global top-nav flag delete window.KefinTweaksConfig.showCustomMenuLinksInTopNav; if (window.KefinTweaksUtils && window.KefinTweaksUtils.saveConfigToJavaScriptInjector) { await window.KefinTweaksUtils.saveConfigToJavaScriptInjector(); LOG('Configuration saved to JS Injector'); return true; } else if (window.KefinTweaksConfiguration && window.KefinTweaksConfiguration.findJavaScriptInjectorPlugin) { const pluginId = await window.KefinTweaksConfiguration.findJavaScriptInjectorPlugin(); if (pluginId) { await window.KefinTweaksConfiguration.saveConfigToJavaScriptInjector(window.KefinTweaksConfig); LOG('Configuration saved to JS Injector (fallback)'); return true; } ERR('saveConfigToJavaScriptInjector not available'); return false; } else { ERR('saveConfigToJavaScriptInjector not available'); return false; } } catch (error) { ERR('Error saving config:', error); return false; } } /** * Build configuration HTML */ function buildConfigHTML(config) { return `
Custom Menu Links JSON
Add custom menu links with per-link placements. Require name and either url or action (a dotted path on window, e.g. MyPlugin.openDialog). Defaults: sideMenu true, topNavigation "none", userMenu false. Use collapsed with topNavigation: "main" on v12 to place under More. Optional order overrides top-nav position: for main, left-to-right child index (0 = first); for right, right-to-left (0 = last child).
These links are not added by modifying your config.json, so if you wish for these to appear on supported third party clients which don't use jellyfin-web, you will want to manually edit your config.json rather than adding the links here. You can find the information about doing that here.
View Example Format
[
  {
    "name": "My Custom Link",
    "icon": "link",
    "url": "#/userpluginsettings.html?pageUrl=https://domain.com/custom-page",
    "openInNewTab": false,
    "sideMenu": true,
    "topNavigation": "none",
    "userMenu": false,
    "collapsed": false
  },
  {
    "name": "Top Nav First",
    "icon": "star",
    "url": "#/home",
    "sideMenu": false,
    "topNavigation": "main",
    "order": 0
  },
  {
    "name": "Top Nav Link",
    "icon": "star",
    "url": "#/home",
    "sideMenu": false,
    "topNavigation": "main",
    "collapsed": true
  },
  {
    "name": "Header Right Near End",
    "icon": "extension",
    "action": "MyPlugin.openDialog",
    "sideMenu": false,
    "topNavigation": "right",
    "order": 1
  },
  {
    "name": "External Link",
    "icon": "open_in_new",
    "url": "https://example.com",
    "openInNewTab": true,
    "sideMenu": true,
    "topNavigation": "none"
  }
]
`; } /** * Collect configuration from form */ function collectConfig() { try { const jsonText = document.getElementById('customMenuLinksJson')?.value || '[]'; const customMenuLinks = JSON.parse(jsonText); return { customMenuLinks }; } catch (e) { ERR('Error parsing custom menu links JSON:', e); alert('Invalid JSON format. Please check your input.'); return null; } } /** * Open configuration modal */ async function openConfigModal() { try { const config = await loadConfig(); const content = document.createElement('div'); content.innerHTML = buildConfigHTML(config); const footer = document.createElement('div'); footer.style.display = 'flex'; footer.style.gap = '0.75em'; footer.style.justifyContent = 'flex-end'; footer.innerHTML = ` `; window.ModalSystem.create({ id: MODAL_ID, title: 'Custom Menu Links Configuration', content: content, footer: footer, closeOnBackdrop: true, closeOnEscape: true, showCloseButton: true, dialogStyle: window.innerWidth >= 900 ? { maxWidth: '90vw', width: '1400px', height: '90vh' } : undefined, onOpen: (modal) => { const saveBtn = modal.dialogFooter.querySelector('#save-custommenulinks-config-btn'); if (saveBtn) { saveBtn.addEventListener('click', async () => { const collected = collectConfig(); if (!collected) return; const success = await saveConfig(collected.customMenuLinks); if (success) { if (window.KefinTweaksToaster && window.KefinTweaksToaster.toast) { window.KefinTweaksToaster.toast('Custom Menu Links configuration saved!'); } window.ModalSystem.close(MODAL_ID); } else { alert('Error saving configuration. Please ensure the JavaScript Injector plugin is installed and you have administrator permissions.'); } }); } } }); LOG('Custom Menu Links configuration modal opened'); } catch (error) { ERR('Error opening custom menu links config modal:', error); alert('Error opening configuration: ' + error.message); } } window.KefinTweaksFeatureConfigs = window.KefinTweaksFeatureConfigs || {}; window.KefinTweaksFeatureConfigs.customMenuLinks = { openConfigModal }; console.log('[KefinTweaks CustomMenuLinks Config] Script loaded'); })();