diff options
Diffstat (limited to 'src/components/VencordSettings/ThemesTab.tsx')
-rw-r--r-- | src/components/VencordSettings/ThemesTab.tsx | 299 |
1 files changed, 252 insertions, 47 deletions
diff --git a/src/components/VencordSettings/ThemesTab.tsx b/src/components/VencordSettings/ThemesTab.tsx index a670394..c44ad45 100644 --- a/src/components/VencordSettings/ThemesTab.tsx +++ b/src/components/VencordSettings/ThemesTab.tsx @@ -17,16 +17,35 @@ */ import { useSettings } from "@api/Settings"; +import { classNameFactory } from "@api/Styles"; +import { Flex } from "@components/Flex"; import { Link } from "@components/Link"; import { Margins } from "@utils/margins"; +import { classes } from "@utils/misc"; +import { showItemInFolder } from "@utils/native"; import { useAwaiter } from "@utils/react"; -import { findLazy } from "@webpack"; -import { Button, Card, Forms, React, TextArea } from "@webpack/common"; +import { findByCodeLazy, findByPropsLazy, findLazy } from "@webpack"; +import { Button, Card, FluxDispatcher, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common"; +import { UserThemeHeader } from "main/themes"; +import type { ComponentType, Ref, SyntheticEvent } from "react"; +import { AddonCard } from "./AddonCard"; import { SettingsTab, wrapTab } from "./shared"; +type FileInput = ComponentType<{ + ref: Ref<HTMLInputElement>; + onChange: (e: SyntheticEvent<HTMLInputElement>) => void; + multiple?: boolean; + filters?: { name?: string; extensions: string[]; }[]; +}>; + +const InviteActions = findByPropsLazy("resolveInvite"); +const TrashIcon = findByCodeLazy("M5 6.99902V18.999C5 20.101 5.897 20.999"); +const FileInput: FileInput = findByCodeLazy("activateUploadDialogue="); const TextAreaProps = findLazy(m => typeof m.textarea === "string"); +const cl = classNameFactory("vc-settings-theme-"); + function Validator({ link }: { link: string; }) { const [res, err, pending] = useAwaiter(() => fetch(link).then(res => { if (res.status > 300) throw `${res.status} ${res.statusText}`; @@ -75,10 +94,191 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) { ); } +interface ThemeCardProps { + theme: UserThemeHeader; + enabled: boolean; + onChange: (enabled: boolean) => void; + onDelete: () => void; +} + +function ThemeCard({ theme, enabled, onChange, onDelete }: ThemeCardProps) { + 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}> + <TrashIcon /> + </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 +} + function ThemesTab() { - const settings = useSettings(["themeLinks"]); - const [themeText, setThemeText] = React.useState(settings.themeLinks.join("\n")); + const settings = useSettings(["themeLinks", "enabledThemes"]); + + const fileInputRef = useRef<HTMLInputElement>(null); + const [currentTab, setCurrentTab] = useState(ThemeTab.LOCAL); + const [themeText, setThemeText] = useState(settings.themeLinks.join("\n")); + const [userThemes, setUserThemes] = useState<UserThemeHeader[] | null>(null); + const [themeDir, , themeDirPending] = useAwaiter(VencordNative.themes.getThemesDir); + useEffect(() => { + refreshLocalThemes(); + }, []); + + async function refreshLocalThemes() { + const themes = await VencordNative.themes.getThemesList(); + setUserThemes(themes); + } + + // 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} + filters={[{ extensions: ["*.css"] }]} + /> + </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> + <Button + onClick={() => VencordNative.quickCss.openEditor()} + size={Button.Sizes.SMALL} + > + Edit QuickCSS + </Button> + </> + </Card> + + <div className={cl("grid")}> + {userThemes?.map(theme => ( + <ThemeCard + 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} + /> + ))} + </div> + </Forms.FormSection> + </> + ); + } + + // When the user leaves the online theme textbox, update the settings function onBlur() { settings.themeLinks = [...new Set( themeText @@ -89,51 +289,56 @@ function ThemesTab() { )]; } + 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> + </> + ); + } + return ( <SettingsTab title="Themes"> - <Card className="vc-settings-card vc-text-selectable"> - <Forms.FormTitle tag="h5">Paste links to .theme.css files here</Forms.FormTitle> - <Forms.FormText>One link per line</Forms.FormText> - <Forms.FormText><strong>Make sure to use the raw links or github.io links!</strong></Forms.FormText> - <Forms.FormDivider className={Margins.top8 + " " + Margins.bottom8} /> - <Forms.FormTitle tag="h5">Find Themes:</Forms.FormTitle> - <div style={{ marginBottom: ".5em" }}> - <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 "Source" somewhere below the Download button</Forms.FormText> - <Forms.FormText>In the GitHub repository of your theme, find X.theme.css, click on it, then click the "Raw" button</Forms.FormText> - <Forms.FormText> - If the theme has configuration that requires you to edit the file: - <ul> - <li>• Make a <Link href="https://github.com/signup">GitHub</Link> account</li> - <li>• Click the fork button on the top right</li> - <li>• Edit the file</li> - <li>• Use the link to your own repository instead</li> - <li>• Use the link to your own repository instead </li> - <li>OR</li> - <li>• Paste the contents of the edited theme file into the QuickCSS editor</li> - </ul> - <Forms.FormDivider className={Margins.top8 + " " + Margins.bottom16} /> - <Button - onClick={() => VencordNative.quickCss.openEditor()} - size={Button.Sizes.SMALL}> - Open QuickCSS File - </Button> - </Forms.FormText> - </Card> - <Forms.FormTitle tag="h5">Themes</Forms.FormTitle> - <TextArea - value={themeText} - onChange={setThemeText} - className={`${TextAreaProps.textarea} vc-settings-theme-links`} - placeholder="Theme Links" - spellCheck={false} - onBlur={onBlur} - /> - <Validators themeLinks={settings.themeLinks} /> + <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()} </SettingsTab> ); } |