mirror of
https://github.com/LemmyNet/lemmy-ui.git
synced 2024-12-25 10:27:29 +00:00
Custom themes (#584)
* Add support for custom themes (fixes #560) * load theme list in site-form.tsx
This commit is contained in:
parent
20207bd599
commit
2ffe7e4c6f
1
extra_themes/test.min.css
vendored
Normal file
1
extra_themes/test.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -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 {
|
||||
|
|
|
@ -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)"
|
||||
/>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue