mirror of
https://github.com/Equicord/Equicord.git
synced 2025-02-20 15:18:50 -05:00
Updates
This commit is contained in:
parent
40a53e0da9
commit
2960e9a74a
21 changed files with 414 additions and 686 deletions
|
@ -21,7 +21,7 @@ An enhanced version of [Vencord](https://github.com/Vendicated/Vencord) by [Vend
|
|||
- Request for plugins from Discord.
|
||||
|
||||
<details>
|
||||
<summary>Extra included plugins (62 additional plugins)</summary>
|
||||
<summary>Extra included plugins (61 additional plugins)</summary>
|
||||
|
||||
- AllCallTimers by MaxHerbold and D3SOX
|
||||
- AltKrispSwitch by newwares
|
||||
|
@ -35,7 +35,6 @@ An enhanced version of [Vencord](https://github.com/Vendicated/Vencord) by [Vend
|
|||
- ColorMessage by Kyuuhachi
|
||||
- CopyUserMention by Cortex and castdrian
|
||||
- CustomAppIcons by Happy Enderman and SerStars
|
||||
- CustomScreenShare by KawaiianPizza
|
||||
- DNDWhilePlaying by thororen
|
||||
- DoNotLeak by Perny
|
||||
- DoubleCounterBypass by nyx
|
||||
|
|
|
@ -34,8 +34,8 @@ export interface Settings {
|
|||
useQuickCss: boolean;
|
||||
enableReactDevtools: boolean;
|
||||
themeLinks: string[];
|
||||
disabledThemeLinks: string[];
|
||||
enabledThemes: string[];
|
||||
enabledThemeLinks: string[];
|
||||
frameless: boolean;
|
||||
transparent: boolean;
|
||||
winCtrlQ: boolean;
|
||||
|
@ -88,8 +88,8 @@ const DefaultSettings: Settings = {
|
|||
autoUpdateNotification: true,
|
||||
useQuickCss: true,
|
||||
themeLinks: [],
|
||||
disabledThemeLinks: [],
|
||||
enabledThemes: [],
|
||||
enabledThemeLinks: [],
|
||||
enableReactDevtools: false,
|
||||
frameless: false,
|
||||
transparent: false,
|
||||
|
|
|
@ -309,3 +309,40 @@ export function NoEntrySignIcon(props: IconProps) {
|
|||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
export function PasteIcon(props: IconProps) {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
className={classes(props.className, "vc-paste-icon")}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect width="8" height="4" x="8" y="2" rx="1" ry="1" />
|
||||
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
export function ResetIcon(props: IconProps) {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
|
||||
<path d="M3 3v5h5" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,258 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { useSettings } from "@api/Settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { CopyIcon, DeleteIcon } from "@components/Icons";
|
||||
import { copyWithToast } from "@utils/misc";
|
||||
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModalLazy } from "@utils/modal";
|
||||
import { getThemeInfo, UserThemeHeader } from "@utils/themes/bd";
|
||||
import { Button, Card, Forms, Text, TextInput, useEffect, useState } from "@webpack/common";
|
||||
|
||||
import { AddonCard } from "../VencordSettings/AddonCard";
|
||||
import { ThemeCard } from "./ThemesTab";
|
||||
|
||||
export interface OnlineTheme {
|
||||
link: string;
|
||||
headers?: UserThemeHeader;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const cl = classNameFactory("vc-settings-theme-");
|
||||
|
||||
/**
|
||||
* Trims URLs like so
|
||||
* https://whatever.example.com/whatever/whatever/whatever/whatever/index.html
|
||||
* -> whatever/index.html
|
||||
*/
|
||||
function trimThemeUrl(url: string) {
|
||||
let urlObj: URL;
|
||||
|
||||
try {
|
||||
urlObj = new URL(url);
|
||||
} catch (e) {
|
||||
return url;
|
||||
}
|
||||
|
||||
return urlObj.pathname.split("/").slice(-2).join("/");
|
||||
}
|
||||
|
||||
async function FetchTheme(link: string) {
|
||||
const theme: OnlineTheme = await fetch(link, { redirect: "follow" })
|
||||
.then(res => {
|
||||
if (res.status >= 400) 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 res.text();
|
||||
})
|
||||
.then(text => {
|
||||
const headers = getThemeInfo(text, trimThemeUrl(link));
|
||||
return { link, headers };
|
||||
}).catch(e => {
|
||||
return { link, error: e.toString() };
|
||||
});
|
||||
|
||||
return theme;
|
||||
}
|
||||
|
||||
export function OnlineThemes() {
|
||||
const settings = useSettings(["themeLinks", "disabledThemeLinks"]);
|
||||
const [themes, setThemes] = useState<OnlineTheme[]>([]);
|
||||
|
||||
async function fetchThemes() {
|
||||
const themes = await Promise.all(settings.themeLinks.map(link => FetchTheme(link)));
|
||||
setThemes(themes);
|
||||
}
|
||||
|
||||
async function addTheme(link: string) {
|
||||
settings.disabledThemeLinks = [...settings.disabledThemeLinks, link];
|
||||
settings.themeLinks = [...settings.themeLinks, link];
|
||||
setThemes([...themes, await FetchTheme(link)]);
|
||||
}
|
||||
|
||||
async function removeTheme(link: string) {
|
||||
settings.disabledThemeLinks = settings.disabledThemeLinks.filter(l => l !== link);
|
||||
settings.themeLinks = settings.themeLinks.filter(l => l !== link);
|
||||
setThemes(themes.filter(t => t.link !== link));
|
||||
}
|
||||
|
||||
function setThemeEnabled(link: string, enabled: boolean) {
|
||||
if (enabled) {
|
||||
settings.disabledThemeLinks = settings.disabledThemeLinks.filter(l => l !== link);
|
||||
} else {
|
||||
settings.disabledThemeLinks = [...settings.disabledThemeLinks, link];
|
||||
}
|
||||
settings.themeLinks = [...settings.themeLinks];
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchThemes();
|
||||
}, []);
|
||||
|
||||
function AddThemeModal({ transitionState, onClose }: ModalProps) {
|
||||
const [disabled, setDisabled] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [url, setUrl] = useState("");
|
||||
|
||||
async function checkUrl() {
|
||||
if (!url) {
|
||||
setDisabled(true);
|
||||
setError(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (themes.some(t => t.link === url)) {
|
||||
setError("Theme already added");
|
||||
setDisabled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
let urlObj: URL | undefined = undefined;
|
||||
try {
|
||||
urlObj = new URL(url);
|
||||
} catch (e) {
|
||||
setDisabled(true);
|
||||
setError("Invalid URL");
|
||||
return;
|
||||
}
|
||||
|
||||
if (urlObj.hostname !== "raw.githubusercontent.com" && !urlObj.hostname.endsWith("github.io")) {
|
||||
setError("Only raw.githubusercontent.com and github.io URLs are allowed, otherwise the theme will not work");
|
||||
setDisabled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!urlObj.pathname.endsWith(".css")) {
|
||||
setError("Not a CSS file. Remember to use the raw link!");
|
||||
setDisabled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
let success = true;
|
||||
await fetch(url).then(res => {
|
||||
if (!res.ok) {
|
||||
setError(`Could not fetch theme: ${res.status} ${res.statusText}`);
|
||||
setDisabled(true);
|
||||
success = false;
|
||||
}
|
||||
}).catch(e => {
|
||||
setError(`Could not fetch theme: ${e}`);
|
||||
setDisabled(true);
|
||||
success = false;
|
||||
});
|
||||
|
||||
if (!success) return;
|
||||
|
||||
setDisabled(false);
|
||||
setError(null);
|
||||
}
|
||||
|
||||
return <ModalRoot transitionState={transitionState} size={ModalSize.SMALL}>
|
||||
<ModalHeader>
|
||||
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>Add Online Theme</Text>
|
||||
<ModalCloseButton onClick={onClose} />
|
||||
</ModalHeader>
|
||||
<ModalContent className={cl("add-theme-content")}>
|
||||
<Forms.FormText>Only raw.githubusercontent.com and github.io URLs will work</Forms.FormText>
|
||||
|
||||
<TextInput placeholder="URL" name="url" onBlur={checkUrl} onChange={setUrl} />
|
||||
<Forms.FormText className={cl("add-theme-error")}>{error}</Forms.FormText>
|
||||
</ModalContent>
|
||||
<ModalFooter className={cl("add-theme-footer")}>
|
||||
<Button onClick={onClose} color={Button.Colors.RED}>Cancel</Button>
|
||||
<Button onClick={() => {
|
||||
addTheme(url);
|
||||
onClose();
|
||||
}} color={Button.Colors.BRAND} disabled={disabled}>
|
||||
Add
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalRoot>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="vc-settings-card">
|
||||
<Forms.FormTitle tag="h5">Find Themes:</Forms.FormTitle>
|
||||
<Forms.FormText>
|
||||
Find a theme you like and press "Add Theme" to add it.
|
||||
To get a raw link to a theme, go to it's GitHub repository,
|
||||
find the CSS file, and press the "Raw" button, then copy the URL.
|
||||
</Forms.FormText>
|
||||
</Card>
|
||||
<Forms.FormSection title="Online Themes" tag="h5">
|
||||
<Card className="vc-settings-quick-actions-card">
|
||||
<Button onClick={() => openModalLazy(async () => {
|
||||
return modalProps => {
|
||||
return <AddThemeModal {...modalProps} />;
|
||||
};
|
||||
})} size={Button.Sizes.SMALL}>
|
||||
Add Theme
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => fetchThemes()}
|
||||
size={Button.Sizes.SMALL}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</Card>
|
||||
<div className={cl("grid")}>
|
||||
{themes.length === 0 && (
|
||||
<Forms.FormText>Add themes with the "Add Theme" button above</Forms.FormText>
|
||||
)}
|
||||
{themes.map(theme => (
|
||||
(!theme.error && <ThemeCard
|
||||
key={theme.link}
|
||||
enabled={!settings.disabledThemeLinks.includes(theme.link)}
|
||||
onChange={value => {
|
||||
setThemeEnabled(theme.link, value);
|
||||
}}
|
||||
onDelete={() => removeTheme(theme.link)}
|
||||
theme={theme.headers!}
|
||||
showDelete={true}
|
||||
extraButtons={
|
||||
<div
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => copyWithToast(theme.link, "Link copied to clipboard!")}
|
||||
>
|
||||
<CopyIcon />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
) || (
|
||||
<AddonCard
|
||||
key={theme.link}
|
||||
name={theme.error}
|
||||
description={theme.link}
|
||||
enabled={false}
|
||||
setEnabled={() => { }}
|
||||
hideSwitch={true}
|
||||
infoButton={<>
|
||||
<div
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => copyWithToast(theme.link, "Link copied to clipboard!")}
|
||||
>
|
||||
<CopyIcon />
|
||||
</div>
|
||||
<div
|
||||
style={{ cursor: "pointer", color: "var(--status-danger" }}
|
||||
onClick={() => removeTheme(theme.link)}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
</Forms.FormSection>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,8 +1,20 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
import "./themesStyles.css";
|
||||
|
||||
|
@ -18,16 +30,15 @@ import { openInviteModal } from "@utils/discord";
|
|||
import { openModal } from "@utils/modal";
|
||||
import { showItemInFolder } from "@utils/native";
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import type { ThemeHeader } from "@utils/themes";
|
||||
import { ThemeHeader } from "@utils/themes";
|
||||
import { getThemeInfo, stripBOM, type UserThemeHeader } from "@utils/themes/bd";
|
||||
import { usercssParse } from "@utils/themes/usercss";
|
||||
import { findLazy } from "@webpack";
|
||||
import { Button, Card, Forms, React, showToast, TabBar, Tooltip, useEffect, useMemo, useRef, useState } from "@webpack/common";
|
||||
import { findByPropsLazy, findLazy } from "@webpack";
|
||||
import { Button, Card, Forms, React, showToast, TabBar, TextInput, Tooltip, useEffect, useMemo, useRef, useState } from "@webpack/common";
|
||||
import type { ComponentType, Ref, SyntheticEvent } from "react";
|
||||
import type { UserstyleHeader } from "usercss-meta";
|
||||
import { UserstyleHeader } from "usercss-meta";
|
||||
|
||||
import { isPluginEnabled } from "../../plugins";
|
||||
import { OnlineThemes } from "./OnlineThemes";
|
||||
import { UserCSSSettingsModal } from "./UserCSSModal";
|
||||
|
||||
type FileInput = ComponentType<{
|
||||
|
@ -37,16 +48,34 @@ type FileInput = ComponentType<{
|
|||
filters?: { name?: string; extensions: string[]; }[];
|
||||
}>;
|
||||
|
||||
const InviteActions = findByPropsLazy("resolveInvite");
|
||||
const FileInput: FileInput = findLazy(m => m.prototype?.activateUploadDialogue && m.prototype.setRef);
|
||||
const TextAreaProps = findLazy(m => typeof m.textarea === "string");
|
||||
|
||||
const cl = classNameFactory("vc-settings-theme-");
|
||||
|
||||
interface ThemeCardProps {
|
||||
theme: UserThemeHeader;
|
||||
enabled: boolean;
|
||||
onChange: (enabled: boolean) => void;
|
||||
onDelete: () => void;
|
||||
showDelete?: boolean;
|
||||
extraButtons?: React.ReactNode;
|
||||
function Validator({ link, onValidate }: { link: string; onValidate: (valid: boolean) => void; }) {
|
||||
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")) {
|
||||
onValidate(false);
|
||||
throw "Not a CSS file. Remember to use the raw link!";
|
||||
}
|
||||
|
||||
onValidate(true);
|
||||
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>;
|
||||
}
|
||||
|
||||
interface OtherThemeCardProps {
|
||||
|
@ -54,8 +83,7 @@ interface OtherThemeCardProps {
|
|||
enabled: boolean;
|
||||
onChange: (enabled: boolean) => void;
|
||||
onDelete: () => void;
|
||||
showDelete?: boolean;
|
||||
extraButtons?: React.ReactNode;
|
||||
showDeleteButton?: boolean;
|
||||
}
|
||||
|
||||
interface UserCSSCardProps {
|
||||
|
@ -63,50 +91,10 @@ interface UserCSSCardProps {
|
|||
enabled: boolean;
|
||||
onChange: (enabled: boolean) => void;
|
||||
onDelete: () => void;
|
||||
onSettingsReset: () => void;
|
||||
}
|
||||
|
||||
export function ThemeCard({ theme, enabled, onChange, onDelete, showDelete, extraButtons }: ThemeCardProps) {
|
||||
return (
|
||||
<AddonCard
|
||||
name={theme.name}
|
||||
description={theme.description}
|
||||
author={theme.author}
|
||||
enabled={enabled}
|
||||
setEnabled={onChange}
|
||||
infoButton={
|
||||
(IS_WEB || showDelete) && (<>
|
||||
{extraButtons}
|
||||
<div
|
||||
style={{ cursor: "pointer", color: "var(--status-danger" }}
|
||||
onClick={onDelete}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</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();
|
||||
theme.invite != null && openInviteModal(theme.invite).catch(() => showToast("Invalid or expired invite"));
|
||||
}}
|
||||
>
|
||||
Discord Server
|
||||
</Link>
|
||||
)}
|
||||
</Flex>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function UserCSSThemeCard({ theme, enabled, onChange, onDelete }: UserCSSCardProps) {
|
||||
function UserCSSThemeCard({ theme, enabled, onChange, onDelete, onSettingsReset }: UserCSSCardProps) {
|
||||
const missingPlugins = useMemo(() =>
|
||||
theme.requiredPlugins?.filter(p => !isPluginEnabled(p)), [theme]);
|
||||
|
||||
|
@ -117,13 +105,14 @@ function UserCSSThemeCard({ theme, enabled, onChange, onDelete }: UserCSSCardPro
|
|||
author={theme.author ?? "Unknown"}
|
||||
enabled={enabled}
|
||||
setEnabled={onChange}
|
||||
disabled={missingPlugins && missingPlugins.length > 0}
|
||||
infoButton={
|
||||
<>
|
||||
{missingPlugins && missingPlugins.length > 0 && (
|
||||
<Tooltip text={"The following plugins are required, but aren't enabled: " + missingPlugins.join(", ")}>
|
||||
{({ onMouseLeave, onMouseEnter }) => (
|
||||
<div
|
||||
style={{ color: "var(--status-warning" }}
|
||||
style={{ color: "var(--status-danger" }}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
|
@ -135,7 +124,7 @@ function UserCSSThemeCard({ theme, enabled, onChange, onDelete }: UserCSSCardPro
|
|||
{theme.vars && (
|
||||
<div style={{ cursor: "pointer" }} onClick={
|
||||
() => openModal(modalProps =>
|
||||
<UserCSSSettingsModal modalProps={modalProps} theme={theme} />)
|
||||
<UserCSSSettingsModal modalProps={modalProps} theme={theme} onSettingsReset={onSettingsReset} />)
|
||||
}>
|
||||
<CogWheel />
|
||||
</div>
|
||||
|
@ -158,7 +147,7 @@ function UserCSSThemeCard({ theme, enabled, onChange, onDelete }: UserCSSCardPro
|
|||
);
|
||||
}
|
||||
|
||||
function OtherThemeCard({ theme, enabled, onChange, onDelete, showDelete, extraButtons }: OtherThemeCardProps) {
|
||||
function OtherThemeCard({ theme, enabled, onChange, onDelete, showDeleteButton }: OtherThemeCardProps) {
|
||||
return (
|
||||
<AddonCard
|
||||
name={theme.name}
|
||||
|
@ -167,15 +156,10 @@ function OtherThemeCard({ theme, enabled, onChange, onDelete, showDelete, extraB
|
|||
enabled={enabled}
|
||||
setEnabled={onChange}
|
||||
infoButton={
|
||||
(IS_WEB || showDelete) && (<>
|
||||
{extraButtons}
|
||||
<div
|
||||
style={{ cursor: "pointer", color: "var(--status-danger" }}
|
||||
onClick={onDelete}
|
||||
>
|
||||
(IS_WEB || showDeleteButton) && (
|
||||
<div style={{ cursor: "pointer", color: "var(--status-danger" }} onClick={onDelete}>
|
||||
<DeleteIcon />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
footer={
|
||||
|
@ -201,25 +185,27 @@ function OtherThemeCard({ theme, enabled, onChange, onDelete, showDelete, extraB
|
|||
|
||||
enum ThemeTab {
|
||||
LOCAL,
|
||||
ONLINE,
|
||||
REPO
|
||||
ONLINE
|
||||
}
|
||||
|
||||
function ThemesTab() {
|
||||
const settings = useSettings(["themeLinks", "disabledThemeLinks", "enabledThemes"]);
|
||||
const settings = useSettings(["themeLinks", "enabledThemeLinks", "enabledThemes"]);
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [currentTab, setCurrentTab] = useState(ThemeTab.LOCAL);
|
||||
const [currentThemeLink, setCurrentThemeLink] = useState("");
|
||||
const [themeLinkValid, setThemeLinkValid] = useState(false);
|
||||
const [userThemes, setUserThemes] = useState<ThemeHeader[] | null>(null);
|
||||
const [onlineThemes, setOnlineThemes] = useState<(UserThemeHeader & { link: string; })[] | null>(null);
|
||||
const [themeDir, , themeDirPending] = useAwaiter(VencordNative.themes.getThemesDir);
|
||||
|
||||
useEffect(() => {
|
||||
refreshLocalThemes();
|
||||
}, [settings.themeLinks]);
|
||||
refreshOnlineThemes();
|
||||
}, []);
|
||||
|
||||
async function refreshLocalThemes() {
|
||||
const themes = await VencordNative.themes.getThemesList();
|
||||
|
||||
const themeInfo: ThemeHeader[] = [];
|
||||
|
||||
for (const { fileName, content } of themes) {
|
||||
|
@ -242,14 +228,12 @@ function ThemesTab() {
|
|||
switch (varInfo.type) {
|
||||
case "text":
|
||||
case "color":
|
||||
case "checkbox":
|
||||
normalizedValue = varInfo.default;
|
||||
break;
|
||||
case "select":
|
||||
normalizedValue = varInfo.options.find(v => v.name === varInfo.default)!.value;
|
||||
break;
|
||||
case "checkbox":
|
||||
normalizedValue = varInfo.default ? "1" : "0";
|
||||
break;
|
||||
case "range":
|
||||
normalizedValue = `${varInfo.default}${varInfo.units}`;
|
||||
break;
|
||||
|
@ -295,8 +279,7 @@ function ThemesTab() {
|
|||
return new Promise<void>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
VencordNative.themes
|
||||
.uploadTheme(name, reader.result as string)
|
||||
VencordNative.themes.uploadTheme(name, reader.result as string)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
};
|
||||
|
@ -308,7 +291,7 @@ function ThemesTab() {
|
|||
refreshLocalThemes();
|
||||
}
|
||||
|
||||
function renderLocalThemes() {
|
||||
function LocalThemes() {
|
||||
return (
|
||||
<>
|
||||
<Card className="vc-settings-card">
|
||||
|
@ -319,38 +302,45 @@ function ThemesTab() {
|
|||
</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>
|
||||
<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}>
|
||||
{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}>
|
||||
<Button
|
||||
onClick={() => VencordNative.quickCss.openEditor()}
|
||||
size={Button.Sizes.SMALL}
|
||||
>
|
||||
Edit QuickCSS
|
||||
</Button>
|
||||
|
||||
|
@ -395,6 +385,7 @@ function ThemesTab() {
|
|||
await VencordNative.themes.deleteTheme(theme.fileName);
|
||||
refreshLocalThemes();
|
||||
}}
|
||||
onSettingsReset={refreshLocalThemes}
|
||||
theme={theme as UserstyleHeader}
|
||||
/>
|
||||
)))}
|
||||
|
@ -404,6 +395,68 @@ function ThemesTab() {
|
|||
);
|
||||
}
|
||||
|
||||
function addThemeLink(link: string) {
|
||||
if (!themeLinkValid) return;
|
||||
if (settings.themeLinks.includes(link)) return;
|
||||
|
||||
settings.themeLinks = [...settings.themeLinks, link];
|
||||
setCurrentThemeLink("");
|
||||
refreshOnlineThemes();
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Forms.FormSection title="Online Themes" tag="h5">
|
||||
<Card className="vc-settings-theme-add-card">
|
||||
<Forms.FormText>Make sure to use direct links to files (raw or github.io)!</Forms.FormText>
|
||||
<Flex flexDirection="row">
|
||||
<TextInput placeholder="Theme Link" className="vc-settings-theme-link-input" value={currentThemeLink} onChange={setCurrentThemeLink} />
|
||||
<Button onClick={() => addThemeLink(currentThemeLink)} disabled={!themeLinkValid}>Add</Button>
|
||||
</Flex>
|
||||
{currentThemeLink && <Validator link={currentThemeLink} onValidate={setThemeLinkValid} />}
|
||||
</Card>
|
||||
|
||||
<div className={cl("grid")}>
|
||||
{onlineThemes?.map(theme => {
|
||||
return <OtherThemeCard
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsTab title="Themes">
|
||||
<TabBar
|
||||
|
@ -413,15 +466,21 @@ function ThemesTab() {
|
|||
selectedItem={currentTab}
|
||||
onItemSelect={setCurrentTab}
|
||||
>
|
||||
<TabBar.Item className="vc-settings-tab-bar-item" id={ThemeTab.LOCAL}>
|
||||
<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}>
|
||||
<TabBar.Item
|
||||
className="vc-settings-tab-bar-item"
|
||||
id={ThemeTab.ONLINE}
|
||||
>
|
||||
Online Themes
|
||||
</TabBar.Item>
|
||||
</TabBar>
|
||||
|
||||
{currentTab === ThemeTab.LOCAL && renderLocalThemes()}
|
||||
{currentTab === ThemeTab.LOCAL && <LocalThemes />}
|
||||
{currentTab === ThemeTab.ONLINE && <OnlineThemes />}
|
||||
</SettingsTab>
|
||||
);
|
||||
|
|
|
@ -4,11 +4,13 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { useSettings } from "@api/Settings";
|
||||
import { Settings, useSettings } from "@api/Settings";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { CopyIcon, PasteIcon, ResetIcon } from "@components/Icons";
|
||||
import { copyWithToast } from "@utils/misc";
|
||||
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot } from "@utils/modal";
|
||||
import { Text } from "@webpack/common";
|
||||
import type { ReactNode } from "react";
|
||||
import { showToast, Text, Toasts, Tooltip } from "@webpack/common";
|
||||
import { type ReactNode } from "react";
|
||||
import { UserstyleHeader } from "usercss-meta";
|
||||
|
||||
import { SettingBooleanComponent, SettingColorComponent, SettingNumberComponent, SettingRangeComponent, SettingSelectComponent, SettingTextComponent } from "./components";
|
||||
|
@ -16,11 +18,93 @@ import { SettingBooleanComponent, SettingColorComponent, SettingNumberComponent,
|
|||
interface UserCSSSettingsModalProps {
|
||||
modalProps: ModalProps;
|
||||
theme: UserstyleHeader;
|
||||
onSettingsReset: () => void;
|
||||
}
|
||||
|
||||
export function UserCSSSettingsModal({ modalProps, theme }: UserCSSSettingsModalProps) {
|
||||
function ExportButton({ themeSettings }: { themeSettings: Settings["userCssVars"][""]; }) {
|
||||
return <Tooltip text={"Copy theme settings"}>
|
||||
{({ onMouseLeave, onMouseEnter }) => (
|
||||
<div
|
||||
style={{ cursor: "pointer" }}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
|
||||
onClick={() => {
|
||||
copyWithToast(JSON.stringify(themeSettings), "Copied theme settings to clipboard.");
|
||||
}}>
|
||||
<CopyIcon />
|
||||
</div>
|
||||
)}
|
||||
</Tooltip>;
|
||||
}
|
||||
|
||||
function ImportButton({ themeSettings }: { themeSettings: Settings["userCssVars"][""]; }) {
|
||||
return <Tooltip text={"Paste theme settings"}>
|
||||
{({ onMouseLeave, onMouseEnter }) => (
|
||||
<div
|
||||
style={{ cursor: "pointer" }}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
|
||||
onClick={async () => {
|
||||
const clip = (await navigator.clipboard.read())[0];
|
||||
|
||||
if (!clip) return showToast("Your clipboard is empty.", Toasts.Type.FAILURE);
|
||||
|
||||
if (!clip.types.includes("text/plain"))
|
||||
return showToast("Your clipboard doesn't have valid settings data.", Toasts.Type.FAILURE);
|
||||
|
||||
try {
|
||||
var potentialSettings: Record<string, string> =
|
||||
JSON.parse(await clip.getType("text/plain").then(b => b.text()));
|
||||
} catch (e) {
|
||||
return showToast("Your clipboard doesn't have valid settings data.", Toasts.Type.FAILURE);
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(potentialSettings)) {
|
||||
if (Object.prototype.hasOwnProperty.call(themeSettings, key))
|
||||
themeSettings[key] = value;
|
||||
}
|
||||
|
||||
showToast("Pasted theme settings from clipboard.", Toasts.Type.SUCCESS);
|
||||
}}>
|
||||
<PasteIcon />
|
||||
</div>
|
||||
)}
|
||||
</Tooltip>;
|
||||
}
|
||||
|
||||
interface ResetButtonProps {
|
||||
themeSettings: Settings["userCssVars"];
|
||||
themeId: string;
|
||||
close: () => void;
|
||||
onReset: () => void;
|
||||
}
|
||||
|
||||
function ResetButton({ themeSettings, themeId, close, onReset }: ResetButtonProps) {
|
||||
return <Tooltip text={"Reset settings to default"}>
|
||||
{({ onMouseLeave, onMouseEnter }) => (
|
||||
<div
|
||||
style={{ cursor: "pointer" }}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
|
||||
onClick={async () => {
|
||||
await close(); // close the modal first to stop rendering
|
||||
delete themeSettings[themeId];
|
||||
onReset();
|
||||
}}>
|
||||
<ResetIcon />
|
||||
</div>
|
||||
)}
|
||||
</Tooltip>;
|
||||
}
|
||||
|
||||
export function UserCSSSettingsModal({ modalProps, theme, onSettingsReset }: UserCSSSettingsModalProps) {
|
||||
// @ts-expect-error UseSettings<> can't determine this is a valid key
|
||||
const themeSettings = useSettings(["userCssVars"], false).userCssVars[theme.id];
|
||||
const { userCssVars } = useSettings(["userCssVars"], false);
|
||||
|
||||
const themeVars = userCssVars[theme.id];
|
||||
|
||||
const controls: ReactNode[] = [];
|
||||
|
||||
|
@ -31,7 +115,7 @@ export function UserCSSSettingsModal({ modalProps, theme }: UserCSSSettingsModal
|
|||
<SettingTextComponent
|
||||
label={varInfo.label}
|
||||
name={name}
|
||||
themeSettings={themeSettings}
|
||||
themeSettings={themeVars}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
@ -42,7 +126,7 @@ export function UserCSSSettingsModal({ modalProps, theme }: UserCSSSettingsModal
|
|||
<SettingBooleanComponent
|
||||
label={varInfo.label}
|
||||
name={name}
|
||||
themeSettings={themeSettings}
|
||||
themeSettings={themeVars}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
@ -53,7 +137,7 @@ export function UserCSSSettingsModal({ modalProps, theme }: UserCSSSettingsModal
|
|||
<SettingColorComponent
|
||||
label={varInfo.label}
|
||||
name={name}
|
||||
themeSettings={themeSettings}
|
||||
themeSettings={themeVars}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
@ -64,7 +148,7 @@ export function UserCSSSettingsModal({ modalProps, theme }: UserCSSSettingsModal
|
|||
<SettingNumberComponent
|
||||
label={varInfo.label}
|
||||
name={name}
|
||||
themeSettings={themeSettings}
|
||||
themeSettings={themeVars}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
@ -77,7 +161,7 @@ export function UserCSSSettingsModal({ modalProps, theme }: UserCSSSettingsModal
|
|||
name={name}
|
||||
options={varInfo.options}
|
||||
default={varInfo.default}
|
||||
themeSettings={themeSettings}
|
||||
themeSettings={themeVars}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
@ -92,7 +176,7 @@ export function UserCSSSettingsModal({ modalProps, theme }: UserCSSSettingsModal
|
|||
min={varInfo.min}
|
||||
max={varInfo.max}
|
||||
step={varInfo.step}
|
||||
themeSettings={themeSettings}
|
||||
themeSettings={themeVars}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
@ -104,10 +188,17 @@ export function UserCSSSettingsModal({ modalProps, theme }: UserCSSSettingsModal
|
|||
<ModalRoot {...modalProps}>
|
||||
<ModalHeader separator={false}>
|
||||
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>Settings for {theme.name}</Text>
|
||||
<Flex style={{ gap: 4, marginRight: 4 }} className="vc-settings-usercss-ie-buttons">
|
||||
<ExportButton themeSettings={themeVars} />
|
||||
<ImportButton themeSettings={themeVars} />
|
||||
<ResetButton themeSettings={userCssVars} themeId={theme.id} close={modalProps.onClose} onReset={onSettingsReset} />
|
||||
</Flex>
|
||||
<ModalCloseButton onClick={modalProps.onClose} />
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<Flex flexDirection="column" style={{ gap: 12, marginBottom: 16 }}>{controls}</Flex>
|
||||
<Flex flexDirection="column" style={{ gap: 12, marginBottom: 16 }}>
|
||||
{controls}
|
||||
</Flex>
|
||||
</ModalContent>
|
||||
</ModalRoot>
|
||||
);
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Forms, Switch, useState } from "@webpack/common";
|
||||
import { Forms, Switch } from "@webpack/common";
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
|
@ -13,13 +13,9 @@ interface Props {
|
|||
}
|
||||
|
||||
export function SettingBooleanComponent({ label, name, themeSettings }: Props) {
|
||||
const [value, setValue] = useState(themeSettings[name]);
|
||||
|
||||
function handleChange(value: boolean) {
|
||||
const corrected = value ? "1" : "0";
|
||||
|
||||
setValue(corrected);
|
||||
|
||||
themeSettings[name] = corrected;
|
||||
}
|
||||
|
||||
|
@ -27,7 +23,7 @@ export function SettingBooleanComponent({ label, name, themeSettings }: Props) {
|
|||
<Forms.FormSection>
|
||||
<Switch
|
||||
key={name}
|
||||
value={value === "1"}
|
||||
value={themeSettings[name] === "1"}
|
||||
onChange={handleChange}
|
||||
hideBorder
|
||||
style={{ marginBottom: "0.5em" }}
|
||||
|
|
|
@ -8,7 +8,7 @@ import "./colorStyles.css";
|
|||
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { findByCodeLazy, findComponentByCodeLazy } from "@webpack";
|
||||
import { Forms, useMemo, useState } from "@webpack/common";
|
||||
import { Forms, useMemo } from "@webpack/common";
|
||||
|
||||
interface ColorPickerProps {
|
||||
color: number | null;
|
||||
|
@ -29,17 +29,13 @@ interface Props {
|
|||
}
|
||||
|
||||
export function SettingColorComponent({ label, name, themeSettings }: Props) {
|
||||
const [value, setValue] = useState(themeSettings[name]);
|
||||
|
||||
function handleChange(value: number) {
|
||||
const corrected = "#" + (value?.toString(16).padStart(6, "0") ?? "000000");
|
||||
|
||||
setValue(corrected);
|
||||
|
||||
themeSettings[name] = corrected;
|
||||
}
|
||||
|
||||
const normalizedValue = useMemo(() => parseInt(TinyColor(value).toHex(), 16), [value]);
|
||||
const normalizedValue = useMemo(() => parseInt(TinyColor(themeSettings[name]).toHex(), 16), [themeSettings[name]]);
|
||||
|
||||
return (
|
||||
<Forms.FormSection>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Forms, TextInput, useState } from "@webpack/common";
|
||||
import { Forms, TextInput } from "@webpack/common";
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
|
@ -13,11 +13,7 @@ interface Props {
|
|||
}
|
||||
|
||||
export function SettingNumberComponent({ label, name, themeSettings }: Props) {
|
||||
const [value, setValue] = useState(themeSettings[name]);
|
||||
|
||||
function handleChange(value: string) {
|
||||
setValue(value);
|
||||
|
||||
themeSettings[name] = value;
|
||||
}
|
||||
|
||||
|
@ -28,7 +24,7 @@ export function SettingNumberComponent({ label, name, themeSettings }: Props) {
|
|||
type="number"
|
||||
pattern="-?[0-9]+"
|
||||
key={name}
|
||||
value={value}
|
||||
value={themeSettings[name]}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Forms.FormSection>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Forms, Slider, useMemo, useState } from "@webpack/common";
|
||||
import { Forms, Slider, useMemo } from "@webpack/common";
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
|
@ -17,13 +17,9 @@ interface Props {
|
|||
}
|
||||
|
||||
export function SettingRangeComponent({ label, name, default: def, min, max, step, themeSettings }: Props) {
|
||||
const [value, setValue] = useState(themeSettings[name]);
|
||||
|
||||
function handleChange(value: number) {
|
||||
const corrected = value.toString();
|
||||
|
||||
setValue(corrected);
|
||||
|
||||
themeSettings[name] = corrected;
|
||||
}
|
||||
|
||||
|
@ -42,7 +38,7 @@ export function SettingRangeComponent({ label, name, default: def, min, max, ste
|
|||
<Forms.FormSection>
|
||||
<Forms.FormTitle tag="h5">{label}</Forms.FormTitle>
|
||||
<Slider
|
||||
initialValue={parseInt(value, 10)}
|
||||
initialValue={parseInt(themeSettings[name], 10)}
|
||||
defaultValue={def}
|
||||
onValueChange={handleChange}
|
||||
minValue={min}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { identity } from "@utils/misc";
|
||||
import { ComponentTypes, Forms, Select, useMemo, useState } from "@webpack/common";
|
||||
import { ComponentTypes, Forms, Select, useMemo } from "@webpack/common";
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
|
@ -20,11 +20,7 @@ interface Props {
|
|||
}
|
||||
|
||||
export function SettingSelectComponent({ label, name, options, default: def, themeSettings }: Props) {
|
||||
const [value, setValue] = useState(themeSettings[name]);
|
||||
|
||||
function handleChange(value: string) {
|
||||
setValue(value);
|
||||
|
||||
themeSettings[name] = value;
|
||||
}
|
||||
|
||||
|
@ -47,7 +43,7 @@ export function SettingSelectComponent({ label, name, options, default: def, the
|
|||
closeOnSelect={true}
|
||||
|
||||
select={handleChange}
|
||||
isSelected={v => v === value}
|
||||
isSelected={v => v === themeSettings[name]}
|
||||
serialize={identity}
|
||||
/>
|
||||
</Forms.FormSection>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Forms, TextInput, useState } from "@webpack/common";
|
||||
import { Forms, TextInput } from "@webpack/common";
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
|
@ -13,11 +13,7 @@ interface Props {
|
|||
}
|
||||
|
||||
export function SettingTextComponent({ label, name, themeSettings }: Props) {
|
||||
const [value, setValue] = useState(themeSettings[name]);
|
||||
|
||||
function handleChange(value: string) {
|
||||
setValue(value);
|
||||
|
||||
themeSettings[name] = value;
|
||||
}
|
||||
|
||||
|
@ -26,7 +22,7 @@ export function SettingTextComponent({ label, name, themeSettings }: Props) {
|
|||
<Forms.FormTitle tag="h5">{label}</Forms.FormTitle>
|
||||
<TextInput
|
||||
key={name}
|
||||
value={value}
|
||||
value={themeSettings[name]}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Forms.FormSection>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.vc-usercss-settings-color-swatch-row>span {
|
||||
.vc-usercss-settings-color-swatch-row > span {
|
||||
display: block;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
@ -16,4 +16,4 @@
|
|||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,3 +45,14 @@
|
|||
.vc-settings-theme-add-theme-error {
|
||||
color: var(--text-danger);
|
||||
}
|
||||
|
||||
.vc-settings-usercss-ie-buttons > div {
|
||||
color: var(--interactive-normal);
|
||||
opacity: .5;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.vc-settings-usercss-ie-buttons > div:hover {
|
||||
color: var(--interactive-hover);
|
||||
opacity: 1;
|
||||
}
|
||||
|
|
|
@ -37,12 +37,11 @@ interface Props {
|
|||
onMouseLeave?: MouseEventHandler<HTMLDivElement>;
|
||||
|
||||
infoButton?: ReactNode;
|
||||
hideSwitch?: boolean;
|
||||
footer?: ReactNode;
|
||||
author?: ReactNode;
|
||||
}
|
||||
|
||||
export function AddonCard({ disabled, isNew, name, infoButton, footer, author, enabled, setEnabled, description, onMouseEnter, onMouseLeave, hideSwitch }: Props) {
|
||||
export function AddonCard({ disabled, isNew, name, infoButton, footer, author, enabled, setEnabled, description, onMouseEnter, onMouseLeave }: Props) {
|
||||
const titleRef = useRef<HTMLDivElement>(null);
|
||||
const titleContainerRef = useRef<HTMLDivElement>(null);
|
||||
return (
|
||||
|
@ -79,11 +78,11 @@ export function AddonCard({ disabled, isNew, name, infoButton, footer, author, e
|
|||
|
||||
{infoButton}
|
||||
|
||||
{!hideSwitch && <Switch
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={setEnabled}
|
||||
disabled={disabled}
|
||||
/>}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Text className={cl("note")} variant="text-sm/normal">{description}</Text>
|
||||
|
|
|
@ -28,20 +28,11 @@
|
|||
content: "by ";
|
||||
}
|
||||
|
||||
.vc-settings-theme-add-theme-content {
|
||||
margin: 10px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
.vc-settings-theme-link-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.vc-settings-theme-add-theme-footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
.vc-settings-theme-add-card {
|
||||
padding: 1em;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.vc-settings-theme-add-theme-error {
|
||||
color: var(--text-danger);
|
||||
}
|
|
@ -1,201 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { EquicordDevs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { Forms, Menu, TextInput, useState } from "@webpack/common";
|
||||
|
||||
import { cooldown, denormalize, normalize } from "./utils";
|
||||
|
||||
const settings = definePluginSettings({
|
||||
maxFPS: {
|
||||
description: "Max FPS for the range slider",
|
||||
default: 144,
|
||||
type: OptionType.COMPONENT,
|
||||
component: (props: any) => {
|
||||
const [value, setValue] = useState(settings.store.maxFPS);
|
||||
return <Forms.FormSection>
|
||||
<Forms.FormTitle>Max FPS for the range slider</Forms.FormTitle>
|
||||
<TextInput type="number" pattern="-?[0-9]+" onChange={value => { props.setValue(Math.max(Number.parseInt(value), 1)); setValue(value); }} value={value} />
|
||||
</Forms.FormSection>;
|
||||
}
|
||||
},
|
||||
maxResolution: {
|
||||
description: "Max Resolution for the range slider",
|
||||
default: 1080,
|
||||
type: OptionType.COMPONENT,
|
||||
component: (props: any) => {
|
||||
const [value, setValue] = useState(settings.store.maxResolution);
|
||||
return <Forms.FormSection>
|
||||
<Forms.FormTitle>Max Resolution for the range slider</Forms.FormTitle>
|
||||
<TextInput type="number" pattern="-?[0-9]+" onChange={value => { props.setValue(Math.max(Number.parseInt(value), 22)); setValue(value); }} value={value} />
|
||||
</Forms.FormSection>;
|
||||
}
|
||||
},
|
||||
roundValues: {
|
||||
description: "Round Resolution and FPS values to the nearest whole number",
|
||||
default: true,
|
||||
type: OptionType.BOOLEAN,
|
||||
},
|
||||
bitrates: {
|
||||
description: "ADVANCED: ONLY USE FOR TESTING PURPOSES!",
|
||||
default: false,
|
||||
type: OptionType.BOOLEAN,
|
||||
restartNeeded: false,
|
||||
},
|
||||
targetBitrate: {
|
||||
description: "Target Bitrate (seemingly no effect)",
|
||||
default: 600000,
|
||||
type: OptionType.NUMBER,
|
||||
hidden: true
|
||||
},
|
||||
minBitrate: {
|
||||
description: "Minimum Bitrate (forces the bitrate to be at LEAST this)",
|
||||
default: 500000,
|
||||
type: OptionType.NUMBER,
|
||||
hidden: true
|
||||
},
|
||||
maxBitrate: {
|
||||
description: "Maxmimum Bitrate (seems to not be reached most of the time)",
|
||||
default: 8000000,
|
||||
type: OptionType.NUMBER,
|
||||
hidden: true
|
||||
},
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "CustomScreenShare",
|
||||
description: "Stream any resolution and any FPS!",
|
||||
authors: [EquicordDevs.KawaiianPizza],
|
||||
settingsAboutComponent: () => (
|
||||
<Forms.FormSection>
|
||||
<Forms.FormTitle tag="h3">Usage</Forms.FormTitle>
|
||||
<Forms.FormText>
|
||||
Adds a slider for the quality and fps options submenu
|
||||
</Forms.FormText>
|
||||
</Forms.FormSection>),
|
||||
settings,
|
||||
patches: [
|
||||
{
|
||||
find: "ApplicationStreamSettingRequirements)",
|
||||
replacement: {
|
||||
match: /for\(let \i of \i\.ApplicationStreamSettingRequirements\).+?!1/,
|
||||
replace: "return !0"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: "ApplicationStreamFPSButtonsWithSuffixLabel.map",
|
||||
replacement: {
|
||||
match: /(\i=)(.{19}FPS.+?(\i).{11}>(\i).\i,(\i),\i,([A-z.]+).+?\}\)),/,
|
||||
replace: (_, g1, g2, g3, g4, g5, g6) => `${g1}[$self.CustomRange(${g4},${g5},${g3},${g6},'fps'),...${g2}],`
|
||||
}
|
||||
},
|
||||
{
|
||||
find: "ApplicationStreamResolutionButtonsWithSuffixLabel.map",
|
||||
replacement: {
|
||||
match: /(\i=)(.{19}Resolution.+?(\i).{11}>(\i).\i,\i,(\i),([A-z.]+).+?\}\));/,
|
||||
replace: (_, g1, g2, g3, g4, g5, g6) => `${g1}[$self.CustomRange(${g4},${g3},${g5},${g6},'resolution'),...${g2}];`
|
||||
}
|
||||
},
|
||||
{
|
||||
find: "\"remoteSinkWantsPixelCount\",\"remoteSinkWantsMaxFramerate\"",
|
||||
replacement: {
|
||||
match: /(max:|\i=)4e6,/,
|
||||
replace: (_, g1) => `${g1}8e6,`
|
||||
}
|
||||
},
|
||||
{
|
||||
find: "\"remoteSinkWantsPixelCount\",\"remoteSinkWantsMaxFramerate\"",
|
||||
replacement: {
|
||||
match: /(\i)=15e3/, // disable discord idle fps reduction
|
||||
replace: (_, g1) => `${g1}=15e8`
|
||||
}
|
||||
},
|
||||
{
|
||||
find: "updateRemoteWantsFramerate(){",
|
||||
replacement: {
|
||||
match: /updateRemoteWantsFramerate\(\)\{/, // disable discord mute fps reduction
|
||||
replace: match => `${match}return;`
|
||||
}
|
||||
},
|
||||
{
|
||||
find: "{getQuality(",
|
||||
replacement: {
|
||||
match: /bitrateMin:.+?,bitrateMax:.+?,bitrateTarget:.+?,/,
|
||||
replace: "bitrateMin:$self.getMinBitrate(),bitrateMax:$self.getMaxBitrate(),bitrateTarget:$self.getTargetBitrate(),"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: "ApplicationStreamResolutionButtonsWithSuffixLabel.map",
|
||||
replacement: {
|
||||
match: /stream-settings-resolution-.+?children:\[/,
|
||||
replace: match => `${match}$self.settings.store.bitrates?$self.BitrateGroup():null,`
|
||||
}
|
||||
}
|
||||
],
|
||||
CustomRange(changeRes: Function, res: number, fps: number, analytics: string, group: "fps" | "resolution") {
|
||||
const [value, setValue] = useState(group === "fps" ? fps : res);
|
||||
const { maxFPS, maxResolution, roundValues } = settings.store;
|
||||
|
||||
const maxValue = group === "fps" ? maxFPS : maxResolution,
|
||||
minValue = group === "fps" ? 1 : 22; // 0 FPS freezes (obviously) and anything less than 22p doesn't work
|
||||
|
||||
const onChange = (number: number) => {
|
||||
let tmp = denormalize(number, minValue, maxValue);
|
||||
if (roundValues)
|
||||
tmp = Math.round(tmp);
|
||||
setValue(tmp);
|
||||
cooldown(() => changeRes(true, group === "resolution" ? tmp : res, group === "fps" ? tmp : fps, analytics));
|
||||
};
|
||||
return (<Menu.MenuControlItem group={`stream-settings-${group}`} id={`stream-settings-${group}-custom`}>
|
||||
<Menu.MenuSliderControl
|
||||
onChange={onChange}
|
||||
renderValue={() => value + (group === "fps" ? " FPS" : "p")}
|
||||
value={normalize((group === "fps" ? fps : res), minValue, maxValue) || 0}
|
||||
minValue={0}
|
||||
maxValue={100}>
|
||||
</Menu.MenuSliderControl>
|
||||
</Menu.MenuControlItem>);
|
||||
},
|
||||
BitrateGroup() {
|
||||
const bitrates: Array<"target" | "min" | "max"> = ["min", "target", "max"];
|
||||
|
||||
return (<Menu.MenuGroup label="Bitrate (Min/Target/Max)">
|
||||
{bitrates.map(e => this.BitrateSlider(e))}
|
||||
</Menu.MenuGroup>);
|
||||
},
|
||||
BitrateSlider(name: "target" | "min" | "max") {
|
||||
const [bitrate, setBitrate] = useState(this.settings.store[name + "Bitrate"]);
|
||||
const { minBitrate, maxBitrate } = settings.store;
|
||||
const onChange = (number: number) => {
|
||||
const tmp = denormalize(number, name === "min" ? 1000 : minBitrate, name === "max" ? 20000000 : maxBitrate);
|
||||
setBitrate(tmp);
|
||||
this.settings.store[name + "Bitrate"] = Math.round(tmp);
|
||||
};
|
||||
return (<Menu.MenuControlItem group={`stream-settings-bitrate-${name}`} id={`stream-settings-bitrate-${name}-custom`}>
|
||||
<Menu.MenuSliderControl
|
||||
onChange={onChange}
|
||||
renderValue={() => Math.round(bitrate / 1000) + "kbps"}
|
||||
value={normalize(bitrate, name === "min" ? 1000 : minBitrate, name === "max" ? 20000000 : maxBitrate) || 0}
|
||||
minValue={0}
|
||||
maxValue={100}>
|
||||
</Menu.MenuSliderControl>
|
||||
</Menu.MenuControlItem>);
|
||||
},
|
||||
getMinBitrate() {
|
||||
const { minBitrate } = settings.store;
|
||||
return minBitrate;
|
||||
},
|
||||
getTargetBitrate() {
|
||||
const { targetBitrate } = settings.store;
|
||||
return targetBitrate;
|
||||
},
|
||||
getMaxBitrate() {
|
||||
const { maxBitrate } = settings.store;
|
||||
return maxBitrate;
|
||||
}
|
||||
});
|
|
@ -1,34 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
let isOnCooldown = false;
|
||||
let nextFunction: Function | undefined = undefined;
|
||||
|
||||
export function cooldown(func: Function | undefined) {
|
||||
if (isOnCooldown) {
|
||||
nextFunction = func;
|
||||
return;
|
||||
}
|
||||
isOnCooldown = true;
|
||||
nextFunction = undefined;
|
||||
|
||||
if (func && typeof func === "function")
|
||||
func();
|
||||
|
||||
setTimeout(() => {
|
||||
isOnCooldown = false;
|
||||
if (nextFunction)
|
||||
cooldown(nextFunction);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
export function normalize(value: number, minValue: number, maxValue: any): number | undefined {
|
||||
return (value - minValue) / (maxValue - minValue) * 100;
|
||||
}
|
||||
|
||||
export function denormalize(number: number, minValue: number, maxValue: any) {
|
||||
return number * (maxValue - minValue) / 100 + minValue;
|
||||
}
|
|
@ -19,8 +19,11 @@
|
|||
import { Settings, SettingsStore } from "@api/Settings";
|
||||
import { Toasts } from "@webpack/common";
|
||||
|
||||
import { Logger } from "./Logger";
|
||||
import { compileUsercss } from "./themes/usercss/compiler";
|
||||
|
||||
const logger = new Logger("QuickCSS");
|
||||
|
||||
|
||||
let style: HTMLStyleElement;
|
||||
let themesStyle: HTMLStyleElement;
|
||||
|
@ -60,26 +63,75 @@ export async function toggle(isEnabled: boolean) {
|
|||
async function initThemes() {
|
||||
themesStyle ??= createStyle("vencord-themes");
|
||||
|
||||
const { themeLinks, disabledThemeLinks, enabledThemes } = Settings;
|
||||
const { enabledThemeLinks, enabledThemes } = Settings;
|
||||
|
||||
let links: string[] = [...themeLinks];
|
||||
|
||||
links = links.filter(link => !disabledThemeLinks.includes(link));
|
||||
const links: string[] = [...enabledThemeLinks];
|
||||
|
||||
if (IS_WEB) {
|
||||
for (const theme of enabledThemes) {
|
||||
const themeData = await VencordNative.themes.getThemeData(theme);
|
||||
if (!themeData) continue;
|
||||
for (let i = enabledThemes.length - 1; i >= 0; i--) {
|
||||
const theme = enabledThemes[i];
|
||||
|
||||
try {
|
||||
var themeData = await VencordNative.themes.getThemeData(theme);
|
||||
} catch (e) {
|
||||
logger.warn("Failed to get theme data for", theme, "(has it gone missing?)", e);
|
||||
}
|
||||
|
||||
if (!themeData) {
|
||||
// disable the theme since it has problems
|
||||
Settings.enabledThemes = enabledThemes.splice(enabledThemes.indexOf(theme), 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
const blob = new Blob([themeData], { type: "text/css" });
|
||||
links.push(URL.createObjectURL(blob));
|
||||
}
|
||||
} else {
|
||||
for (const theme of enabledThemes) if (!theme.endsWith(".user.css")) {
|
||||
for (let i = enabledThemes.length - 1; i >= 0; i--) {
|
||||
const theme = enabledThemes[i];
|
||||
|
||||
if (theme.endsWith(".user.css")) continue;
|
||||
|
||||
try {
|
||||
// whilst this is unnecessary here, we're doing it to make sure the theme is valid
|
||||
await VencordNative.themes.getThemeData(theme);
|
||||
} catch (e) {
|
||||
logger.warn("Failed to get theme data for", theme, "(has it gone missing?)", e);
|
||||
Settings.enabledThemes = enabledThemes.splice(enabledThemes.indexOf(theme), 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
links.push(`vencord:///themes/${theme}?v=${Date.now()}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!IS_WEB || "armcord" in window) {
|
||||
for (let i = enabledThemes.length - 1; i >= 0; i--) {
|
||||
const theme = enabledThemes[i];
|
||||
|
||||
if (!theme.endsWith(".user.css")) continue;
|
||||
|
||||
// UserCSS goes through a compile step first
|
||||
const css = await compileUsercss(theme);
|
||||
if (!css) {
|
||||
// let's not leave the user in the dark about this and point them to where they can find the error
|
||||
Toasts.show({
|
||||
message: `Failed to compile ${theme}, check the console for more info.`,
|
||||
type: Toasts.Type.FAILURE,
|
||||
id: Toasts.genId(),
|
||||
options: {
|
||||
position: Toasts.Position.BOTTOM
|
||||
}
|
||||
});
|
||||
Settings.enabledThemes = enabledThemes.splice(enabledThemes.indexOf(theme), 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
const blob = new Blob([css], { type: "text/css" });
|
||||
links.push(URL.createObjectURL(blob));
|
||||
}
|
||||
}
|
||||
|
||||
if (!IS_WEB || "armcord" in window) {
|
||||
for (const theme of enabledThemes) if (theme.endsWith(".user.css")) {
|
||||
// UserCSS goes through a compile step first
|
||||
|
@ -112,7 +164,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||
toggle(Settings.useQuickCss);
|
||||
SettingsStore.addChangeListener("useQuickCss", toggle);
|
||||
|
||||
SettingsStore.addChangeListener("themeLinks", initThemes);
|
||||
SettingsStore.addChangeListener("enabledThemeLinks", initThemes);
|
||||
SettingsStore.addChangeListener("enabledThemes", initThemes);
|
||||
SettingsStore.addChangeListener("userCssVars", initThemes);
|
||||
|
||||
|
|
|
@ -55,7 +55,11 @@ const preprocessors: { [preprocessor: string]: (text: string, vars: Record<strin
|
|||
};
|
||||
|
||||
export async function compileUsercss(fileName: string) {
|
||||
const themeData = await VencordNative.themes.getThemeData(fileName);
|
||||
try {
|
||||
var themeData = await VencordNative.themes.getThemeData(fileName);
|
||||
} catch (e) {
|
||||
UserCSSLogger.warn("Failed to get theme data for", fileName, "(has it gone missing?)", e);
|
||||
}
|
||||
if (!themeData) return null;
|
||||
|
||||
// UserCSS preprocessor order look like this:
|
||||
|
@ -71,7 +75,9 @@ export async function compileUsercss(fileName: string) {
|
|||
return null;
|
||||
}
|
||||
|
||||
const varsToPass = {};
|
||||
const varsToPass = {
|
||||
vencord: "true"
|
||||
};
|
||||
|
||||
for (const [k, v] of Object.entries(vars)) {
|
||||
varsToPass[k] = Settings.userCssVars[id]?.[k] ?? v.default;
|
||||
|
|
2
src/utils/themes/usercss/usercss-meta.d.ts
vendored
2
src/utils/themes/usercss/usercss-meta.d.ts
vendored
|
@ -19,7 +19,7 @@ declare module "usercss-meta" {
|
|||
}
|
||||
| {
|
||||
type: "checkbox";
|
||||
default: boolean;
|
||||
default: string;
|
||||
}
|
||||
| {
|
||||
type: "range";
|
||||
|
|
Loading…
Add table
Reference in a new issue