Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fbacc229ea | |||
| c9932f275a | |||
| 596e828223 | |||
| 673366db5f | |||
| 969c240553 | |||
| b17efa805f | |||
| 01fcb31052 | |||
| fb07089ee5 | |||
| 494f689008 |
@@ -12,6 +12,8 @@ Custom CSS + JS theme for Unraid 7.2.3 WebGUI. B&W mountain wallpaper with frost
|
|||||||
|------|---------|--------------------------|---------------------|
|
|------|---------|--------------------------|---------------------|
|
||||||
| `style.css` | Main CSS theme | `/boot/config/plugins/custom.css/style.css` | `/usr/local/emhttp/plugins/custom.css/style.css` |
|
| `style.css` | Main CSS theme | `/boot/config/plugins/custom.css/style.css` | `/usr/local/emhttp/plugins/custom.css/style.css` |
|
||||||
| `sidebar.js` | Sidebar toggle + search overlay | `/boot/config/plugins/custom.css/assets/sidebar.js` | `/usr/local/emhttp/plugins/custom.css/assets/sidebar.js` |
|
| `sidebar.js` | Sidebar toggle + search overlay | `/boot/config/plugins/custom.css/assets/sidebar.js` | `/usr/local/emhttp/plugins/custom.css/assets/sidebar.js` |
|
||||||
|
| `update-icons.sh` | Replace Docker/VM icons with selfh.st CDN icons | `/boot/config/plugins/custom.css/assets/update-icons.sh` | `/usr/local/emhttp/plugins/custom.css/assets/update-icons.sh` |
|
||||||
|
| `vm-icons.conf` | VM name → icon name mapping | `/boot/config/plugins/custom.css/assets/vm-icons.conf` | — |
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
@@ -21,6 +23,9 @@ scp -i ~/.ssh/id_ed25519_unraid -P 422 style.css root@192.168.10.20:/boot/config
|
|||||||
scp -i ~/.ssh/id_ed25519_unraid -P 422 style.css root@192.168.10.20:/usr/local/emhttp/plugins/custom.css/style.css
|
scp -i ~/.ssh/id_ed25519_unraid -P 422 style.css root@192.168.10.20:/usr/local/emhttp/plugins/custom.css/style.css
|
||||||
scp -i ~/.ssh/id_ed25519_unraid -P 422 sidebar.js root@192.168.10.20:/boot/config/plugins/custom.css/assets/sidebar.js
|
scp -i ~/.ssh/id_ed25519_unraid -P 422 sidebar.js root@192.168.10.20:/boot/config/plugins/custom.css/assets/sidebar.js
|
||||||
scp -i ~/.ssh/id_ed25519_unraid -P 422 sidebar.js root@192.168.10.20:/usr/local/emhttp/plugins/custom.css/assets/sidebar.js
|
scp -i ~/.ssh/id_ed25519_unraid -P 422 sidebar.js root@192.168.10.20:/usr/local/emhttp/plugins/custom.css/assets/sidebar.js
|
||||||
|
scp -i ~/.ssh/id_ed25519_unraid -P 422 update-icons.sh root@192.168.10.20:/boot/config/plugins/custom.css/assets/update-icons.sh
|
||||||
|
scp -i ~/.ssh/id_ed25519_unraid -P 422 update-icons.sh root@192.168.10.20:/usr/local/emhttp/plugins/custom.css/assets/update-icons.sh
|
||||||
|
scp -i ~/.ssh/id_ed25519_unraid -P 422 vm-icons.conf root@192.168.10.20:/boot/config/plugins/custom.css/assets/vm-icons.conf
|
||||||
```
|
```
|
||||||
|
|
||||||
JS injection is handled by `CustomJS_Loader.page` on the server (persisted via `/boot/config/go`).
|
JS injection is handled by `CustomJS_Loader.page` on the server (persisted via `/boot/config/go`).
|
||||||
@@ -106,6 +111,37 @@ The `dynamix.gui.search` plugin uses hover-trigger in sidebar theme. We replace
|
|||||||
- Cmd+K / Ctrl+K keyboard shortcut
|
- Cmd+K / Ctrl+K keyboard shortcut
|
||||||
- Escape to close
|
- Escape to close
|
||||||
|
|
||||||
|
## Custom Docker/VM Icons
|
||||||
|
|
||||||
|
Icons are sourced from [selfh.st/icons](https://selfh.st/icons/) via jsDelivr CDN.
|
||||||
|
|
||||||
|
### How it works
|
||||||
|
`update-icons.sh` reads a `glass.icon` Docker label from each container, downloads the matching PNG from `https://cdn.jsdelivr.net/gh/selfhst/icons@main/png/{name}.png`, and replaces Unraid's cached icon files. This works on Docker page, VM page, and Dashboard — no client-side JS needed.
|
||||||
|
|
||||||
|
### Docker label convention
|
||||||
|
Add label `glass.icon` to containers:
|
||||||
|
- Short name: `glass.icon=plex` → fetches from selfh.st CDN
|
||||||
|
- Full URL: `glass.icon=https://example.com/icon.png` → fetches from URL
|
||||||
|
- No label: auto-detects from container name (lowercase + CDN HEAD check)
|
||||||
|
|
||||||
|
### VM icons
|
||||||
|
Edit `vm-icons.conf` with `VM_NAME=icon_name` entries (one per line).
|
||||||
|
|
||||||
|
### Running
|
||||||
|
```bash
|
||||||
|
# Dry run (show what would change)
|
||||||
|
ssh -i ~/.ssh/id_ed25519_unraid root@192.168.10.20 -p 422 'bash /boot/config/plugins/custom.css/assets/update-icons.sh --dry-run'
|
||||||
|
# Real run
|
||||||
|
ssh -i ~/.ssh/id_ed25519_unraid root@192.168.10.20 -p 422 'bash /boot/config/plugins/custom.css/assets/update-icons.sh'
|
||||||
|
# Force re-download all
|
||||||
|
ssh -i ~/.ssh/id_ed25519_unraid root@192.168.10.20 -p 422 'bash /boot/config/plugins/custom.css/assets/update-icons.sh --force'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Icon paths on Unraid
|
||||||
|
- RAM cache: `/usr/local/emhttp/state/plugins/dynamix.docker.manager/images/{Name}-icon.png`
|
||||||
|
- Persistent: `/var/lib/docker/unraid/images/{Name}-icon.png`
|
||||||
|
- VM templates: `/usr/local/emhttp/plugins/dynamix.vm.manager/templates/images/`
|
||||||
|
|
||||||
## Wallpaper
|
## Wallpaper
|
||||||
|
|
||||||
Place wallpaper at `/boot/config/plugins/custom.css/assets/wallpaper.jpg` — accessible at `/custom/wallpaper.jpg`.
|
Place wallpaper at `/boot/config/plugins/custom.css/assets/wallpaper.jpg` — accessible at `/custom/wallpaper.jpg`.
|
||||||
|
|||||||
+194
@@ -54,6 +54,10 @@
|
|||||||
var link = searchItem.querySelector('a');
|
var link = searchItem.querySelector('a');
|
||||||
if (!link) return;
|
if (!link) return;
|
||||||
|
|
||||||
|
// Remove any inline onclick handler (Unraid sets onclick="gui_search()")
|
||||||
|
link.removeAttribute('onclick');
|
||||||
|
link.onclick = null;
|
||||||
|
|
||||||
// Replace hover with click
|
// Replace hover with click
|
||||||
link.addEventListener('click', function(e) {
|
link.addEventListener('click', function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -141,3 +145,193 @@
|
|||||||
init();
|
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();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|||||||
+269
@@ -0,0 +1,269 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# update-icons.sh — Replace Unraid Docker/VM icons with selfh.st CDN icons
|
||||||
|
# Part of the unraid-glass theme project
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# --- Configuration ---
|
||||||
|
CDN_BASE="https://cdn.jsdelivr.net/gh/selfhst/icons@main/png"
|
||||||
|
ICON_RAM="/usr/local/emhttp/state/plugins/dynamix.docker.manager/images"
|
||||||
|
ICON_PERSIST="/var/lib/docker/unraid/images"
|
||||||
|
VM_ICON_DIR="/usr/local/emhttp/plugins/dynamix.vm.manager/templates/images"
|
||||||
|
CONFIG_DIR="/boot/config/plugins/custom.css/assets"
|
||||||
|
VM_CONFIG="${CONFIG_DIR}/vm-icons.conf"
|
||||||
|
TMP_DIR="/tmp/glass-icons"
|
||||||
|
LABEL_NAME="glass.icon"
|
||||||
|
|
||||||
|
FORCE=false
|
||||||
|
QUIET=false
|
||||||
|
DRY_RUN=false
|
||||||
|
DOCKER_ONLY=false
|
||||||
|
VM_ONLY=false
|
||||||
|
|
||||||
|
updated=0
|
||||||
|
skipped=0
|
||||||
|
failed=0
|
||||||
|
|
||||||
|
# --- Helpers ---
|
||||||
|
log() { $QUIET || echo "$@"; }
|
||||||
|
warn() { echo "WARN: $@" >&2; }
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage: update-icons.sh [OPTIONS]
|
||||||
|
|
||||||
|
Replace Unraid Docker/VM icons with selfh.st CDN icons.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--force Re-download even if icon file already matches
|
||||||
|
--quiet Suppress output (for cron)
|
||||||
|
--dry-run Show what would be done without doing it
|
||||||
|
--docker-only Skip VM icon processing
|
||||||
|
--vm-only Skip Docker icon processing
|
||||||
|
--help Show this help
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Argument parsing ---
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--force) FORCE=true ;;
|
||||||
|
--quiet) QUIET=true ;;
|
||||||
|
--dry-run) DRY_RUN=true ;;
|
||||||
|
--docker-only) DOCKER_ONLY=true ;;
|
||||||
|
--vm-only) VM_ONLY=true ;;
|
||||||
|
--help) usage ;;
|
||||||
|
*) warn "Unknown option: $1"; usage ;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- Auto-detection: try to match container name to selfh.st icon ---
|
||||||
|
cdn_exists() {
|
||||||
|
curl -fsSL --head --max-time 5 "${CDN_BASE}/${1}.png" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
auto_detect_icon() {
|
||||||
|
local container="$1"
|
||||||
|
local name
|
||||||
|
|
||||||
|
# Known mappings for containers that won't auto-match
|
||||||
|
case "$(echo "$container" | tr '[:upper:]' '[:lower:]')" in
|
||||||
|
autokuma) echo "uptime-kuma"; return 0 ;;
|
||||||
|
uptimekuma) echo "uptime-kuma"; return 0 ;;
|
||||||
|
uptime-kuma-api) echo "uptime-kuma"; return 0 ;;
|
||||||
|
homeassistant_inabox) echo "home-assistant"; return 0 ;;
|
||||||
|
homeassistant*) echo "home-assistant"; return 0 ;;
|
||||||
|
dockersocket) echo "docker"; return 0 ;;
|
||||||
|
pgadmin*) echo "pgadmin"; return 0 ;;
|
||||||
|
postgresql*) echo "postgresql"; return 0 ;;
|
||||||
|
woodpecker-agent) echo "woodpecker-ci"; return 0 ;;
|
||||||
|
woodpecker-server) echo "woodpecker-ci"; return 0 ;;
|
||||||
|
rustdesk-hbbr) echo "rustdesk"; return 0 ;;
|
||||||
|
rustdesk-hbbs) echo "rustdesk"; return 0 ;;
|
||||||
|
rustfs) echo "rustdesk"; return 0 ;;
|
||||||
|
netbox-redis-cache) echo "redis"; return 0 ;;
|
||||||
|
netbox-worker) echo "netbox"; return 0 ;;
|
||||||
|
netdisco-backend) echo "networking-toolbox"; return 0 ;;
|
||||||
|
netdisco-web) echo "networking-toolbox"; return 0 ;;
|
||||||
|
libation) echo "audible"; return 0 ;;
|
||||||
|
diode-*) echo "netbox"; return 0 ;;
|
||||||
|
authentik-worker) echo "authentik"; return 0 ;;
|
||||||
|
adguardhome-sync) echo "adguard-home"; return 0 ;;
|
||||||
|
adguardhome) echo "adguard-home"; return 0 ;;
|
||||||
|
actual-budget) echo "actual-budget"; return 0 ;;
|
||||||
|
speedtest-tracker) echo "speedtest-tracker"; return 0 ;;
|
||||||
|
timemachine) echo "apple"; return 0 ;;
|
||||||
|
open-webui) echo "open-webui"; return 0 ;;
|
||||||
|
urbackup) echo "networking-toolbox"; return 0 ;;
|
||||||
|
netalertx) echo "netalertx"; return 0 ;;
|
||||||
|
seekandwatch) echo "databasement"; return 0 ;;
|
||||||
|
unmarr) echo "homarr"; return 0 ;;
|
||||||
|
dockhand) echo "docker"; return 0 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Strategy 1: lowercase name directly
|
||||||
|
name=$(echo "$container" | tr '[:upper:]' '[:lower:]')
|
||||||
|
if cdn_exists "$name"; then echo "$name"; return 0; fi
|
||||||
|
|
||||||
|
# Strategy 2: underscores to hyphens
|
||||||
|
name=$(echo "$container" | tr '[:upper:]' '[:lower:]' | tr '_' '-')
|
||||||
|
if cdn_exists "$name"; then echo "$name"; return 0; fi
|
||||||
|
|
||||||
|
# Strategy 3: strip common suffixes
|
||||||
|
name=$(echo "$container" | tr '[:upper:]' '[:lower:]' | sed -E 's/-(worker|agent|server|web|backend|api|cache|migrate|bootstrap|sync|ingress|ingester|reconciler)$//')
|
||||||
|
if cdn_exists "$name"; then echo "$name"; return 0; fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Resolve label value to a full URL
|
||||||
|
resolve_url() {
|
||||||
|
local value="$1"
|
||||||
|
if [[ "$value" == http* ]]; then
|
||||||
|
echo "$value"
|
||||||
|
else
|
||||||
|
echo "${CDN_BASE}/${value}.png"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Download icon to temp, validate, and copy to target paths
|
||||||
|
download_and_replace() {
|
||||||
|
local name="$1"
|
||||||
|
local url="$2"
|
||||||
|
local target_ram="$3"
|
||||||
|
local target_persist="$4"
|
||||||
|
local tmp_file="${TMP_DIR}/${name}.png"
|
||||||
|
|
||||||
|
if $DRY_RUN; then
|
||||||
|
log " [DRY-RUN] Would download: $url"
|
||||||
|
log " [DRY-RUN] Would replace: $target_ram"
|
||||||
|
log " [DRY-RUN] Would replace: $target_persist"
|
||||||
|
((updated++)) || true
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Download
|
||||||
|
if ! curl -fsSL --max-time 15 -o "$tmp_file" "$url" 2>/dev/null; then
|
||||||
|
warn "Download failed for $name: $url"
|
||||||
|
((failed++)) || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate non-empty
|
||||||
|
if [ ! -s "$tmp_file" ]; then
|
||||||
|
warn "Empty download for $name: $url"
|
||||||
|
rm -f "$tmp_file"
|
||||||
|
((failed++)) || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Skip if identical (unless --force)
|
||||||
|
if ! $FORCE && [ -f "$target_ram" ] && cmp -s "$tmp_file" "$target_ram"; then
|
||||||
|
log " [SKIP] $name — icon unchanged"
|
||||||
|
rm -f "$tmp_file"
|
||||||
|
((skipped++)) || true
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Replace in both paths
|
||||||
|
cp "$tmp_file" "$target_ram" 2>/dev/null || true
|
||||||
|
if [ -n "$target_persist" ] && [ -d "$(dirname "$target_persist")" ]; then
|
||||||
|
cp "$tmp_file" "$target_persist" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
rm -f "$tmp_file"
|
||||||
|
|
||||||
|
log " [OK] $name — icon updated"
|
||||||
|
((updated++)) || true
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Main ---
|
||||||
|
mkdir -p "$TMP_DIR"
|
||||||
|
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||||
|
|
||||||
|
# === Docker containers ===
|
||||||
|
if ! $VM_ONLY; then
|
||||||
|
log "=== Docker Containers ==="
|
||||||
|
|
||||||
|
if ! docker info >/dev/null 2>&1; then
|
||||||
|
warn "Docker is not running, skipping containers"
|
||||||
|
else
|
||||||
|
while IFS= read -r container; do
|
||||||
|
[ -z "$container" ] && continue
|
||||||
|
log "Processing: $container"
|
||||||
|
|
||||||
|
# Read glass.icon label
|
||||||
|
label=$(docker inspect --format "{{index .Config.Labels \"${LABEL_NAME}\"}}" "$container" 2>/dev/null || echo "")
|
||||||
|
label="${label#<no value>}" # docker inspect returns "<no value>" if missing
|
||||||
|
|
||||||
|
if [ -n "$label" ]; then
|
||||||
|
icon_url=$(resolve_url "$label")
|
||||||
|
log " Label: $label → $icon_url"
|
||||||
|
else
|
||||||
|
# Auto-detect
|
||||||
|
if icon_name=$(auto_detect_icon "$container"); then
|
||||||
|
icon_url=$(resolve_url "$icon_name")
|
||||||
|
log " Auto-detect: $icon_name → $icon_url"
|
||||||
|
else
|
||||||
|
log " [SKIP] No label, no auto-match"
|
||||||
|
((skipped++)) || true
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
download_and_replace \
|
||||||
|
"$container" \
|
||||||
|
"$icon_url" \
|
||||||
|
"${ICON_RAM}/${container}-icon.png" \
|
||||||
|
"${ICON_PERSIST}/${container}-icon.png" || true
|
||||||
|
|
||||||
|
done < <(docker ps -a --format '{{.Names}}')
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# === Virtual Machines ===
|
||||||
|
if ! $DOCKER_ONLY; then
|
||||||
|
log ""
|
||||||
|
log "=== Virtual Machines ==="
|
||||||
|
|
||||||
|
if [ ! -f "$VM_CONFIG" ]; then
|
||||||
|
log "No VM config found at $VM_CONFIG, skipping VMs"
|
||||||
|
elif ! command -v virsh >/dev/null 2>&1; then
|
||||||
|
warn "virsh not found, skipping VMs"
|
||||||
|
else
|
||||||
|
while IFS='=' read -r vm_name icon_value; do
|
||||||
|
# Skip comments and empty lines
|
||||||
|
[[ "$vm_name" =~ ^[[:space:]]*# ]] && continue
|
||||||
|
[ -z "$vm_name" ] && continue
|
||||||
|
vm_name=$(echo "$vm_name" | xargs) # trim whitespace
|
||||||
|
icon_value=$(echo "$icon_value" | xargs)
|
||||||
|
[ -z "$icon_value" ] && continue
|
||||||
|
|
||||||
|
log "Processing VM: $vm_name"
|
||||||
|
|
||||||
|
# Get current icon filename from VM XML
|
||||||
|
icon_file=$(virsh dumpxml "$vm_name" 2>/dev/null | grep -oP 'icon="\K[^"]+' || echo "")
|
||||||
|
if [ -z "$icon_file" ]; then
|
||||||
|
warn "Could not read icon for VM '$vm_name'"
|
||||||
|
((failed++)) || true
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
icon_url=$(resolve_url "$icon_value")
|
||||||
|
log " Config: $icon_value → $icon_url"
|
||||||
|
|
||||||
|
download_and_replace \
|
||||||
|
"vm-${vm_name// /-}" \
|
||||||
|
"$icon_url" \
|
||||||
|
"${VM_ICON_DIR}/${icon_file}" \
|
||||||
|
"" || true
|
||||||
|
|
||||||
|
done < "$VM_CONFIG"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# === Summary ===
|
||||||
|
log ""
|
||||||
|
log "=== Done ==="
|
||||||
|
log "Updated: $updated | Skipped: $skipped | Failed: $failed"
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# vm-icons.conf — VM icon overrides for unraid-glass theme
|
||||||
|
# Format: VM_NAME=icon_name (or full URL)
|
||||||
|
# VM_NAME must match exactly as shown in `virsh list --all`
|
||||||
|
# Lines starting with # are comments, empty lines are ignored.
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# Home Assistant=home-assistant
|
||||||
|
# Windows 11=windows-11
|
||||||
|
# Ubuntu Server=ubuntu
|
||||||
|
|
||||||
|
Home Assistant=home-assistant
|
||||||
Reference in New Issue
Block a user