Skip to content

Theme-aware plugin icons via an optional -dark or -light variant of the icon file - #2722

Open
Junker der Provinz (junkerderprovinz) wants to merge 3 commits into
unraid:masterfrom
junkerderprovinz:feat/theme-aware-plugin-icons
Open

Theme-aware plugin icons via an optional -dark or -light variant of the icon file#2722
Junker der Provinz (junkerderprovinz) wants to merge 3 commits into
unraid:masterfrom
junkerderprovinz:feat/theme-aware-plugin-icons

Conversation

@junkerderprovinz

@junkerderprovinz Junker der Provinz (junkerderprovinz) commented Aug 11, 2026

Copy link
Copy Markdown

A plugin can now ship a per-theme variant of its .png icon. Next to icon.png it may place icon-dark.png and/or icon-light.png, and the webgui serves the variant matching the active theme.

Plugins that ship a single icon are untouched: with no matching variant file present the icon renders exactly as it does today, and glyph icons (icon-* and FontAwesome) are not affected at all.

Closes #2704.

Why

Glyph icons already follow the theme, because they render as <b class="fa fa-... system"> and inherit the theme text color. Image icons do not. A .png renders as a static <img>, so a full-color logo can be unreadable on some themes: a dark logo disappears on the black theme, a light one washes out on the white theme.

The only theme-adaptive option for an image icon today is a single-color inline SVG using currentColor, which forces the logo to be monochrome and loses its real colors. In practice plugin authors work around this in the artwork instead, for example by drawing concentric dark and light rings so that one of them always contrasts with whatever background the icon lands on. That constrains the logo rather than fixing the problem.

How

One helper in Helpers.php:

function theme_icon($icon)

It takes the docroot-relative path of an already resolved icon and returns either the theme variant or the same path unchanged. It returns early for anything that is not a .png, so glyph icons never reach the file check.

Which themes count as dark is not a new decision. The helper asks the existing ThemeHelper::isDarkTheme(), so black and gray get -dark, white and azure get -light, and any theme added later is classified in one place. The suffix is resolved once per request and cached in a static, so a page with many icons costs at most one extra file_exists per icon.

The theme comes from $display['theme'] on a normal page load. ShowPlugins.php is fetched standalone over AJAX and has no $display, so the helper falls back to parse_plugin_cfg('dynamix',true) there. That keeps the helper self-contained instead of changing the variable scope of that endpoint.

The helper is applied at the four places that render a plugin-supplied .png icon:

  • the nav and sidebar utility buttons (.page Icon=, from <root>/icons/)
  • the Settings and Tools tiles (.page Icon=, from <root>/images/ or <root>/)
  • the page and tab title (from <root>/icons/)
  • the Plugins page list (.plg icon, from plugins/<name>/images/ or plugins/<name>/)

In each case the helper runs after the existing lookup has decided which file to use, so the existing resolution order and the existing fallbacks are unchanged.

Deliberately not covered: Docker container and VM icons, which come from user-editable templates and remote URLs rather than from files a plugin ships. Also SVG, because a .svg value in Icon= is not handled as an image today (it falls through to the FontAwesome branch), so supporting it would be a separate change.

This implements option A from the issue (naming convention, no manifest change), because it needs no new field in either the .page or the .plg format and works the same for both.

Testing

php -l is clean on all changed files under PHP 8.4.23 with short_open_tag=On.

theme_icon() was also exercised directly against the real ThemeHelper using a sandbox docroot:

  • black and gray pick icon-dark.png when it exists
  • white and azure pick icon-light.png when it exists
  • an icon with no variant, an icon whose file is missing, and the glyph values rocket and icon-app all come back unchanged
  • with $display unset, the config fallback resolves the theme correctly and raises nothing at E_ALL

Files

  • emhttp/plugins/dynamix/include/Helpers.php
  • emhttp/plugins/dynamix/include/PageBuilder.php
  • emhttp/plugins/dynamix/include/DefaultPageLayout/MainContent.php
  • emhttp/plugins/dynamix/include/DefaultPageLayout/Navigation/Main.php
  • emhttp/plugins/dynamix.plugin.manager/include/ShowPlugins.php

Summary by CodeRabbit

  • New Features
    • Added theme-aware icons across plugins, navigation, pages, tabs, and virtual machine displays.
    • Icons automatically use light or dark variants based on the active theme when available.
    • Standard icons remain available as a fallback when no themed version exists.
    • Virtual machine icon selectors now hide theme-specific variant files to simplify selection.

…variant

A plugin may place 'icon-dark.png' and/or 'icon-light.png' next to its
'icon.png'; the variant matching the active theme is served instead. Which
themes count as dark comes from the existing ThemeHelper::isDarkTheme(), so
black and gray get -dark and white and azure get -light.

Applied at the four places that render a plugin supplied .png icon: the nav
and sidebar utility buttons, the Settings and Tools tiles, the page and tab
title, and the Plugins page list. In each case the helper runs after the
existing lookup has picked a file, so the existing resolution order and
fallbacks are unchanged.

Backward compatible: with no matching variant present the icon renders
exactly as before, and glyph icons return early and never reach the check.
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

PNG plugin and VM icons now support light and dark theme variants. The new theme_icon() helper selects an existing themed PNG and preserves the original path when no variant exists. Plugin, navigation, page, tab, dashboard, and VM renderers use the helper.

Changes

Theme-aware plugin icons

Layer / File(s) Summary
Theme-specific icon resolution
emhttp/plugins/dynamix/include/Helpers.php
The new theme_icon() helper resolves the active theme, selects a matching light or dark PNG variant, and falls back to the original icon path.
Plugin and page icon rendering
emhttp/plugins/dynamix/include/DefaultPageLayout/..., emhttp/plugins/dynamix/include/PageBuilder.php, emhttp/plugins/dynamix.plugin.manager/include/ShowPlugins.php
Plugin, navigation, main-content, and tab-title renderers apply theme_icon() before creating image markup.
VM icon rendering and selection
emhttp/plugins/dynamix/include/DashboardApps.php, emhttp/plugins/dynamix.vm.manager/include/VMMachines.php, emhttp/plugins/dynamix.vm.manager/include/VMedit.php
Dashboard and VM views apply theme_icon(). The VM icon chooser excludes dark and light variant files.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant IconRenderer
  participant theme_icon
  participant ThemeHelper
  IconRenderer->>theme_icon: pass PNG icon path
  theme_icon->>ThemeHelper: resolve active theme
  ThemeHelper-->>theme_icon: return theme
  theme_icon-->>IconRenderer: return themed or original path
Loading

Poem

I’m a rabbit with icons to spare,
Light in the sun, dark in night air.
theme_icon() chooses the right view,
And keeps old paths when variants are few.
Hop, hop—the pages now match their hue!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: theme-aware plugin icons with optional dark and light variants.
Linked Issues check ✅ Passed The changes implement theme-aware PNG variants, preserve fallback behavior, and apply the helper across the requested plugin and VM icon locations [#2704].
Out of Scope Changes check ✅ Passed The VM icon updates extend the same theme-aware behavior to related plugin UI locations and remain aligned with the pull request objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Same theme_icon() helper from the plugin-icon PR, wired into the three
places a VM's icon renders: the VMs tab list, the Dashboard widget (and
its VM-usage tile, which shares the same resolved icon), and the
Add/Edit VM form's preview + icon-chooser grid. The chooser grid also
skips -dark/-light files as their own entries, since theme_icon()
already serves the matching variant for its canonical icon.

