Files
unraid-glass/sidebar.js
Kaloyan Danchev 55ad7c66bc Fix 23 theme bugs across all pages: toggles, text visibility, icons, bars, shadows, layout
Phase 1: Fix toggle switch knob positioning (absolute + translateY centering)
Phase 2: Fix text visibility on Main/Shares thead td, Plugins descriptions, Settings/Tools PanelText labels
Phase 3: Fix icon coloring - JS replacement for Tailscale/ZFS Master with selfh.st light SVGs, CSS invert for remaining plugin imgs
Phase 4: Softer box-shadow on inner tables, stacked Panel rounding for Array Op
Phase 5: Browse table styling, form glass containers, Apps detail panel transparency
Phase 6: Main page usage bars (8px thin + gradient), plugin/VM button visibility
Phase 7: Header overflow fix, Docker folder-preview softening, Apps ribbon clipping, ZFS Master toggle spacing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 22:07:24 +02:00

546 lines
17 KiB
JavaScript

(function() {
'use strict';
var KEY = 'unraid-sidebar-expanded';
// Restore state immediately (before DOM ready to avoid flash)
if (localStorage.getItem(KEY) === '1') {
document.documentElement.classList.add('sidebar-expanded');
}
function init() {
var menu = document.getElementById('menu');
if (!menu) return;
// --- Unraid logo (absolute positioned at very top of sidebar) ---
if (!document.getElementById('sidebar-logo')) {
var logo = document.createElement('img');
logo.id = 'sidebar-logo';
logo.src = '/custom/assets/icons/unraid.svg';
logo.alt = '';
menu.appendChild(logo);
}
// --- Toggle button with chevron (below logo) ---
if (!document.getElementById('sidebar-toggle-btn')) {
var btn = document.createElement('button');
btn.id = 'sidebar-toggle-btn';
btn.type = 'button';
btn.title = 'Toggle Sidebar';
btn.innerHTML = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>';
btn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
var expanded = document.documentElement.classList.toggle('sidebar-expanded');
localStorage.setItem(KEY, expanded ? '1' : '0');
});
menu.appendChild(btn);
}
// --- Lock button icon state ---
initLockButton();
// --- Search overlay (delayed to wait for search plugin init) ---
setTimeout(initSearchOverlay, 800);
}
function initLockButton() {
var lockItem = document.querySelector('.nav-item.LockButton');
if (!lockItem) return;
var lockLink = lockItem.querySelector('a');
if (!lockLink) return;
function updateLockState() {
var title = lockLink.getAttribute('title') || '';
if (title.toLowerCase().indexOf('unlock') > -1) {
lockItem.classList.add('is-locked');
} else {
lockItem.classList.remove('is-locked');
}
}
updateLockState();
var observer = new MutationObserver(updateLockState);
observer.observe(lockLink, { attributes: true, attributeFilter: ['title'] });
}
function initSearchOverlay() {
if (!window.jQuery) return;
var searchItem = document.querySelector('.nav-item.gui_search');
if (!searchItem) return;
// Remove default hover-based open/close handlers
$(searchItem).off('mouseenter mouseleave');
var link = searchItem.querySelector('a');
if (!link) return;
// Remove any inline onclick handler (Unraid sets onclick="gui_search()")
link.removeAttribute('onclick');
link.onclick = null;
// Replace hover with click
link.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
toggleSearch();
});
// Add Cmd+K / Ctrl+K keyboard shortcut (sidebar theme doesn't have one by default)
document.addEventListener('keydown', function(e) {
var isMac = navigator.platform.indexOf('Mac') > -1;
if ((isMac ? e.metaKey : e.ctrlKey) && (e.key === 'k' || e.keyCode === 75)) {
if ($('[role="modal"]:visible').length) return;
e.preventDefault();
toggleSearch();
}
});
}
function toggleSearch() {
var existing = document.getElementById('guiSearchBoxSpan');
if (existing) {
closeSearch();
return;
}
// Call original gui_search to create the input
if (typeof gui_search === 'function') {
gui_search();
}
// Move search box to <body> — backdrop-filter on #menu creates a new
// containing block, so position:fixed inside it is relative to #menu
// and gets clipped by overflow:hidden. Moving to body fixes this.
var searchSpan = document.getElementById('guiSearchBoxSpan');
if (searchSpan) {
document.body.appendChild(searchSpan);
}
// Create backdrop overlay
if (!document.getElementById('search-backdrop')) {
var backdrop = document.createElement('div');
backdrop.id = 'search-backdrop';
backdrop.addEventListener('click', function() {
closeSearch();
});
document.body.appendChild(backdrop);
}
// Override blur/keydown behavior on the input
var input = document.getElementById('guiSearchBox');
if (input && window.jQuery) {
$(input).off('blur keydown');
input.addEventListener('blur', function() {
setTimeout(function() {
var span = document.getElementById('guiSearchBoxSpan');
if (span && !span.contains(document.activeElement)) {
closeSearch();
}
}, 300);
});
input.addEventListener('keydown', function(e) {
if (e.key === 'Escape' || e.keyCode === 27) {
e.preventDefault();
closeSearch();
}
});
input.focus();
}
}
function closeSearch() {
// Remove the search span directly (works whether moved to body or not)
var span = document.getElementById('guiSearchBoxSpan');
if (span) span.remove();
var backdrop = document.getElementById('search-backdrop');
if (backdrop) backdrop.remove();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
// =============================================================
// Icon Replacement Module — selfh.st CDN icons for Docker/VMs
// =============================================================
(function() {
'use strict';
var CDN_PNG = '/custom/assets/icons';
var CDN_SVG = '/custom/assets/icons';
// Container name → selfh.st icon name (lowercase)
// Only needed for names that don't auto-match.
// Add entries here or use the glass.icon Docker label.
var ICON_MAP = {
'autokuma': 'uptime-kuma',
'uptimekuma': 'uptime-kuma',
'uptime-kuma-api': 'uptime-kuma',
'homeassistant_inabox': 'home-assistant',
'dockersocket': 'docker',
'pgadmin4': 'pgadmin',
'postgresql17': 'postgresql',
'woodpecker-agent': 'woodpecker-ci',
'woodpecker-server': 'woodpecker-ci',
'rustdesk-hbbr': 'rustdesk',
'rustdesk-hbbs': 'rustdesk',
'rustfs': 'rustdesk',
'netbox-redis-cache': 'redis',
'netbox-worker': 'netbox',
'netdisco-backend': 'networking-toolbox',
'netdisco-web': 'networking-toolbox',
'diode-agent': 'netbox',
'diode-auth': 'netbox',
'diode-auth-bootstrap': 'netbox',
'diode-hydra': 'netbox',
'diode-hydra-migrate': 'netbox',
'diode-ingester': 'netbox',
'diode-ingress': 'netbox',
'diode-reconciler': 'netbox',
'authentik-worker': 'authentik',
'adguardhome': 'adguard-home',
'adguardhome-sync': 'adguard-home',
'timemachine': 'apple',
'libation': 'audible',
'netalertx': 'netalertx',
'actual-budget': 'actual-budget',
'speedtest-tracker': 'speedtest-tracker',
'open-webui': 'open-webui',
'urbackup': 'networking-toolbox',
'seekandwatch': 'databasement',
'unmarr': 'homarr',
'dockhand': 'docker'
};
// Resolve container name to selfh.st CDN URL
// mode: 'color' for expanded containers, 'light' for dashboard/folder-preview
function resolveIconUrl(name, mode) {
var key = name.toLowerCase();
var iconName = ICON_MAP[key] || key;
if (mode === 'light') {
return CDN_SVG + '/' + iconName + '-light.svg';
}
return CDN_PNG + '/' + iconName + '.png';
}
// Extract container name from an <img> element by walking up the DOM
function getContainerName(img) {
// Strategy 1: sibling .inner > .appname text
var outer = img.closest('span.outer');
if (outer) {
var appname = outer.querySelector('span.appname');
if (appname) {
var a = appname.querySelector('a');
return (a ? a.textContent : appname.textContent).trim();
}
}
// Strategy 2: .inner > first non-state span (Dashboard view)
if (outer) {
var inner = outer.querySelector('span.inner');
if (inner) {
var nameSpan = inner.querySelector('span:not(.state)');
if (nameSpan && nameSpan.textContent.trim()) {
return nameSpan.textContent.trim();
}
}
}
// Strategy 3: .inner > a.exec text (FolderView uses this)
if (outer) {
var exec = outer.querySelector('a.exec');
if (exec) return exec.textContent.trim();
}
// Strategy 4: parent row ct-name
var row = img.closest('tr');
if (row) {
var ctName = row.querySelector('td.ct-name span.appname');
if (ctName) {
var a2 = ctName.querySelector('a');
return (a2 ? a2.textContent : ctName.textContent).trim();
}
}
// Strategy 4: extract from icon src path (fallback)
var src = img.getAttribute('src') || '';
var match = src.match(/images\/(.+?)-icon\.png/);
if (match) return match[1];
return null;
}
// Determine icon mode based on DOM context
function getIconMode(img) {
// Dashboard → light
if (img.closest('table.dashboard')) return 'light';
// Folder preview (collapsed row) → light
if (img.closest('div.folder-preview')) return 'light';
// Expanded container inside folder → color
if (img.closest('tr.folder-element')) return 'color';
// Regular container row → color
return 'color';
}
// Replace all container icons on the page
function replaceIcons() {
var imgs = document.querySelectorAll('img.img');
imgs.forEach(function(img) {
// Skip sidebar logo and non-container icons
if (img.id === 'sidebar-logo') return;
// Skip folder category icons (SVGs managed by FolderView)
if (img.classList.contains('folder-img') && img.closest('td.folder-name')) return;
if (img.classList.contains('folder-img-docker')) return;
var name = getContainerName(img);
if (!name) return;
// Skip folder names (they start with "folder-")
if (name.indexOf('folder-') === 0) return;
var mode = getIconMode(img);
var url = resolveIconUrl(name, mode);
// Only replace if different (avoid infinite MutationObserver loop)
if (img.src !== url && img.getAttribute('src') !== url) {
img.src = url;
// Remove cache-busting onerror that resets to question mark
img.removeAttribute('onerror');
}
});
}
// Debounce to avoid excessive calls during DOM manipulation
var replaceTimer = null;
function debouncedReplace() {
if (replaceTimer) clearTimeout(replaceTimer);
replaceTimer = setTimeout(function() {
replaceIcons();
replacePluginIcons();
}, 200);
}
// Watch for DOM changes (AJAX loads, FolderView manipulation)
function startObserver() {
var target = document.getElementById('displaybox') || document.body;
var observer = new MutationObserver(function(mutations) {
var dominated = false;
for (var i = 0; i < mutations.length; i++) {
if (mutations[i].addedNodes.length > 0) {
dominated = true;
break;
}
}
if (dominated) debouncedReplace();
});
observer.observe(target, { childList: true, subtree: true });
}
// Plugin icon source → selfh.st light SVG replacement
// Maps partial src paths to icon names in /custom/assets/icons/
var PLUGIN_ICON_MAP = {
'/plugins/tailscale/': 'tailscale',
'/plugins/zfs.master/': 'openzfs',
// SNMP has no selfh.st icon — leave for CSS invert filter
};
// Replace plugin icons on Settings/Plugins/Tools pages with selfh.st light SVGs.
// Also replaces Tailscale sidebar icon (img[alt="Tailscale"]).
function replacePluginIcons() {
// Settings/Tools/Plugins grid icons
var imgs = document.querySelectorAll('#displaybox img, img[alt="Tailscale"]');
imgs.forEach(function(img) {
// Skip container icons (handled by replaceIcons)
if (img.classList.contains('img')) return;
// Skip sidebar logo
if (img.id === 'sidebar-logo') return;
// Skip already replaced
if (img.getAttribute('data-glass-icon')) return;
var src = img.getAttribute('src') || '';
var keys = Object.keys(PLUGIN_ICON_MAP);
for (var i = 0; i < keys.length; i++) {
if (src.indexOf(keys[i]) !== -1) {
var iconName = PLUGIN_ICON_MAP[keys[i]];
var url = CDN_SVG + '/' + iconName + '-light.svg';
img.src = url;
img.setAttribute('data-glass-icon', iconName);
img.removeAttribute('onerror');
return;
}
}
// Also handle Tailscale by alt attribute (sidebar)
if (img.alt === 'Tailscale') {
var tsUrl = CDN_SVG + '/tailscale-light.svg';
if (src !== tsUrl) {
img.src = tsUrl;
img.setAttribute('data-glass-icon', 'tailscale');
img.removeAttribute('onerror');
}
}
});
}
// Initialize when DOM is ready
function initIcons() {
// Initial replacement
setTimeout(replaceIcons, 500);
setTimeout(replacePluginIcons, 500);
// Second pass after FolderView finishes its DOM work
setTimeout(replaceIcons, 2000);
setTimeout(replacePluginIcons, 2000);
// Start watching for future changes
startObserver();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initIcons);
} else {
initIcons();
}
})();
// =============================================================
// Pie Chart Recoloring — vibrant theme colors for System donuts
// =============================================================
(function() {
'use strict';
// Unraid's muted defaults → vibrant theme replacements
var COLOR_MAP = {
'rgb(68,119,170)': '#7EB8DA', // accent blue (used)
'rgb(204,204,204)': 'rgba(255,255,255,0.10)', // free → subtle
'rgb(34,136,51)': '#4ade80', // green
'rgb(170,68,153)': '#f97316', // purple → orange
'rgb(238,102,119)': '#ef4444', // red
'rgb(0,153,136)': '#22d3ee', // teal → cyan
'rgb(221,204,119)': '#facc15', // yellow
'rgb(136,34,85)': '#f87171', // dark red → light red
'rgb(17,119,51)': '#22c55e', // dark green → bright green
'rgb(51,34,136)': '#818cf8', // dark purple → indigo
'rgb(102,153,204)': '#93c5fd' // light blue
};
function recolorPie(pie) {
var style = pie.getAttribute('style');
if (!style || style.indexOf('conic-gradient') === -1) return;
var newStyle = style;
var keys = Object.keys(COLOR_MAP);
for (var i = 0; i < keys.length; i++) {
// Replace with global flag via split/join
newStyle = newStyle.split(keys[i]).join(COLOR_MAP[keys[i]]);
}
if (newStyle !== style) {
pie.setAttribute('style', newStyle);
}
}
function recolorAll() {
var pies = document.querySelectorAll('div.pie');
pies.forEach(recolorPie);
}
function startObserver() {
var target = document.getElementById('displaybox') || document.body;
var observer = new MutationObserver(function(mutations) {
for (var i = 0; i < mutations.length; i++) {
var m = mutations[i];
if (m.type === 'attributes' && m.attributeName === 'style') {
var el = m.target;
if (el.classList && el.classList.contains('pie')) {
recolorPie(el);
}
}
if (m.type === 'childList' && m.addedNodes.length > 0) {
recolorAll();
}
}
});
observer.observe(target, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['style']
});
}
function init() {
setTimeout(recolorAll, 600);
setTimeout(recolorAll, 2000);
startObserver();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
// =============================================================
// System Legend Colors — match pie segment colors in legends
// =============================================================
(function() {
'use strict';
// Unraid default legend colors → vibrant theme colors
var LEGEND_MAP = {
'rgb(68, 119, 170)': '#7EB8DA',
'rgb(204, 204, 204)': 'rgba(255,255,255,0.35)',
'rgb(34, 136, 51)': '#4ade80',
'rgb(170, 68, 153)': '#f97316',
'rgb(238, 102, 119)': '#ef4444',
'rgb(0, 153, 136)': '#22d3ee',
'rgb(221, 204, 119)': '#facc15',
'rgb(136, 34, 85)': '#f87171',
'rgb(17, 119, 51)': '#22c55e',
'rgb(51, 34, 136)': '#818cf8',
'rgb(102, 153, 204)': '#93c5fd'
};
function recolorLegends() {
var icons = document.querySelectorAll('#dynamic i.fa-circle');
icons.forEach(function(icon) {
var currentColor = icon.style.color;
if (!currentColor) return;
var mapped = LEGEND_MAP[currentColor];
if (mapped) {
icon.style.setProperty('color', mapped, 'important');
}
});
}
function startObserver() {
var target = document.getElementById('dynamic') || document.getElementById('displaybox') || document.body;
var observer = new MutationObserver(function() {
recolorLegends();
});
observer.observe(target, { childList: true, subtree: true });
}
function init() {
setTimeout(recolorLegends, 700);
setTimeout(recolorLegends, 2500);
startObserver();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();