2022-12-01 03:01:44 +01:00
|
|
|
/*
|
|
|
|
* Vencord, a modification for Discord's desktop app
|
|
|
|
* Copyright (c) 2022 Vendicated and contributors
|
|
|
|
*
|
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
|
|
* (at your option) any later version.
|
|
|
|
*
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
2023-09-10 13:40:04 +01:00
|
|
|
import "./themesStyles.css";
|
|
|
|
|
2023-09-08 22:19:21 +01:00
|
|
|
import { Settings, useSettings } from "@api/Settings";
|
2023-08-04 18:52:20 +01:00
|
|
|
import { classNameFactory } from "@api/Styles";
|
|
|
|
import { Flex } from "@components/Flex";
|
2023-09-27 21:42:29 +01:00
|
|
|
import { CogWheel, DeleteIcon } from "@components/Icons";
|
2022-12-01 03:01:44 +01:00
|
|
|
import { Link } from "@components/Link";
|
2023-09-10 13:40:04 +01:00
|
|
|
import { AddonCard } from "@components/VencordSettings/AddonCard";
|
|
|
|
import { SettingsTab, wrapTab } from "@components/VencordSettings/shared";
|
2023-03-01 20:35:08 +00:00
|
|
|
import { Margins } from "@utils/margins";
|
2023-09-10 13:40:04 +01:00
|
|
|
import { classes } from "@utils/misc";
|
|
|
|
import { openModal } from "@utils/modal";
|
2023-08-04 18:52:20 +01:00
|
|
|
import { showItemInFolder } from "@utils/native";
|
2023-09-10 13:40:04 +01:00
|
|
|
import { useAwaiter } from "@utils/react";
|
2023-09-02 21:27:22 +01:00
|
|
|
import type { ThemeHeader } from "@utils/themes";
|
2023-09-08 16:35:37 +01:00
|
|
|
import { getThemeInfo, stripBOM, type UserThemeHeader } from "@utils/themes/bd";
|
|
|
|
import { usercssParse } from "@utils/themes/usercss";
|
2023-09-10 13:40:04 +01:00
|
|
|
import { findByCodeLazy, findByPropsLazy, findLazy } from "@webpack";
|
|
|
|
import { Button, Card, FluxDispatcher, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common";
|
|
|
|
import type { ComponentType, Ref, SyntheticEvent } from "react";
|
2023-09-02 21:29:44 +01:00
|
|
|
import type { UserstyleHeader } from "usercss-meta";
|
2022-12-01 03:01:44 +01:00
|
|
|
|
2023-09-10 13:40:04 +01:00
|
|
|
import { UserCSSSettingsModal } from "./UserCSSModal";
|
2023-05-12 01:40:43 +02:00
|
|
|
|
2023-08-04 18:52:20 +01:00
|
|
|
type FileInput = ComponentType<{
|
|
|
|
ref: Ref<HTMLInputElement>;
|
|
|
|
onChange: (e: SyntheticEvent<HTMLInputElement>) => void;
|
|
|
|
multiple?: boolean;
|
|
|
|
filters?: { name?: string; extensions: string[]; }[];
|
|
|
|
}>;
|
|
|
|
|
|
|
|
const InviteActions = findByPropsLazy("resolveInvite");
|
|
|
|
const FileInput: FileInput = findByCodeLazy("activateUploadDialogue=");
|
2023-09-10 13:40:04 +01:00
|
|
|
|
2022-12-01 03:01:44 +01:00
|
|
|
const TextAreaProps = findLazy(m => typeof m.textarea === "string");
|
|
|
|
|
2023-08-04 18:52:20 +01:00
|
|
|
const cl = classNameFactory("vc-settings-theme-");
|
|
|
|
|
2023-09-08 22:19:21 +01:00
|
|
|
|
2022-12-01 03:01:44 +01:00
|
|
|
function Validator({ link }: { link: string; }) {
|
|
|
|
const [res, err, pending] = useAwaiter(() => fetch(link).then(res => {
|
|
|
|
if (res.status > 300) throw `${res.status} ${res.statusText}`;
|
|
|
|
const contentType = res.headers.get("Content-Type");
|
|
|
|
if (!contentType?.startsWith("text/css") && !contentType?.startsWith("text/plain"))
|
|
|
|
throw "Not a CSS file. Remember to use the raw link!";
|
|
|
|
|
|
|
|
return "Okay!";
|
|
|
|
}));
|
|
|
|
|
|
|
|
const text = pending
|
|
|
|
? "Checking..."
|
|
|
|
: err
|
|
|
|
? `Error: ${err instanceof Error ? err.message : String(err)}`
|
|
|
|
: "Valid!";
|
|
|
|
|
|
|
|
return <Forms.FormText style={{
|
|
|
|
color: pending ? "var(--text-muted)" : err ? "var(--text-danger)" : "var(--text-positive)"
|
|
|
|
}}>{text}</Forms.FormText>;
|
|
|
|
}
|
|
|
|
|
|
|
|
function Validators({ themeLinks }: { themeLinks: string[]; }) {
|
|
|
|
if (!themeLinks.length) return null;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<>
|
2023-03-01 20:35:08 +00:00
|
|
|
<Forms.FormTitle className={Margins.top20} tag="h5">Validator</Forms.FormTitle>
|
2022-12-01 03:01:44 +01:00
|
|
|
<Forms.FormText>This section will tell you whether your themes can successfully be loaded</Forms.FormText>
|
|
|
|
<div>
|
|
|
|
{themeLinks.map(link => (
|
|
|
|
<Card style={{
|
|
|
|
padding: ".5em",
|
2022-12-08 22:51:18 +00:00
|
|
|
marginBottom: ".5em",
|
|
|
|
marginTop: ".5em"
|
2022-12-01 03:01:44 +01:00
|
|
|
}} key={link}>
|
|
|
|
<Forms.FormTitle tag="h5" style={{
|
|
|
|
overflowWrap: "break-word"
|
|
|
|
}}>
|
|
|
|
{link}
|
|
|
|
</Forms.FormTitle>
|
|
|
|
<Validator link={link} />
|
|
|
|
</Card>
|
|
|
|
))}
|
|
|
|
</div>
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-09-15 19:42:01 +01:00
|
|
|
interface OtherThemeCardProps {
|
2023-08-04 18:52:20 +01:00
|
|
|
theme: UserThemeHeader;
|
|
|
|
enabled: boolean;
|
|
|
|
onChange: (enabled: boolean) => void;
|
|
|
|
onDelete: () => void;
|
|
|
|
}
|
|
|
|
|
2023-09-02 17:51:17 +01:00
|
|
|
interface UserCSSCardProps {
|
|
|
|
theme: UserstyleHeader;
|
|
|
|
enabled: boolean;
|
|
|
|
onChange: (enabled: boolean) => void;
|
|
|
|
onDelete: () => void;
|
|
|
|
}
|
|
|
|
|
|
|
|
function UserCSSThemeCard({ theme, enabled, onChange, onDelete }: UserCSSCardProps) {
|
|
|
|
return (
|
|
|
|
<AddonCard
|
2023-09-08 15:54:25 +01:00
|
|
|
name={theme.name ?? "Unknown"}
|
2023-09-02 17:51:17 +01:00
|
|
|
description={theme.description}
|
|
|
|
author={theme.author ?? "Unknown"}
|
|
|
|
enabled={enabled}
|
|
|
|
setEnabled={onChange}
|
|
|
|
infoButton={
|
|
|
|
<>
|
2023-09-08 22:19:21 +01:00
|
|
|
{theme.vars && (
|
|
|
|
<div style={{ cursor: "pointer" }} onClick={
|
|
|
|
() => openModal(modalProps =>
|
|
|
|
<UserCSSSettingsModal modalProps={modalProps} theme={theme} />)
|
|
|
|
}>
|
|
|
|
<CogWheel />
|
|
|
|
</div>
|
|
|
|
)}
|
2023-09-02 17:51:17 +01:00
|
|
|
{IS_WEB && (
|
|
|
|
<div style={{ cursor: "pointer", color: "var(--status-danger" }} onClick={onDelete}>
|
2023-09-27 21:42:29 +01:00
|
|
|
<DeleteIcon />
|
2023-09-02 17:51:17 +01:00
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</>
|
|
|
|
}
|
|
|
|
footer={
|
|
|
|
<Flex flexDirection="row" style={{ gap: "0.2em" }}>
|
|
|
|
{!!theme.homepageURL && <Link href={theme.homepageURL}>Homepage</Link>}
|
|
|
|
{!!(theme.homepageURL && theme.supportURL) && " • "}
|
|
|
|
{!!theme.supportURL && <Link href={theme.supportURL}>Support</Link>}
|
|
|
|
</Flex>
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-09-15 19:42:01 +01:00
|
|
|
function OtherThemeCard({ theme, enabled, onChange, onDelete }: OtherThemeCardProps) {
|
2023-08-04 18:52:20 +01:00
|
|
|
return (
|
|
|
|
<AddonCard
|
|
|
|
name={theme.name}
|
|
|
|
description={theme.description}
|
|
|
|
author={theme.author}
|
|
|
|
enabled={enabled}
|
|
|
|
setEnabled={onChange}
|
|
|
|
infoButton={
|
|
|
|
IS_WEB && (
|
|
|
|
<div style={{ cursor: "pointer", color: "var(--status-danger" }} onClick={onDelete}>
|
2023-09-26 20:47:12 +01:00
|
|
|
<DeleteIcon />
|
2023-08-04 18:52:20 +01:00
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
footer={
|
|
|
|
<Flex flexDirection="row" style={{ gap: "0.2em" }}>
|
|
|
|
{!!theme.website && <Link href={theme.website}>Website</Link>}
|
|
|
|
{!!(theme.website && theme.invite) && " • "}
|
|
|
|
{!!theme.invite && (
|
|
|
|
<Link
|
|
|
|
href={`https://discord.gg/${theme.invite}`}
|
|
|
|
onClick={async e => {
|
|
|
|
e.preventDefault();
|
|
|
|
const { invite } = await InviteActions.resolveInvite(theme.invite, "Desktop Modal");
|
|
|
|
if (!invite) return showToast("Invalid or expired invite");
|
|
|
|
|
|
|
|
FluxDispatcher.dispatch({
|
|
|
|
type: "INVITE_MODAL_OPEN",
|
|
|
|
invite,
|
|
|
|
code: theme.invite,
|
|
|
|
context: "APP"
|
|
|
|
});
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
Discord Server
|
|
|
|
</Link>
|
|
|
|
)}
|
|
|
|
</Flex>
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
enum ThemeTab {
|
|
|
|
LOCAL,
|
|
|
|
ONLINE
|
|
|
|
}
|
|
|
|
|
2023-05-12 01:40:43 +02:00
|
|
|
function ThemesTab() {
|
2023-08-04 18:52:20 +01:00
|
|
|
const settings = useSettings(["themeLinks", "enabledThemes"]);
|
|
|
|
|
|
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
const [currentTab, setCurrentTab] = useState(ThemeTab.LOCAL);
|
|
|
|
const [themeText, setThemeText] = useState(settings.themeLinks.join("\n"));
|
2023-09-02 17:51:17 +01:00
|
|
|
const [userThemes, setUserThemes] = useState<ThemeHeader[] | null>(null);
|
2023-08-04 18:52:20 +01:00
|
|
|
const [themeDir, , themeDirPending] = useAwaiter(VencordNative.themes.getThemesDir);
|
2022-12-01 03:01:44 +01:00
|
|
|
|
2023-08-04 18:52:20 +01:00
|
|
|
useEffect(() => {
|
|
|
|
refreshLocalThemes();
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
async function refreshLocalThemes() {
|
|
|
|
const themes = await VencordNative.themes.getThemesList();
|
2023-09-08 16:35:37 +01:00
|
|
|
|
|
|
|
const themeInfo: ThemeHeader[] = [];
|
|
|
|
|
|
|
|
for (const { fileName, content } of themes) {
|
|
|
|
if (!fileName.endsWith(".css")) continue;
|
|
|
|
|
2023-09-25 19:06:36 +01:00
|
|
|
if ((!IS_WEB || "armcord" in window) && fileName.endsWith(".user.css")) {
|
2023-09-08 16:35:37 +01:00
|
|
|
// handle it as usercss
|
2023-09-08 22:19:21 +01:00
|
|
|
const header = await usercssParse(content, fileName);
|
|
|
|
|
2023-09-08 16:35:37 +01:00
|
|
|
themeInfo.push({
|
|
|
|
type: "usercss",
|
2023-09-08 22:19:21 +01:00
|
|
|
header
|
2023-09-08 16:35:37 +01:00
|
|
|
});
|
2023-09-08 22:19:21 +01:00
|
|
|
|
|
|
|
Settings.userCssVars[header.id] ??= {};
|
|
|
|
|
|
|
|
for (const [name, varInfo] of Object.entries(header.vars ?? {})) {
|
|
|
|
let normalizedValue = "";
|
|
|
|
|
|
|
|
switch (varInfo.type) {
|
|
|
|
case "text":
|
|
|
|
case "color":
|
|
|
|
normalizedValue = varInfo.default;
|
|
|
|
break;
|
2023-09-10 14:09:00 +01:00
|
|
|
case "select":
|
|
|
|
normalizedValue = varInfo.options.find(v => v.name === varInfo.default)!.value;
|
|
|
|
break;
|
2023-09-08 22:19:21 +01:00
|
|
|
case "checkbox":
|
|
|
|
normalizedValue = varInfo.default ? "1" : "0";
|
|
|
|
break;
|
|
|
|
case "range":
|
|
|
|
normalizedValue = `${varInfo.default}${varInfo.units}`;
|
|
|
|
break;
|
|
|
|
case "number":
|
|
|
|
normalizedValue = String(varInfo.default);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
Settings.userCssVars[header.id][name] ??= normalizedValue;
|
|
|
|
}
|
2023-09-08 16:35:37 +01:00
|
|
|
} else {
|
|
|
|
// presumably BD but could also be plain css
|
|
|
|
themeInfo.push({
|
2023-09-15 19:42:01 +01:00
|
|
|
type: "other",
|
2023-09-08 16:35:37 +01:00
|
|
|
header: getThemeInfo(stripBOM(content), fileName)
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
setUserThemes(themeInfo);
|
2023-08-04 18:52:20 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// When a local theme is enabled/disabled, update the settings
|
|
|
|
function onLocalThemeChange(fileName: string, value: boolean) {
|
|
|
|
if (value) {
|
|
|
|
if (settings.enabledThemes.includes(fileName)) return;
|
|
|
|
settings.enabledThemes = [...settings.enabledThemes, fileName];
|
|
|
|
} else {
|
|
|
|
settings.enabledThemes = settings.enabledThemes.filter(f => f !== fileName);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function onFileUpload(e: SyntheticEvent<HTMLInputElement>) {
|
|
|
|
e.stopPropagation();
|
|
|
|
e.preventDefault();
|
|
|
|
if (!e.currentTarget?.files?.length) return;
|
|
|
|
const { files } = e.currentTarget;
|
|
|
|
|
|
|
|
const uploads = Array.from(files, file => {
|
|
|
|
const { name } = file;
|
|
|
|
if (!name.endsWith(".css")) return;
|
|
|
|
|
|
|
|
return new Promise<void>((resolve, reject) => {
|
|
|
|
const reader = new FileReader();
|
|
|
|
reader.onload = () => {
|
|
|
|
VencordNative.themes.uploadTheme(name, reader.result as string)
|
|
|
|
.then(resolve)
|
|
|
|
.catch(reject);
|
|
|
|
};
|
|
|
|
reader.readAsText(file);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
await Promise.all(uploads);
|
|
|
|
refreshLocalThemes();
|
|
|
|
}
|
|
|
|
|
|
|
|
function renderLocalThemes() {
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<Card className="vc-settings-card">
|
|
|
|
<Forms.FormTitle tag="h5">Find Themes:</Forms.FormTitle>
|
|
|
|
<div style={{ marginBottom: ".5em", display: "flex", flexDirection: "column" }}>
|
|
|
|
<Link style={{ marginRight: ".5em" }} href="https://betterdiscord.app/themes">
|
|
|
|
BetterDiscord Themes
|
|
|
|
</Link>
|
|
|
|
<Link href="https://github.com/search?q=discord+theme">GitHub</Link>
|
|
|
|
</div>
|
|
|
|
<Forms.FormText>If using the BD site, click on "Download" and place the downloaded .theme.css file into your themes folder.</Forms.FormText>
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
<Forms.FormSection title="Local Themes">
|
|
|
|
<Card className="vc-settings-quick-actions-card">
|
|
|
|
<>
|
|
|
|
{IS_WEB ?
|
|
|
|
(
|
|
|
|
<Button
|
|
|
|
size={Button.Sizes.SMALL}
|
|
|
|
disabled={themeDirPending}
|
|
|
|
>
|
|
|
|
Upload Theme
|
|
|
|
<FileInput
|
|
|
|
ref={fileInputRef}
|
|
|
|
onChange={onFileUpload}
|
|
|
|
multiple={true}
|
2023-08-25 14:37:50 +02:00
|
|
|
filters={[{ extensions: ["css"] }]}
|
2023-08-04 18:52:20 +01:00
|
|
|
/>
|
|
|
|
</Button>
|
|
|
|
) : (
|
|
|
|
<Button
|
|
|
|
onClick={() => showItemInFolder(themeDir!)}
|
|
|
|
size={Button.Sizes.SMALL}
|
|
|
|
disabled={themeDirPending}
|
|
|
|
>
|
|
|
|
Open Themes Folder
|
|
|
|
</Button>
|
|
|
|
)}
|
|
|
|
<Button
|
|
|
|
onClick={refreshLocalThemes}
|
|
|
|
size={Button.Sizes.SMALL}
|
|
|
|
>
|
|
|
|
Load missing Themes
|
|
|
|
</Button>
|
2023-10-09 03:15:43 +02:00
|
|
|
<Button
|
|
|
|
onClick={() => VencordNative.quickCss.openEditor()}
|
|
|
|
size={Button.Sizes.SMALL}
|
|
|
|
>
|
|
|
|
Edit QuickCSS
|
|
|
|
</Button>
|
2023-08-04 18:52:20 +01:00
|
|
|
</>
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
<div className={cl("grid")}>
|
2023-09-02 17:51:17 +01:00
|
|
|
{userThemes?.map(({ type, header: theme }: ThemeHeader) => (
|
2023-09-15 19:42:01 +01:00
|
|
|
type === "other" ? (
|
|
|
|
<OtherThemeCard
|
2023-09-02 17:51:17 +01:00
|
|
|
key={theme.fileName}
|
|
|
|
enabled={settings.enabledThemes.includes(theme.fileName)}
|
|
|
|
onChange={enabled => onLocalThemeChange(theme.fileName, enabled)}
|
|
|
|
onDelete={async () => {
|
|
|
|
onLocalThemeChange(theme.fileName, false);
|
|
|
|
await VencordNative.themes.deleteTheme(theme.fileName);
|
|
|
|
refreshLocalThemes();
|
|
|
|
}}
|
|
|
|
theme={theme as UserThemeHeader}
|
|
|
|
/>
|
|
|
|
) : (
|
|
|
|
<UserCSSThemeCard
|
|
|
|
key={theme.fileName}
|
|
|
|
enabled={settings.enabledThemes.includes(theme.fileName)}
|
|
|
|
onChange={enabled => onLocalThemeChange(theme.fileName, enabled)}
|
|
|
|
onDelete={async () => {
|
|
|
|
onLocalThemeChange(theme.fileName, false);
|
|
|
|
await VencordNative.themes.deleteTheme(theme.fileName);
|
|
|
|
refreshLocalThemes();
|
|
|
|
}}
|
|
|
|
theme={theme as UserstyleHeader}
|
|
|
|
/>
|
|
|
|
)))}
|
2023-08-04 18:52:20 +01:00
|
|
|
</div>
|
|
|
|
</Forms.FormSection>
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// When the user leaves the online theme textbox, update the settings
|
2022-12-01 03:01:44 +01:00
|
|
|
function onBlur() {
|
|
|
|
settings.themeLinks = [...new Set(
|
2023-01-25 17:49:19 +01:00
|
|
|
themeText
|
2022-12-01 03:01:44 +01:00
|
|
|
.trim()
|
|
|
|
.split(/\n+/)
|
|
|
|
.map(s => s.trim())
|
|
|
|
.filter(Boolean)
|
|
|
|
)];
|
|
|
|
}
|
|
|
|
|
2023-08-04 18:52:20 +01:00
|
|
|
function renderOnlineThemes() {
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<Card className="vc-settings-card vc-text-selectable">
|
|
|
|
<Forms.FormTitle tag="h5">Paste links to css files here</Forms.FormTitle>
|
|
|
|
<Forms.FormText>One link per line</Forms.FormText>
|
|
|
|
<Forms.FormText>Make sure to use direct links to files (raw or github.io)!</Forms.FormText>
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
<Forms.FormSection title="Online Themes" tag="h5">
|
|
|
|
<TextArea
|
|
|
|
value={themeText}
|
|
|
|
onChange={setThemeText}
|
|
|
|
className={classes(TextAreaProps.textarea, "vc-settings-theme-links")}
|
|
|
|
placeholder="Theme Links"
|
|
|
|
spellCheck={false}
|
|
|
|
onBlur={onBlur}
|
|
|
|
rows={10}
|
|
|
|
/>
|
|
|
|
<Validators themeLinks={settings.themeLinks} />
|
|
|
|
</Forms.FormSection>
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-12-01 03:01:44 +01:00
|
|
|
return (
|
2023-05-12 01:40:43 +02:00
|
|
|
<SettingsTab title="Themes">
|
2023-08-04 18:52:20 +01:00
|
|
|
<TabBar
|
|
|
|
type="top"
|
|
|
|
look="brand"
|
|
|
|
className="vc-settings-tab-bar"
|
|
|
|
selectedItem={currentTab}
|
|
|
|
onItemSelect={setCurrentTab}
|
|
|
|
>
|
|
|
|
<TabBar.Item
|
|
|
|
className="vc-settings-tab-bar-item"
|
|
|
|
id={ThemeTab.LOCAL}
|
|
|
|
>
|
|
|
|
Local Themes
|
|
|
|
</TabBar.Item>
|
|
|
|
<TabBar.Item
|
|
|
|
className="vc-settings-tab-bar-item"
|
|
|
|
id={ThemeTab.ONLINE}
|
|
|
|
>
|
|
|
|
Online Themes
|
|
|
|
</TabBar.Item>
|
|
|
|
</TabBar>
|
|
|
|
|
|
|
|
{currentTab === ThemeTab.LOCAL && renderLocalThemes()}
|
|
|
|
{currentTab === ThemeTab.ONLINE && renderOnlineThemes()}
|
2023-05-12 01:40:43 +02:00
|
|
|
</SettingsTab>
|
2022-12-01 03:01:44 +01:00
|
|
|
);
|
2023-05-12 01:40:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
export default wrapTab(ThemesTab, "Themes");
|