VM icons ship as first-party files in this repo (unlike Docker container
icons, which come from third-party template URLs with no dark/light
concept), so a future -dark/-light pair for any of them works without
further changes -- no such pairs exist yet, so this is a no-op today.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@emhttp/plugins/dynamix.vm.manager/include/VMMachines.php`:
- Line 54: Update the icon handling around $lv->domain_get_icon_url($res) and
theme_icon() so absolute filesystem paths are normalized to the theme resolver’s
expected path before variant lookup. Preserve existing URL and relative-path
behavior while ensuring absolute VM icon paths still resolve their themed
variants.

In `@emhttp/plugins/dynamix/include/DashboardApps.php`:
- Line 179: Update theme_icon() call sites to normalize absolute /boot/config
paths returned or used by domain_get_icon_url() before resolution. Apply this in
emhttp/plugins/dynamix/include/DashboardApps.php lines 179-179,
emhttp/plugins/dynamix.vm.manager/include/VMedit.php lines 129-129, and
emhttp/plugins/dynamix.vm.manager/include/VMedit.php lines 139-143; preserve
relative-path handling and ensure themed variants resolve for user-installed
icons.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c2ed5261-83cc-4edb-9ee3-3bd6c5888b9f

📥 Commits

Reviewing files that changed from the base of the PR and between 04f7181 and 47a7395.

📒 Files selected for processing (3)
  • emhttp/plugins/dynamix.vm.manager/include/VMMachines.php
  • emhttp/plugins/dynamix.vm.manager/include/VMedit.php
  • emhttp/plugins/dynamix/include/DashboardApps.php

$autostart = $lv->domain_get_autostart($res) ? 'checked' : '';
$state = $lv->domain_state_translate($dom['state']);
$icon = $lv->domain_get_icon_url($res);
$icon = theme_icon($lv->domain_get_icon_url($res));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve theme resolution for absolute VM icon paths.

domain_get_icon_url() returns the raw $strIcon when is_file($strIcon) succeeds. theme_icon() then checks the variant under $docroot/$variant. For an absolute path such as /boot/config/.../icon.png, this checks the wrong filesystem location, so the themed variant is skipped. Normalize filesystem paths before calling theme_icon(), or update theme_icon() to resolve them correctly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@emhttp/plugins/dynamix.vm.manager/include/VMMachines.php` at line 54, Update
the icon handling around $lv->domain_get_icon_url($res) and theme_icon() so
absolute filesystem paths are normalized to the theme resolver’s expected path
before variant lookup. Preserve existing URL and relative-path behavior while
ensuring absolute VM icon paths still resolve their themed variants.

}
$menu = sprintf("onclick=\"addVMContext('%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s')\"", addslashes($vm), addslashes($uuid), addslashes($template), $state, addslashes($vmrcurl), strtoupper($vmrcprotocol), addslashes($log),addslashes($fstype), $vmrcconsole,false,addslashes(str_replace('"',"'",$WebUI)),$pcierror,$srioverror);
$icon = $lv->domain_get_icon_url($res);
$icon = theme_icon($lv->domain_get_icon_url($res));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Fix the shared path contract for /boot/config icons.

theme_icon() cannot resolve variants when callers pass absolute /boot/config/... paths because it prepends $docroot before file_exists(). User-installed VM icons therefore keep the base image instead of switching themes.

  • emhttp/plugins/dynamix/include/DashboardApps.php#L179: handle absolute paths returned by domain_get_icon_url().
  • emhttp/plugins/dynamix.vm.manager/include/VMedit.php#L129: handle absolute paths used by the edit preview.
  • emhttp/plugins/dynamix.vm.manager/include/VMedit.php#L139-L143: handle absolute paths used by the icon chooser.
📍 Affects 2 files
  • emhttp/plugins/dynamix/include/DashboardApps.php#L179-L179 (this comment)
  • emhttp/plugins/dynamix.vm.manager/include/VMedit.php#L129-L129
  • emhttp/plugins/dynamix.vm.manager/include/VMedit.php#L139-L143
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@emhttp/plugins/dynamix/include/DashboardApps.php` at line 179, Update
theme_icon() call sites to normalize absolute /boot/config paths returned or
used by domain_get_icon_url() before resolution. Apply this in
emhttp/plugins/dynamix/include/DashboardApps.php lines 179-179,
emhttp/plugins/dynamix.vm.manager/include/VMedit.php lines 129-129, and
emhttp/plugins/dynamix.vm.manager/include/VMedit.php lines 139-143; preserve
relative-path handling and ensure themed variants resolve for user-installed
icons.

@junkerderprovinz

Copy link
Copy Markdown
Author

CodeRabbit's /boot/config/... finding is a false positive, so I have left the code as is.

emhttp/boot is a committed symlink to /boot, which makes $docroot/boot/config/... the correct filesystem path for a /boot/config/... URL. That is already the convention in this same code: libvirt.php line 1461 tests is_file("$docroot/boot/config/plugins/dynamix.vm.manager/templates/images/...") before returning the /boot/config/... URL, and the icon chooser in VMedit.php globs $docroot/boot/config/... while emitting /boot/config/... as the URL base. theme_icon() follows the same convention, so user-installed VM icons on the flash drive do resolve their -dark and -light variants.

The only branch of domain_get_icon_url() that can return a path outside the docroot is the raw is_file($strIcon) case. Such a path is not servable as an <img src> today either, and theme_icon() returns it unchanged, so nothing regresses there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: theme-aware plugin icons (light/dark logo that switches with the theme, like glyph icons)

1 participant