feat(themes): Online Themes Redesign
This commit is contained in:
parent
18df66a4b4
commit
506aab14c9
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue