Custom themes (#584)

* Add support for custom themes (fixes #560)

* load theme list in site-form.tsx
This commit is contained in:
Nutomic 2022-03-02 15:35:59 +00:00 committed by GitHub
parent 20207bd599
commit 2ffe7e4c6f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 83 additions and 30 deletions

1
extra_themes/test.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -1,4 +1,5 @@
import express from "express"; import express from "express";
import fs from "fs";
import { IncomingHttpHeaders } from "http"; import { IncomingHttpHeaders } from "http";
import { Helmet } from "inferno-helmet"; import { Helmet } from "inferno-helmet";
import { matchPath, StaticRouter } from "inferno-router"; import { matchPath, StaticRouter } from "inferno-router";
@ -23,6 +24,8 @@ const server = express();
const [hostname, port] = process.env["LEMMY_UI_HOST"] const [hostname, port] = process.env["LEMMY_UI_HOST"]
? process.env["LEMMY_UI_HOST"].split(":") ? process.env["LEMMY_UI_HOST"].split(":")
: ["0.0.0.0", "1234"]; : ["0.0.0.0", "1234"];
const extraThemesFolder =
process.env["LEMMY_UI_EXTRA_THEMES_FOLDER"] || "./extra_themes";
server.use(express.json()); server.use(express.json());
server.use(express.urlencoded({ extended: false })); server.use(express.urlencoded({ extended: false }));
@ -46,6 +49,54 @@ server.get("/robots.txt", async (_req, res) => {
res.send(robotstxt); res.send(robotstxt);
}); });
server.get("/css/themes/:name", async (req, res) => {
res.contentType("text/css");
const theme = req.params.name;
if (!theme.endsWith(".min.css")) {
res.send("Theme must be a css file");
}
const customTheme = path.resolve(`./${extraThemesFolder}/${theme}`);
if (fs.existsSync(customTheme)) {
res.sendFile(customTheme);
} else {
const internalTheme = path.resolve(`./dist/assets/css/themes/${theme}`);
res.sendFile(internalTheme);
}
});
function buildThemeList(): string[] {
let themes = [
"litera",
"materia",
"minty",
"solar",
"united",
"cyborg",
"darkly",
"journal",
"sketchy",
"vaporwave",
"vaporwave-dark",
"i386",
"litely",
"nord",
];
if (fs.existsSync(extraThemesFolder)) {
let dirThemes = fs.readdirSync(extraThemesFolder);
let minCssThemes = dirThemes
.filter(d => d.endsWith(".min.css"))
.map(d => d.replace(".min.css", ""));
themes.push(...minCssThemes);
}
return themes;
}
server.get("/css/themelist", async (_req, res) => {
res.type("json");
res.send(JSON.stringify(buildThemeList()));
});
// server.use(cookieParser()); // server.use(cookieParser());
server.get("/*", async (req, res) => { server.get("/*", async (req, res) => {
try { try {

View file

@ -18,7 +18,7 @@ export class Theme extends Component<Props> {
<link <link
rel="stylesheet" rel="stylesheet"
type="text/css" type="text/css"
href={`/static/assets/css/themes/${user.local_user_view.local_user.theme}.min.css`} href={`css/themes/${user.local_user_view.local_user.theme}.min.css`}
/> />
</Helmet> </Helmet>
); );
@ -28,7 +28,7 @@ export class Theme extends Component<Props> {
<link <link
rel="stylesheet" rel="stylesheet"
type="text/css" type="text/css"
href={`/static/assets/css/themes/${this.props.defaultTheme}.min.css`} href={`/css/themes/${this.props.defaultTheme}.min.css`}
/> />
</Helmet> </Helmet>
); );
@ -39,7 +39,7 @@ export class Theme extends Component<Props> {
<link <link
rel="stylesheet" rel="stylesheet"
type="text/css" type="text/css"
href="/static/assets/css/themes/litely.min.css" href="/css/themes/litely.min.css"
id="default-light" id="default-light"
media="(prefers-color-scheme: light)" media="(prefers-color-scheme: light)"
/> />
@ -47,7 +47,7 @@ export class Theme extends Component<Props> {
<link <link
rel="stylesheet" rel="stylesheet"
type="text/css" type="text/css"
href="/static/assets/css/themes/darkly.min.css" href="/css/themes/darkly.min.css"
id="default-dark" id="default-dark"
media="(prefers-color-scheme: no-preference), (prefers-color-scheme: dark)" media="(prefers-color-scheme: no-preference), (prefers-color-scheme: dark)"
/> />

View file

@ -6,7 +6,7 @@ import { WebSocketService } from "../../services";
import { import {
authField, authField,
capitalizeFirstLetter, capitalizeFirstLetter,
themes, fetchThemeList,
wsClient, wsClient,
} from "../../utils"; } from "../../utils";
import { Spinner } from "../common/icon"; import { Spinner } from "../common/icon";
@ -21,6 +21,7 @@ interface SiteFormProps {
interface SiteFormState { interface SiteFormState {
siteForm: EditSite; siteForm: EditSite;
loading: boolean; loading: boolean;
themeList: string[];
} }
export class SiteForm extends Component<SiteFormProps, SiteFormState> { export class SiteForm extends Component<SiteFormProps, SiteFormState> {
@ -40,6 +41,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
auth: authField(), auth: authField(),
}, },
loading: false, loading: false,
themeList: [],
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
@ -78,6 +80,11 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
} }
} }
async componentDidMount() {
this.state.themeList = await fetchThemeList();
this.setState(this.state);
}
// Necessary to stop the loading // Necessary to stop the loading
componentWillReceiveProps() { componentWillReceiveProps() {
this.state.loading = false; this.state.loading = false;
@ -336,7 +343,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
class="custom-select w-auto" class="custom-select w-auto"
> >
<option value="browser">{i18n.t("browser_default")}</option> <option value="browser">{i18n.t("browser_default")}</option>
{themes.map(theme => ( {this.state.themeList.map(theme => (
<option value={theme}>{theme}</option> <option value={theme}>{theme}</option>
))} ))}
</select> </select>

View file

@ -29,6 +29,7 @@ import {
debounce, debounce,
elementUrl, elementUrl,
fetchCommunities, fetchCommunities,
fetchThemeList,
fetchUsers, fetchUsers,
getLanguages, getLanguages,
isBrowser, isBrowser,
@ -39,7 +40,6 @@ import {
setTheme, setTheme,
setupTippy, setupTippy,
showLocal, showLocal,
themes,
toast, toast,
updateCommunityBlock, updateCommunityBlock,
updatePersonBlock, updatePersonBlock,
@ -78,6 +78,7 @@ interface SettingsState {
blockCommunity?: CommunityView; blockCommunity?: CommunityView;
currentTab: string; currentTab: string;
siteRes: GetSiteResponse; siteRes: GetSiteResponse;
themeList: string[];
} }
export class Settings extends Component<any, SettingsState> { export class Settings extends Component<any, SettingsState> {
@ -109,6 +110,7 @@ export class Settings extends Component<any, SettingsState> {
blockCommunityId: 0, blockCommunityId: 0,
currentTab: "settings", currentTab: "settings",
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
themeList: [],
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
@ -131,8 +133,10 @@ export class Settings extends Component<any, SettingsState> {
this.setUserInfo(); this.setUserInfo();
} }
componentDidMount() { async componentDidMount() {
setupTippy(); setupTippy();
this.state.themeList = await fetchThemeList();
this.setState(this.state);
} }
componentWillUnmount() { componentWillUnmount() {
@ -545,7 +549,7 @@ export class Settings extends Component<any, SettingsState> {
{i18n.t("theme")} {i18n.t("theme")}
</option> </option>
<option value="browser">{i18n.t("browser_default")}</option> <option value="browser">{i18n.t("browser_default")}</option>
{themes.map(theme => ( {this.state.themeList.map(theme => (
<option value={theme}>{theme}</option> <option value={theme}>{theme}</option>
))} ))}
</select> </select>

View file

@ -77,23 +77,6 @@ export const mentionDropdownFetchLimit = 10;
export const relTags = "noopener nofollow"; export const relTags = "noopener nofollow";
export const themes = [
"litera",
"materia",
"minty",
"solar",
"united",
"cyborg",
"darkly",
"journal",
"sketchy",
"vaporwave",
"vaporwave-dark",
"i386",
"litely",
"nord",
];
const DEFAULT_ALPHABET = const DEFAULT_ALPHABET =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
@ -365,7 +348,11 @@ function getBrowserLanguages(): string[] {
return allowedLangs; return allowedLangs;
} }
export function setTheme(theme: string, forceReload = false) { export async function fetchThemeList(): Promise<string[]> {
return fetch("/css/themelist").then(res => res.json());
}
export async function setTheme(theme: string, forceReload = false) {
if (!isBrowser()) { if (!isBrowser()) {
return; return;
} }
@ -377,9 +364,11 @@ export function setTheme(theme: string, forceReload = false) {
theme = "darkly"; theme = "darkly";
} }
let themeList = await fetchThemeList();
// Unload all the other themes // Unload all the other themes
for (var i = 0; i < themes.length; i++) { for (var i = 0; i < themeList.length; i++) {
let styleSheet = document.getElementById(themes[i]); let styleSheet = document.getElementById(themeList[i]);
if (styleSheet) { if (styleSheet) {
styleSheet.setAttribute("disabled", "disabled"); styleSheet.setAttribute("disabled", "disabled");
} }
@ -391,7 +380,8 @@ export function setTheme(theme: string, forceReload = false) {
document.getElementById("default-dark")?.setAttribute("disabled", "disabled"); document.getElementById("default-dark")?.setAttribute("disabled", "disabled");
// Load the theme dynamically // Load the theme dynamically
let cssLoc = `/static/assets/css/themes/${theme}.min.css`; let cssLoc = `/css/themes/${theme}.min.css`;
loadCss(theme, cssLoc); loadCss(theme, cssLoc);
document.getElementById(theme).removeAttribute("disabled"); document.getElementById(theme).removeAttribute("disabled");
} }