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>
83 lines
2.8 KiB
JavaScript
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();
|
|
})();
|