librelad 875a60f90f LibrePortal v0.1.0 — initial release
A free, open, self-hosted app platform (GNU AGPLv3): one-click app deploys,
Traefik reverse proxy with automatic SSL, rootless Docker support, gluetun
VPN routing, and a web dashboard to manage it all.

Free & open forever to self-host; optional paid hosted services fund it.
See PROMISE.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-21 20:37:54 +01:00

83 lines
2.8 KiB
JavaScript

// Theme registry
// ---------------
// Discovers themes from frontend/themes/<name>/ via the backend
// /api/themes/list endpoint, <link>s each one's CSS into <head>, and
// exposes the list at window.ThemeRegistry for the topbar dropdown.
//
// Every theme — built-in or custom — lives in frontend/themes/<name>/.
// The inline bootstrap in index.html already <link>s the saved theme
// synchronously before first paint; this script <link>s the rest so the
// dropdown can switch between them without another network round-trip.
//
// If the API fails completely, the dropdown falls back to a single
// option (the currently-loaded theme from data-theme). The user can't
// switch, but the page still renders correctly.
(function () {
var currentTheme = document.documentElement.getAttribute('data-theme') || 'nebula';
var state = {
themes: [{
name: currentTheme,
displayName: currentTheme,
css: '/themes/' + currentTheme + '/theme.css',
builtin: false,
}],
loaded: false,
listeners: [],
};
function notify() {
state.listeners.forEach(function (cb) {
try { cb(state.themes); } catch (_) { /* swallow */ }
});
}
function linkThemeCss(theme) {
// The inline bootstrap already linked the current theme; this dedupes.
if (document.head.querySelector('link[data-theme-css="' + theme.name + '"]')) return;
var link = document.createElement('link');
link.rel = 'stylesheet';
link.href = theme.css;
link.setAttribute('data-theme-css', theme.name);
document.head.appendChild(link);
}
function fetchThemes() {
return fetch('/api/themes/list', { credentials: 'same-origin' })
.then(function (r) { return r.ok ? r.json() : []; })
.then(function (list) {
if (Array.isArray(list) && list.length) state.themes = list;
state.themes.forEach(linkThemeCss);
state.loaded = true;
notify();
})
.catch(function () {
// Network/API failure — keep the built-in fallback list.
state.loaded = true;
notify();
});
}
window.ThemeRegistry = {
/* Synchronous accessor — returns whatever list we have, even if the
fetch is still in flight. Always includes at least the built-ins. */
list: function () { return state.themes.slice(); },
/* True once /api/themes/list has resolved (success or failure). */
isLoaded: function () { return state.loaded; },
/* Subscribe to list updates. Called immediately with the current
list and again after the fetch resolves. */
onChange: function (cb) {
state.listeners.push(cb);
try { cb(state.themes); } catch (_) { /* swallow */ }
},
/* Manually trigger a refresh — used after the user drops a new
theme folder and hits "reload themes" (no UI for that yet). */
refresh: fetchThemes,
};
fetchThemes();
})();