Add client-side icon replacement, Dashboard fixes, and Apps page styling
- Add JS icon replacement module to sidebar.js that swaps container icons with selfh.st CDN versions on Docker page and Dashboard via MutationObserver - Fix Dashboard container name extraction (Strategy 2: span.inner > span) for containers whose original icon was missing (question.png fallback) - Add CSS filter for Dashboard folder icons (folder-img-docker class) - Add icon mappings for containers without CDN matches (netdisco→networking-toolbox, urbackup→networking-toolbox, seekandwatch→databasement, unmarr→homarr, dockhand→docker) - Fix shadowed netdisco-web/netdisco-backend entries in update-icons.sh - Add Community Applications (Apps page) glass theme overrides Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
190
sidebar.js
190
sidebar.js
@@ -145,3 +145,193 @@
|
||||
init();
|
||||
}
|
||||
})();
|
||||
|
||||
// =============================================================
|
||||
// Icon Replacement Module — selfh.st CDN icons for Docker/VMs
|
||||
// =============================================================
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
var CDN_PNG = 'https://cdn.jsdelivr.net/gh/selfhst/icons@main/png';
|
||||
var CDN_SVG = 'https://cdn.jsdelivr.net/gh/selfhst/icons@main/svg';
|
||||
|
||||
// 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(replaceIcons, 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 });
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
function initIcons() {
|
||||
// Initial replacement
|
||||
setTimeout(replaceIcons, 500);
|
||||
// Second pass after FolderView finishes its DOM work
|
||||
setTimeout(replaceIcons, 2000);
|
||||
// Start watching for future changes
|
||||
startObserver();
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initIcons);
|
||||
} else {
|
||||
initIcons();
|
||||
}
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user