diff options
Diffstat (limited to 'src/components')
-rw-r--r-- | src/components/PluginSettings/index.tsx | 29 | ||||
-rw-r--r-- | src/components/PluginSettings/styles.css | 53 | ||||
-rw-r--r-- | src/components/VencordSettings/AddonCard.tsx | 77 | ||||
-rw-r--r-- | src/components/VencordSettings/ThemesTab.tsx | 299 | ||||
-rw-r--r-- | src/components/VencordSettings/addonCard.css | 63 | ||||
-rw-r--r-- | src/components/VencordSettings/settingsStyles.css | 3 | ||||
-rw-r--r-- | src/components/VencordSettings/shared.tsx | 1 | ||||
-rw-r--r-- | src/components/VencordSettings/themesStyles.css | 29 |
8 files changed, 437 insertions, 117 deletions
diff --git a/src/components/PluginSettings/index.tsx b/src/components/PluginSettings/index.tsx index 7749abd..12487c6 100644 --- a/src/components/PluginSettings/index.tsx +++ b/src/components/PluginSettings/index.tsx @@ -22,10 +22,8 @@ import * as DataStore from "@api/DataStore"; import { showNotice } from "@api/Notices"; import { Settings, useSettings } from "@api/Settings"; import { classNameFactory } from "@api/Styles"; -import { Flex } from "@components/Flex"; -import { Badge } from "@components/PluginSettings/components"; import PluginModal from "@components/PluginSettings/PluginModal"; -import { Switch } from "@components/Switch"; +import { AddonCard } from "@components/VencordSettings/AddonCard"; import { SettingsTab } from "@components/VencordSettings/shared"; import { ChangeList } from "@utils/ChangeList"; import { Logger } from "@utils/Logger"; @@ -152,24 +150,23 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe } return ( - <Flex className={cl("card", { "card-disabled": disabled })} flexDirection="column" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}> - <div className={cl("card-header")}> - <Text variant="text-md/bold" className={cl("name")}> - {plugin.name}{isNew && <Badge text="NEW" color="#ED4245" />} - </Text> + <AddonCard + name={plugin.name} + description={plugin.description} + isNew={isNew} + enabled={isEnabled()} + setEnabled={toggleEnabled} + disabled={disabled} + onMouseEnter={onMouseEnter} + onMouseLeave={onMouseLeave} + infoButton={ <button role="switch" onClick={() => openModal()} className={classes(ButtonClasses.button, cl("info-button"))}> {plugin.options ? <CogWheel /> : <InfoIcon width="24" height="24" />} </button> - <Switch - checked={isEnabled()} - onChange={toggleEnabled} - disabled={disabled} - /> - </div> - <Text className={cl("note")} variant="text-sm/normal">{plugin.description}</Text> - </Flex > + } + /> ); } diff --git a/src/components/PluginSettings/styles.css b/src/components/PluginSettings/styles.css index a756fa9..66b2a21 100644 --- a/src/components/PluginSettings/styles.css +++ b/src/components/PluginSettings/styles.css @@ -23,38 +23,6 @@ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); } -.vc-plugins-card { - background-color: var(--background-secondary-alt); - color: var(--interactive-active); - border-radius: 8px; - display: block; - height: 100%; - padding: 12px; - width: 100%; - transition: 0.1s ease-out; - transition-property: box-shadow, transform, background, opacity; -} - -.vc-plugins-card-disabled { - opacity: 0.6; -} - -.vc-plugins-card:hover { - background-color: var(--background-tertiary); - transform: translateY(-1px); - box-shadow: var(--elevation-high); -} - -.vc-plugins-card-header { - margin-top: auto; - display: flex; - width: 100%; - justify-content: flex-end; - height: 1.5rem; - align-items: center; - gap: 8px; -} - .vc-plugins-info-button { height: 24px; width: 24px; @@ -86,27 +54,6 @@ text-align: center; } -.vc-plugins-note { - height: 36px; - overflow: hidden; - text-overflow: ellipsis; - display: -webkit-box; - -webkit-line-clamp: 2; - line-clamp: 2; - -webkit-box-orient: vertical; - /* stylelint-disable-next-line property-no-unknown */ - box-orient: vertical; -} - -.vc-plugins-name { - display: flex; - width: 100%; - align-items: center; - flex-grow: 1; - gap: 8px; - cursor: "default"; -} - .vc-plugins-dep-name { margin: 0 auto; } diff --git a/src/components/VencordSettings/AddonCard.tsx b/src/components/VencordSettings/AddonCard.tsx new file mode 100644 index 0000000..c4c3aac --- /dev/null +++ b/src/components/VencordSettings/AddonCard.tsx @@ -0,0 +1,77 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 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/>. +*/ + +import "./addonCard.css"; + +import { classNameFactory } from "@api/Styles"; +import { Badge } from "@components/Badge"; +import { Switch } from "@components/Switch"; +import { Text } from "@webpack/common"; +import type { MouseEventHandler, ReactNode } from "react"; + +const cl = classNameFactory("vc-addon-"); + +interface Props { + name: ReactNode; + description: ReactNode; + enabled: boolean; + setEnabled: (enabled: boolean) => void; + disabled?: boolean; + isNew?: boolean; + onMouseEnter?: MouseEventHandler<HTMLDivElement>; + onMouseLeave?: MouseEventHandler<HTMLDivElement>; + + infoButton?: ReactNode; + footer?: ReactNode; + author?: ReactNode; +} + +export function AddonCard({ disabled, isNew, name, infoButton, footer, author, enabled, setEnabled, description, onMouseEnter, onMouseLeave }: Props) { + return ( + <div + className={cl("card", { "card-disabled": disabled })} + onMouseEnter={onMouseEnter} + onMouseLeave={onMouseLeave} + > + <div className={cl("header")}> + <div className={cl("name-author")}> + <Text variant="text-md/bold" className={cl("name")}> + {name}{isNew && <Badge text="NEW" color="#ED4245" />} + </Text> + {!!author && ( + <Text variant="text-md/normal" className={cl("author")}> + {author} + </Text> + )} + </div> + + {infoButton} + + <Switch + checked={enabled} + onChange={setEnabled} + disabled={disabled} + /> + </div> + + <Text className={cl("note")} variant="text-sm/normal">{description}</Text> + + {footer} + </div> + ); +} 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> ); } diff --git a/src/components/VencordSettings/addonCard.css b/src/components/VencordSettings/addonCard.css new file mode 100644 index 0000000..92f8c25 --- /dev/null +++ b/src/components/VencordSettings/addonCard.css @@ -0,0 +1,63 @@ +.vc-addon-card { + background-color: var(--background-secondary-alt); + color: var(--interactive-active); + border-radius: 8px; + display: block; + height: 100%; + padding: 12px; + width: 100%; + transition: 0.1s ease-out; + transition-property: box-shadow, transform, background, opacity; +} + +.vc-addon-card-disabled { + opacity: 0.6; +} + +.vc-addon-card:hover { + background-color: var(--background-tertiary); + transform: translateY(-1px); + box-shadow: var(--elevation-high); +} + +.vc-addon-header { + margin-top: auto; + display: flex; + width: 100%; + justify-content: flex-end; + align-items: center; + gap: 8px; + margin-bottom: 0.5em; +} + +.vc-addon-note { + height: 36px; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + line-clamp: 2; + -webkit-box-orient: vertical; + /* stylelint-disable-next-line property-no-unknown */ + box-orient: vertical; +} + +.vc-addon-name-author { + width: 100%; +} + +.vc-addon-name { + display: flex; + width: 100%; + align-items: center; + flex-grow: 1; + gap: 8px; +} + +.vc-addon-author { + font-size: 0.8em; +} + +.vc-addon-author::before { + content: "by "; +} diff --git a/src/components/VencordSettings/settingsStyles.css b/src/components/VencordSettings/settingsStyles.css index f8502c5..01cbcd5 100644 --- a/src/components/VencordSettings/settingsStyles.css +++ b/src/components/VencordSettings/settingsStyles.css @@ -1,6 +1,6 @@ .vc-settings-tab-bar { margin-top: 20px; - margin-bottom: -2px; + margin-bottom: 10px; border-bottom: 2px solid var(--background-modifier-accent); } @@ -43,6 +43,7 @@ color: var(--text-normal) !important; padding: 0.5em; border: 1px solid var(--background-modifier-accent); + max-height: unset; } .vc-cloud-settings-sync-grid { diff --git a/src/components/VencordSettings/shared.tsx b/src/components/VencordSettings/shared.tsx index 0d3910d..6dd34c4 100644 --- a/src/components/VencordSettings/shared.tsx +++ b/src/components/VencordSettings/shared.tsx @@ -17,6 +17,7 @@ */ import "./settingsStyles.css"; +import "./themesStyles.css"; import ErrorBoundary from "@components/ErrorBoundary"; import { handleComponentFailed } from "@components/handleComponentFailed"; diff --git a/src/components/VencordSettings/themesStyles.css b/src/components/VencordSettings/themesStyles.css new file mode 100644 index 0000000..6038274 --- /dev/null +++ b/src/components/VencordSettings/themesStyles.css @@ -0,0 +1,29 @@ +.vc-settings-theme-grid { + display: grid; + grid-gap: 16px; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); +} + +.vc-settings-theme-card { + display: flex; + flex-direction: column; + background-color: var(--background-secondary-alt); + color: var(--interactive-active); + border-radius: 8px; + padding: 1em; + width: 100%; + transition: 0.1s ease-out; + transition-property: box-shadow, transform, background, opacity; +} + +.vc-settings-theme-card-text { + text-overflow: ellipsis; + height: 1.2em; + margin-bottom: 2px; + white-space: nowrap; + overflow: hidden; +} + +.vc-settings-theme-author::before { + content: "by "; +} |