-
Notifications
You must be signed in to change notification settings - Fork 0
Theme Plugin Guide
Theme plugins provide complete custom layouts - not just color skins. You can replace the entire navbar, home page, library views, and couch mode.
A theme plugin provides three hooks:
| Hook | Returns | Purpose |
|---|---|---|
frontend_get_theme() |
dict | Theme definition (skins, settings, layout ID) |
frontend_get_css() |
str | CSS overrides for GD components |
frontend_get_js() |
str | Dynamic JS effects (particles, glass blur, etc.) |
Theme plugins can include .vue files that are compiled on container startup:
- Put
.vuefiles in your plugin root - Name them with your theme prefix:
MyThemeLayout.vue,MyThemeCouch.vue - On container start, Vite compiles them into
layout.js+layout.css - Frontend auto-loads and registers them
| Layout ID | Layout Component | Couch Component | Children |
|---|---|---|---|
my-theme |
MyThemeLayout.vue |
MyThemeCouch.vue |
MyThemeHome.vue, MyThemeLibrary.vue, etc. |
neon-horizon |
NeonHorizonLayout.vue |
NeonHorizonCouch.vue |
NeonHorizonHome.vue, etc. |
Children are imported inside Layout/Couch - the compiler bundles everything together.
Important: After installing/updating a theme plugin, restart the container (Plugin Store has a "Restart Now" button).
@hookimpl
def frontend_get_theme(self):
return {
"id": "my-theme",
"name": "My Theme",
"description": "A cool theme",
"layout": "my-theme", # Must match Vue component naming
"skins": [
{"id": "blue", "name": "Ocean", "preview": "#2563eb"},
{"id": "sunset", "name": "Sunset", "preview": "linear-gradient(135deg, #f97316, #ec4899)", "dual": True},
],
"defaultSkin": "blue",
"cssFile": "my-theme", # Base name for CSS
"font": "https://fonts.googleapis.com/css2?family=MyFont&display=swap",
"previewHtml": "<div>...</div>", # Optional: preview in theme switcher
"settings": [ ... ], # See Settings section below
}Skins define color palettes. Each skin sets CSS variables via data-skin attribute:
-
Solid skins:
{"id": "blue", "name": "Ocean", "preview": "#2563eb"} -
Gradient skins:
{"id": "sunset", "name": "Sunset", "preview": "linear-gradient(...)", "dual": True}
Your CSS uses these selectors:
:root[data-skin="blue"] {
--pl: #2563eb; /* Primary color */
--pl-light: #60a5fa; /* Light variant */
--pglow: rgba(37, 99, 235, .4);
--bg: #0a0a1a; /* Background */
}Settings appear in Settings > Appearance > Theme Settings:
"settings": [
{
"key": "particleCount",
"label": "nh.setting_particles", # i18n key
"hint": "nh.setting_particles_hint", # i18n key
"type": "select",
"default": "6",
"options": ["0", "3", "6", "12"],
"optionLabels": ["nh.opt_none", "nh.opt_few", "nh.opt_normal", "nh.opt_many"],
"cssVar": "--nh-particle-count",
},
{
"key": "glassBlur",
"label": "Glass Blur", # Plain string also works
"type": "range",
"default": 20,
"min": 0, "max": 60, "step": 1,
"unit": "px", # Appended to value: 20 -> "20px"
"cssVar": "--glass-blur-px",
},
{
"key": "scanlines",
"label": "Scanlines",
"type": "toggle",
"default": False,
"cssVar": "--my-scanlines", # "1" when on, "0" when off
},
]Important: Theme settings are NOT the same as config_schema in plugin.json. Theme settings go in frontend_get_theme().settings[] and render in Appearance. config_schema renders in Plugins and is for plugin configuration (API keys, toggles, etc.). Theme plugins typically have empty config_schema: {}.
Plugin .vue files compile outside the main app. You cannot use @/ imports. Use window.__GD__:
import { ref, computed, watch, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
const _gd = (window as any).__GD__
const client = _gd.api // Axios with Bearer token
const auth = _gd.stores.auth() // { user } - token hidden
const themeStore = _gd.stores.theme() // Full theme store
const t = _gd.i18n?.t || ((k: string) => k) // i18n translation
// Couch Mode
const { useCouchNav, couchNavPaused } = _gd.composables
const getEjsCore = _gd.getEjsCore // Platform -> EmulatorJS core
// Notifications
_gd.notifications.add({ id: 'my-alert', count: 1, label: 'Something happened' })Global components available in templates:
-
<DownloadManager />- Download queue (admin only) -
<RandomGamePicker />- Random game dice -
<AmbientBackground />- Animated orb background
Scope all your CSS with data-theme to avoid conflicts:
[data-theme="my-theme"] .some-class {
/* Only applies when your theme is active */
}
[data-theme="my-theme"][data-skin="sunset"] .accent {
/* Specific skin + theme combination */
}GD defines standard CSS vars that all themes should use:
-
--pl- Primary color -
--pl-light- Light primary -
--pglow/--pglow2- Glow effects -
--bg/--bg2/--bg3- Background layers -
--text/--muted- Text colors -
--glass-bg/--glass-border- Glass panel styles
JS returned by frontend_get_js() runs as raw JavaScript (not a module). Use IIFE pattern:
(function() {
const THEME_ID = 'my-theme';
const root = document.documentElement;
function isActive() {
return root.getAttribute('data-theme') === THEME_ID;
}
// React to theme setting changes
root.addEventListener('gd-theme-updated', () => {
if (!isActive()) return;
// Read CSS vars and apply effects
const blur = parseFloat(getComputedStyle(root).getPropertyValue('--glass-blur-px')) || 20;
});
// Watch for DOM changes (new elements need styling)
new MutationObserver(() => {
if (!isActive()) return;
// Apply dynamic styles
}).observe(document.body, { childList: true, subtree: true });
})();Static files in assets/ are served at /api/plugins/{id}/assets/{path}:
const PLUGIN_ID = 'my-theme'
function pluginAsset(path: string): string {
return `/api/plugins/${PLUGIN_ID}/assets/${path}`
}
// Images
pluginAsset('icons/snes.png')
// JSON data files
const { data } = await client.get(`/api/plugins/${PLUGIN_ID}/assets/metadata.json`)Couch Mode components use gamepad/keyboard navigation via useCouchNav:
const { useCouchNav, couchNavPaused } = _gd.composables
useCouchNav({
left: () => { /* D-pad left */ },
right: () => { /* D-pad right */ },
up: () => { /* D-pad up */ },
down: () => { /* D-pad down */ },
confirm: () => { /* A button / Enter */ },
back: () => { /* B button / Escape */ },
menu: () => { /* Start button / M key */ },
})
// Pause nav during overlays
couchNavPaused.value = true
// Resume
couchNavPaused.value = falseLaunch games with EmulatorJS:
const core = _gd.getEjsCore(platform.fs_slug) // Returns core name or null
if (core) {
window.open(`/player.html?rom_id=${rom.id}&ejs_core=${core}&platform=${platform.fs_slug}`)
}