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 fs from "fs";
import { IncomingHttpHeaders } from "http";
import { Helmet } from "inferno-helmet";
import { matchPath, StaticRouter } from "inferno-router";
@ -23,6 +24,8 @@ const server = express();
const [hostname, port] = process.env["LEMMY_UI_HOST"]
? process.env["LEMMY_UI_HOST"].split(":")
: ["0.0.0.0", "1234"];
const extraThemesFolder =
process.env["LEMMY_UI_EXTRA_THEMES_FOLDER"] || "./extra_themes";
server.use(express.json());
server.use(express.urlencoded({ extended: false }));
@ -46,6 +49,54 @@ server.get("/robots.txt", async (_req, res) => {
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.get("/*", async (req, res) => {
try {

View file

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

View file

@ -6,7 +6,7 @@ import { WebSocketService } from "../../services";
import {
authField,
capitalizeFirstLetter,
themes,
fetchThemeList,
wsClient,
} from "../../utils";
import { Spinner } from "../common/icon";
@ -21,6 +21,7 @@ interface SiteFormProps {
interface SiteFormState {
siteForm: EditSite;
loading: boolean;
themeList: string[];
}
export class SiteForm extends Component<SiteFormProps, SiteFormState> {
@ -40,6 +41,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
auth: authField(),
},
loading: false,
themeList: [],
};
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
componentWillReceiveProps() {
this.state.loading = false;
@ -336,7 +343,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
class="custom-select w-auto"
>
<option value="browser">{i18n.t("browser_default")}</option>
{themes.map(theme => (
{this.state.themeList.map(theme => (
<option value={theme}>{theme}</option>
))}
</select>

View file

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

View file

@ -77,23 +77,6 @@ export const mentionDropdownFetchLimit = 10;
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 =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
@ -365,7 +348,11 @@ function getBrowserLanguages(): string[] {
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()) {
return;
}
@ -377,9 +364,11 @@ export function setTheme(theme: string, forceReload = false) {
theme = "darkly";
}
let themeList = await fetchThemeList();
// Unload all the other themes
for (var i = 0; i < themes.length; i++) {
let styleSheet = document.getElementById(themes[i]);
for (var i = 0; i < themeList.length; i++) {
let styleSheet = document.getElementById(themeList[i]);
if (styleSheet) {
styleSheet.setAttribute("disabled", "disabled");
}
@ -391,7 +380,8 @@ export function setTheme(theme: string, forceReload = false) {
document.getElementById("default-dark")?.setAttribute("disabled", "disabled");
// Load the theme dynamically
let cssLoc = `/static/assets/css/themes/${theme}.min.css`;
let cssLoc = `/css/themes/${theme}.min.css`;
loadCss(theme, cssLoc);
document.getElementById(theme).removeAttribute("disabled");
}