feat(themes): Online Themes Redesign

This commit is contained in:
MrDiamondDog 2024-06-20 18:11:01 -06:00 committed by Vendicated
parent 18df66a4b4
commit 506aab14c9
No known key found for this signature in database
GPG key ID: D66986BAF75ECF18
4 changed files with 84 additions and 69 deletions

View file

@ -34,6 +34,7 @@ export interface Settings {
useQuickCss: boolean; useQuickCss: boolean;
enableReactDevtools: boolean; enableReactDevtools: boolean;
themeLinks: string[]; themeLinks: string[];
enabledThemeLinks: string[];
enabledThemes: string[]; enabledThemes: string[];
frameless: boolean; frameless: boolean;
transparent: boolean; transparent: boolean;
@ -81,6 +82,7 @@ const DefaultSettings: Settings = {
autoUpdateNotification: true, autoUpdateNotification: true,
useQuickCss: true, useQuickCss: true,
themeLinks: [], themeLinks: [],
enabledThemeLinks: [],
enabledThemes: [], enabledThemes: [],
enableReactDevtools: false, enableReactDevtools: false,
frameless: false, frameless: false,

View file

@ -22,15 +22,13 @@ import { Flex } from "@components/Flex";
import { DeleteIcon } from "@components/Icons"; import { DeleteIcon } from "@components/Icons";
import { Link } from "@components/Link"; import { Link } from "@components/Link";
import PluginModal from "@components/PluginSettings/PluginModal"; import PluginModal from "@components/PluginSettings/PluginModal";
import type { UserThemeHeader } from "@main/themes"; import { getThemeInfo, type UserThemeHeader } from "@main/themes";
import { openInviteModal } from "@utils/discord"; import { openInviteModal } from "@utils/discord";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { openModal } from "@utils/modal"; import { openModal } from "@utils/modal";
import { showItemInFolder } from "@utils/native"; import { showItemInFolder } from "@utils/native";
import { useAwaiter } from "@utils/react"; import { useAwaiter } from "@utils/react";
import { findByPropsLazy, findLazy } from "@webpack"; import { findByPropsLazy, findLazy } from "@webpack";
import { Button, Card, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common"; import { Button, Card, Forms, React, showToast, TabBar, TextInput, useEffect, useRef, useState } from "@webpack/common";
import type { ComponentType, Ref, SyntheticEvent } from "react"; import type { ComponentType, Ref, SyntheticEvent } from "react";
import { AddonCard } from "./AddonCard"; import { AddonCard } from "./AddonCard";
@ -49,13 +47,16 @@ const TextAreaProps = findLazy(m => typeof m.textarea === "string");
const cl = classNameFactory("vc-settings-theme-"); const cl = classNameFactory("vc-settings-theme-");
function Validator({ link }: { link: string; }) { function Validator({ link, onValidate }: { link: string; onValidate: (valid: boolean) => void; }) {
const [res, err, pending] = useAwaiter(() => fetch(link).then(res => { const [res, err, pending] = useAwaiter(() => fetch(link).then(res => {
if (res.status > 300) throw `${res.status} ${res.statusText}`; if (res.status > 300) throw `${res.status} ${res.statusText}`;
const contentType = res.headers.get("Content-Type"); const contentType = res.headers.get("Content-Type");
if (!contentType?.startsWith("text/css") && !contentType?.startsWith("text/plain")) if (!contentType?.startsWith("text/css") && !contentType?.startsWith("text/plain")) {
onValidate(false);
throw "Not a CSS file. Remember to use the raw link!"; throw "Not a CSS file. Remember to use the raw link!";
}
onValidate(true);
return "Okay!"; return "Okay!";
})); }));
@ -70,41 +71,15 @@ function Validator({ link }: { link: string; }) {
}}>{text}</Forms.FormText>; }}>{text}</Forms.FormText>;
} }
function Validators({ themeLinks }: { themeLinks: string[]; }) {
if (!themeLinks.length) return null;
return (
<>
<Forms.FormTitle className={Margins.top20} tag="h5">Validator</Forms.FormTitle>
<Forms.FormText>This section will tell you whether your themes can successfully be loaded</Forms.FormText>
<div>
{themeLinks.map(link => (
<Card style={{
padding: ".5em",
marginBottom: ".5em",
marginTop: ".5em"
}} key={link}>
<Forms.FormTitle tag="h5" style={{
overflowWrap: "break-word"
}}>
{link}
</Forms.FormTitle>
<Validator link={link} />
</Card>
))}
</div>
</>
);
}
interface ThemeCardProps { interface ThemeCardProps {
theme: UserThemeHeader; theme: UserThemeHeader;
enabled: boolean; enabled: boolean;
onChange: (enabled: boolean) => void; onChange: (enabled: boolean) => void;
onDelete: () => void; onDelete: () => void;
showDeleteButton?: boolean;
} }
function ThemeCard({ theme, enabled, onChange, onDelete }: ThemeCardProps) { function ThemeCard({ theme, enabled, onChange, onDelete, showDeleteButton }: ThemeCardProps) {
return ( return (
<AddonCard <AddonCard
name={theme.name} name={theme.name}
@ -113,7 +88,7 @@ function ThemeCard({ theme, enabled, onChange, onDelete }: ThemeCardProps) {
enabled={enabled} enabled={enabled}
setEnabled={onChange} setEnabled={onChange}
infoButton={ infoButton={
IS_WEB && ( (IS_WEB || showDeleteButton) && (
<div style={{ cursor: "pointer", color: "var(--status-danger" }} onClick={onDelete}> <div style={{ cursor: "pointer", color: "var(--status-danger" }} onClick={onDelete}>
<DeleteIcon /> <DeleteIcon />
</div> </div>
@ -146,16 +121,19 @@ enum ThemeTab {
} }
function ThemesTab() { function ThemesTab() {
const settings = useSettings(["themeLinks", "enabledThemes"]); const settings = useSettings(["themeLinks", "enabledThemeLinks", "enabledThemes"]);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const [currentTab, setCurrentTab] = useState(ThemeTab.LOCAL); const [currentTab, setCurrentTab] = useState(ThemeTab.LOCAL);
const [themeText, setThemeText] = useState(settings.themeLinks.join("\n")); const [currentThemeLink, setCurrentThemeLink] = useState("");
const [themeLinkValid, setThemeLinkValid] = useState(false);
const [userThemes, setUserThemes] = useState<UserThemeHeader[] | null>(null); const [userThemes, setUserThemes] = useState<UserThemeHeader[] | null>(null);
const [onlineThemes, setOnlineThemes] = useState<(UserThemeHeader & { link: string; })[] | null>(null);
const [themeDir, , themeDirPending] = useAwaiter(VencordNative.themes.getThemesDir); const [themeDir, , themeDirPending] = useAwaiter(VencordNative.themes.getThemesDir);
useEffect(() => { useEffect(() => {
refreshLocalThemes(); refreshLocalThemes();
refreshOnlineThemes();
}, []); }, []);
async function refreshLocalThemes() { async function refreshLocalThemes() {
@ -198,7 +176,7 @@ function ThemesTab() {
refreshLocalThemes(); refreshLocalThemes();
} }
function renderLocalThemes() { function LocalThemes() {
return ( return (
<> <>
<Card className="vc-settings-card"> <Card className="vc-settings-card">
@ -288,37 +266,63 @@ function ThemesTab() {
); );
} }
// When the user leaves the online theme textbox, update the settings function addThemeLink(link: string) {
function onBlur() { if (!themeLinkValid) return;
settings.themeLinks = [...new Set( if (settings.themeLinks.includes(link)) return;
themeText
.trim() settings.themeLinks = [...settings.themeLinks, link];
.split(/\n+/) setCurrentThemeLink("");
.map(s => s.trim()) refreshOnlineThemes();
.filter(Boolean)
)];
} }
function renderOnlineThemes() { async function refreshOnlineThemes() {
const themes = await Promise.all(settings.themeLinks.map(async link => {
const css = await fetch(link).then(res => res.text());
return { ...getThemeInfo(css, link), link };
}));
setOnlineThemes(themes);
}
function onThemeLinkEnabledChange(link: string, enabled: boolean) {
if (enabled) {
if (settings.enabledThemeLinks.includes(link)) return;
settings.enabledThemeLinks = [...settings.enabledThemeLinks, link];
} else {
settings.enabledThemeLinks = settings.enabledThemeLinks.filter(f => f !== link);
}
}
function deleteThemeLink(link: string) {
settings.themeLinks = settings.themeLinks.filter(f => f !== link);
refreshOnlineThemes();
}
function OnlineThemes() {
return ( 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"> <Forms.FormSection title="Online Themes" tag="h5">
<TextArea <Card className="vc-settings-theme-add-card">
value={themeText} <Forms.FormText>Make sure to use direct links to files (raw or github.io)!</Forms.FormText>
onChange={setThemeText} <Flex flexDirection="row">
className={classes(TextAreaProps.textarea, "vc-settings-theme-links")} <TextInput placeholder="Theme Link" className="vc-settings-theme-link-input" value={currentThemeLink} onChange={setCurrentThemeLink} />
placeholder="Theme Links" <Button onClick={() => addThemeLink(currentThemeLink)} disabled={!themeLinkValid}>Add</Button>
spellCheck={false} </Flex>
onBlur={onBlur} {currentThemeLink && <Validator link={currentThemeLink} onValidate={setThemeLinkValid} />}
rows={10} </Card>
/>
<Validators themeLinks={settings.themeLinks} /> <div className={cl("grid")}>
{onlineThemes?.map(theme => {
return <ThemeCard
key={theme.fileName}
enabled={settings.enabledThemeLinks.includes(theme.link)}
onChange={enabled => onThemeLinkEnabledChange(theme.link, enabled)}
onDelete={() => deleteThemeLink(theme.link)}
showDeleteButton
theme={theme}
/>;
})}
</div>
</Forms.FormSection> </Forms.FormSection>
</> </>
); );
@ -347,8 +351,8 @@ function ThemesTab() {
</TabBar.Item> </TabBar.Item>
</TabBar> </TabBar>
{currentTab === ThemeTab.LOCAL && renderLocalThemes()} {currentTab === ThemeTab.LOCAL && <LocalThemes />}
{currentTab === ThemeTab.ONLINE && renderOnlineThemes()} {currentTab === ThemeTab.ONLINE && <OnlineThemes />}
</SettingsTab> </SettingsTab>
); );
} }

View file

@ -27,3 +27,12 @@
.vc-settings-theme-author::before { .vc-settings-theme-author::before {
content: "by "; content: "by ";
} }
.vc-settings-theme-link-input {
width: 100%;
}
.vc-settings-theme-add-card {
padding: 1em;
margin-bottom: 16px;
}

View file

@ -57,9 +57,9 @@ export async function toggle(isEnabled: boolean) {
async function initThemes() { async function initThemes() {
themesStyle ??= createStyle("vencord-themes"); themesStyle ??= createStyle("vencord-themes");
const { themeLinks, enabledThemes } = Settings; const { enabledThemeLinks, enabledThemes } = Settings;
const links: string[] = [...themeLinks]; const links: string[] = [...enabledThemeLinks];
if (IS_WEB) { if (IS_WEB) {
for (const theme of enabledThemes) { for (const theme of enabledThemes) {
@ -83,7 +83,7 @@ document.addEventListener("DOMContentLoaded", () => {
toggle(Settings.useQuickCss); toggle(Settings.useQuickCss);
SettingsStore.addChangeListener("useQuickCss", toggle); SettingsStore.addChangeListener("useQuickCss", toggle);
SettingsStore.addChangeListener("themeLinks", initThemes); SettingsStore.addChangeListener("enabledThemeLinks", initThemes);
SettingsStore.addChangeListener("enabledThemes", initThemes); SettingsStore.addChangeListener("enabledThemes", initThemes);
if (!IS_WEB) if (!IS_WEB)