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>
72 lines
2.4 KiB
JavaScript
72 lines
2.4 KiB
JavaScript
const express = require('express');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const router = express.Router();
|
|
|
|
const THEMES_DIR = path.join(__dirname, '..', '..', 'frontend', 'themes');
|
|
|
|
/* Surface order — themes whose folder name appears here are listed first,
|
|
in this order. Anything else is appended alphabetically. Lets us keep
|
|
the built-ins (nebula / dark-blue / light) at the top of the dropdown
|
|
without hardcoding their existence in the API. */
|
|
const PREFERRED_ORDER = ['nebula', 'dark-blue', 'light'];
|
|
|
|
/* =========================
|
|
GET /api/themes/list
|
|
|
|
Walks frontend/themes/<name>/ and returns one entry per directory that
|
|
contains a theme.css. Optional meta.json supplies a friendlier display
|
|
name. No hardcoded list — built-ins live in folders just like any
|
|
custom theme.
|
|
|
|
Public — the list of theme names isn't sensitive and the frontend
|
|
needs it before login to render the right palette on the login
|
|
overlay too.
|
|
========================= */
|
|
router.get('/list', (req, res) => {
|
|
const themes = [];
|
|
try {
|
|
if (fs.existsSync(THEMES_DIR)) {
|
|
for (const entry of fs.readdirSync(THEMES_DIR, { withFileTypes: true })) {
|
|
if (!entry.isDirectory()) continue;
|
|
const name = entry.name;
|
|
const cssPath = path.join(THEMES_DIR, name, 'theme.css');
|
|
if (!fs.existsSync(cssPath)) continue;
|
|
|
|
let displayName = name;
|
|
let builtin = false;
|
|
const metaPath = path.join(THEMES_DIR, name, 'meta.json');
|
|
if (fs.existsSync(metaPath)) {
|
|
try {
|
|
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
|
|
if (meta && typeof meta.displayName === 'string' && meta.displayName.trim()) {
|
|
displayName = meta.displayName.trim();
|
|
}
|
|
if (meta && meta.builtin === true) builtin = true;
|
|
} catch (_) {
|
|
/* malformed meta.json — fall back to folder name */
|
|
}
|
|
}
|
|
|
|
themes.push({ name, displayName, css: `/themes/${name}/theme.css`, builtin });
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('Error scanning themes directory:', err);
|
|
}
|
|
|
|
themes.sort((a, b) => {
|
|
const ai = PREFERRED_ORDER.indexOf(a.name);
|
|
const bi = PREFERRED_ORDER.indexOf(b.name);
|
|
if (ai !== -1 && bi !== -1) return ai - bi;
|
|
if (ai !== -1) return -1;
|
|
if (bi !== -1) return 1;
|
|
return a.displayName.localeCompare(b.displayName);
|
|
});
|
|
|
|
res.json(themes);
|
|
});
|
|
|
|
module.exports = router;
|