mirror of
https://github.com/LemmyNet/lemmy-ui.git
synced 2024-11-26 14:21:13 +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
6 changed files with 83 additions and 30 deletions
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 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 {
|
||||||
|
|
|
@ -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)"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue