From 2960e9a74a02c849af30ca929dd888427b263bb7 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Fri, 21 Jun 2024 15:50:36 -0400 Subject: [PATCH] Updates --- README.md | 3 +- src/api/Settings.ts | 4 +- src/components/Icons.tsx | 37 +++ src/components/ThemeSettings/OnlineThemes.tsx | 258 ---------------- src/components/ThemeSettings/ThemesTab.tsx | 277 +++++++++++------- src/components/ThemeSettings/UserCSSModal.tsx | 115 +++++++- .../components/SettingBooleanComponent.tsx | 8 +- .../components/SettingColorComponent.tsx | 8 +- .../components/SettingNumberComponent.tsx | 8 +- .../components/SettingRangeComponent.tsx | 8 +- .../components/SettingSelectComponent.tsx | 8 +- .../components/SettingTextComponent.tsx | 8 +- .../ThemeSettings/components/colorStyles.css | 4 +- src/components/ThemeSettings/themesStyles.css | 11 + src/components/VencordSettings/AddonCard.tsx | 7 +- .../VencordSettings/themesStyles.css | 19 +- .../customScreenShare.desktop/index.tsx | 201 ------------- .../customScreenShare.desktop/utils.ts | 34 --- src/utils/quickCss.ts | 70 ++++- src/utils/themes/usercss/compiler.ts | 10 +- src/utils/themes/usercss/usercss-meta.d.ts | 2 +- 21 files changed, 414 insertions(+), 686 deletions(-) delete mode 100644 src/components/ThemeSettings/OnlineThemes.tsx delete mode 100644 src/equicordplugins/customScreenShare.desktop/index.tsx delete mode 100644 src/equicordplugins/customScreenShare.desktop/utils.ts diff --git a/README.md b/README.md index 06c734f2..24cac7a5 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ An enhanced version of [Vencord](https://github.com/Vendicated/Vencord) by [Vend - Request for plugins from Discord.
-Extra included plugins (62 additional plugins) +Extra included plugins (61 additional plugins) - 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 diff --git a/src/api/Settings.ts b/src/api/Settings.ts index 004a7aef..6579e783 100644 --- a/src/api/Settings.ts +++ b/src/api/Settings.ts @@ -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, diff --git a/src/components/Icons.tsx b/src/components/Icons.tsx index a3618822..5a5e5402 100644 --- a/src/components/Icons.tsx +++ b/src/components/Icons.tsx @@ -309,3 +309,40 @@ export function NoEntrySignIcon(props: IconProps) { ); } + +export function PasteIcon(props: IconProps) { + return ( + + + + + ); +} + +export function ResetIcon(props: IconProps) { + return ( + + + + + ); +} diff --git a/src/components/ThemeSettings/OnlineThemes.tsx b/src/components/ThemeSettings/OnlineThemes.tsx deleted file mode 100644 index 7a0c921d..00000000 --- a/src/components/ThemeSettings/OnlineThemes.tsx +++ /dev/null @@ -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([]); - - 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(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 - - Add Online Theme - - - - Only raw.githubusercontent.com and github.io URLs will work - - - {error} - - - - - - ; - } - - return ( - <> - - Find Themes: - - 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. - - - - - - - -
- {themes.length === 0 && ( - Add themes with the "Add Theme" button above - )} - {themes.map(theme => ( - (!theme.error && { - setThemeEnabled(theme.link, value); - }} - onDelete={() => removeTheme(theme.link)} - theme={theme.headers!} - showDelete={true} - extraButtons={ -
copyWithToast(theme.link, "Link copied to clipboard!")} - > - -
- } - /> - ) || ( - { }} - hideSwitch={true} - infoButton={<> -
copyWithToast(theme.link, "Link copied to clipboard!")} - > - -
-
removeTheme(theme.link)} - > - -
- - } - /> - ) - ))} -
-
- - ); -} diff --git a/src/components/ThemeSettings/ThemesTab.tsx b/src/components/ThemeSettings/ThemesTab.tsx index 79434e7b..8cd4e5b2 100644 --- a/src/components/ThemeSettings/ThemesTab.tsx +++ b/src/components/ThemeSettings/ThemesTab.tsx @@ -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 . +*/ 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 {text}; } 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 ( - - {extraButtons} -
- -
- - ) - } - footer={ - - {!!theme.website && Website} - {!!(theme.website && theme.invite) && " • "} - {!!theme.invite && ( - { - e.preventDefault(); - theme.invite != null && openInviteModal(theme.invite).catch(() => showToast("Invalid or expired invite")); - }} - > - Discord Server - - )} - - } - /> - ); -} - -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 && ( {({ onMouseLeave, onMouseEnter }) => (
@@ -135,7 +124,7 @@ function UserCSSThemeCard({ theme, enabled, onChange, onDelete }: UserCSSCardPro {theme.vars && (
openModal(modalProps => - ) + ) }>
@@ -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 ( - {extraButtons} -
+ (IS_WEB || showDeleteButton) && ( +
- ) } 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(null); const [currentTab, setCurrentTab] = useState(ThemeTab.LOCAL); + const [currentThemeLink, setCurrentThemeLink] = useState(""); + const [themeLinkValid, setThemeLinkValid] = useState(false); const [userThemes, setUserThemes] = useState(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((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 ( <> @@ -319,38 +302,45 @@ function ThemesTab() { GitHub
- - If using the BD site, click on "Download" and place the downloaded - .theme.css file into your themes folder. - + If using the BD site, click on "Download" and place the downloaded .theme.css file into your themes folder. <> - {IS_WEB ? ( - - ) : ( - - )} - + ) : ( + + )} + - @@ -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 ( + <> + + + Make sure to use direct links to files (raw or github.io)! + + + + + {currentThemeLink && } + + +
+ {onlineThemes?.map(theme => { + return onThemeLinkEnabledChange(theme.link, enabled)} + onDelete={() => deleteThemeLink(theme.link)} + showDeleteButton + theme={theme} + />; + })} +
+
+ + ); + } + return ( - + Local Themes - + Online Themes - {currentTab === ThemeTab.LOCAL && renderLocalThemes()} + {currentTab === ThemeTab.LOCAL && } {currentTab === ThemeTab.ONLINE && } ); diff --git a/src/components/ThemeSettings/UserCSSModal.tsx b/src/components/ThemeSettings/UserCSSModal.tsx index 9880c3b2..a1e43b18 100644 --- a/src/components/ThemeSettings/UserCSSModal.tsx +++ b/src/components/ThemeSettings/UserCSSModal.tsx @@ -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 + {({ onMouseLeave, onMouseEnter }) => ( +
{ + copyWithToast(JSON.stringify(themeSettings), "Copied theme settings to clipboard."); + }}> + +
+ )} +
; +} + +function ImportButton({ themeSettings }: { themeSettings: Settings["userCssVars"][""]; }) { + return + {({ onMouseLeave, onMouseEnter }) => ( +
{ + 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 = + 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); + }}> + +
+ )} +
; +} + +interface ResetButtonProps { + themeSettings: Settings["userCssVars"]; + themeId: string; + close: () => void; + onReset: () => void; +} + +function ResetButton({ themeSettings, themeId, close, onReset }: ResetButtonProps) { + return + {({ onMouseLeave, onMouseEnter }) => ( +
{ + await close(); // close the modal first to stop rendering + delete themeSettings[themeId]; + onReset(); + }}> + +
+ )} +
; +} + +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 ); break; @@ -42,7 +126,7 @@ export function UserCSSSettingsModal({ modalProps, theme }: UserCSSSettingsModal ); break; @@ -53,7 +137,7 @@ export function UserCSSSettingsModal({ modalProps, theme }: UserCSSSettingsModal ); break; @@ -64,7 +148,7 @@ export function UserCSSSettingsModal({ modalProps, theme }: UserCSSSettingsModal ); 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 Settings for {theme.name} + + + + + - {controls} + + {controls} + ); diff --git a/src/components/ThemeSettings/components/SettingBooleanComponent.tsx b/src/components/ThemeSettings/components/SettingBooleanComponent.tsx index a0d3239b..6b49ae43 100644 --- a/src/components/ThemeSettings/components/SettingBooleanComponent.tsx +++ b/src/components/ThemeSettings/components/SettingBooleanComponent.tsx @@ -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) { parseInt(TinyColor(value).toHex(), 16), [value]); + const normalizedValue = useMemo(() => parseInt(TinyColor(themeSettings[name]).toHex(), 16), [themeSettings[name]]); return ( diff --git a/src/components/ThemeSettings/components/SettingNumberComponent.tsx b/src/components/ThemeSettings/components/SettingNumberComponent.tsx index 35f57ed4..d59c21e0 100644 --- a/src/components/ThemeSettings/components/SettingNumberComponent.tsx +++ b/src/components/ThemeSettings/components/SettingNumberComponent.tsx @@ -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} /> diff --git a/src/components/ThemeSettings/components/SettingRangeComponent.tsx b/src/components/ThemeSettings/components/SettingRangeComponent.tsx index 6bb15ff8..b6437582 100644 --- a/src/components/ThemeSettings/components/SettingRangeComponent.tsx +++ b/src/components/ThemeSettings/components/SettingRangeComponent.tsx @@ -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 {label} v === value} + isSelected={v => v === themeSettings[name]} serialize={identity} /> diff --git a/src/components/ThemeSettings/components/SettingTextComponent.tsx b/src/components/ThemeSettings/components/SettingTextComponent.tsx index a568d633..be3ccf42 100644 --- a/src/components/ThemeSettings/components/SettingTextComponent.tsx +++ b/src/components/ThemeSettings/components/SettingTextComponent.tsx @@ -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) { {label} diff --git a/src/components/ThemeSettings/components/colorStyles.css b/src/components/ThemeSettings/components/colorStyles.css index 1bd4a35a..8b7bfc51 100644 --- a/src/components/ThemeSettings/components/colorStyles.css +++ b/src/components/ThemeSettings/components/colorStyles.css @@ -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; -} \ No newline at end of file +} diff --git a/src/components/ThemeSettings/themesStyles.css b/src/components/ThemeSettings/themesStyles.css index 4b8c9a91..ab37c951 100644 --- a/src/components/ThemeSettings/themesStyles.css +++ b/src/components/ThemeSettings/themesStyles.css @@ -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; +} diff --git a/src/components/VencordSettings/AddonCard.tsx b/src/components/VencordSettings/AddonCard.tsx index f2e44be6..1161a641 100644 --- a/src/components/VencordSettings/AddonCard.tsx +++ b/src/components/VencordSettings/AddonCard.tsx @@ -37,12 +37,11 @@ interface Props { onMouseLeave?: MouseEventHandler; 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(null); const titleContainerRef = useRef(null); return ( @@ -79,11 +78,11 @@ export function AddonCard({ disabled, isNew, name, infoButton, footer, author, e {infoButton} - {!hideSwitch && } + />
{description} diff --git a/src/components/VencordSettings/themesStyles.css b/src/components/VencordSettings/themesStyles.css index e37b1c6e..6b8ffdb9 100644 --- a/src/components/VencordSettings/themesStyles.css +++ b/src/components/VencordSettings/themesStyles.css @@ -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); -} \ No newline at end of file diff --git a/src/equicordplugins/customScreenShare.desktop/index.tsx b/src/equicordplugins/customScreenShare.desktop/index.tsx deleted file mode 100644 index 32de11e5..00000000 --- a/src/equicordplugins/customScreenShare.desktop/index.tsx +++ /dev/null @@ -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 - Max FPS for the range slider - { props.setValue(Math.max(Number.parseInt(value), 1)); setValue(value); }} value={value} /> - ; - } - }, - maxResolution: { - description: "Max Resolution for the range slider", - default: 1080, - type: OptionType.COMPONENT, - component: (props: any) => { - const [value, setValue] = useState(settings.store.maxResolution); - return - Max Resolution for the range slider - { props.setValue(Math.max(Number.parseInt(value), 22)); setValue(value); }} value={value} /> - ; - } - }, - 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: () => ( - - Usage - - Adds a slider for the quality and fps options submenu - - ), - 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 ( - value + (group === "fps" ? " FPS" : "p")} - value={normalize((group === "fps" ? fps : res), minValue, maxValue) || 0} - minValue={0} - maxValue={100}> - - ); - }, - BitrateGroup() { - const bitrates: Array<"target" | "min" | "max"> = ["min", "target", "max"]; - - return ( - {bitrates.map(e => this.BitrateSlider(e))} - ); - }, - 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 ( - Math.round(bitrate / 1000) + "kbps"} - value={normalize(bitrate, name === "min" ? 1000 : minBitrate, name === "max" ? 20000000 : maxBitrate) || 0} - minValue={0} - maxValue={100}> - - ); - }, - getMinBitrate() { - const { minBitrate } = settings.store; - return minBitrate; - }, - getTargetBitrate() { - const { targetBitrate } = settings.store; - return targetBitrate; - }, - getMaxBitrate() { - const { maxBitrate } = settings.store; - return maxBitrate; - } -}); diff --git a/src/equicordplugins/customScreenShare.desktop/utils.ts b/src/equicordplugins/customScreenShare.desktop/utils.ts deleted file mode 100644 index 07770d2d..00000000 --- a/src/equicordplugins/customScreenShare.desktop/utils.ts +++ /dev/null @@ -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; -} diff --git a/src/utils/quickCss.ts b/src/utils/quickCss.ts index e1730eb1..49661191 100644 --- a/src/utils/quickCss.ts +++ b/src/utils/quickCss.ts @@ -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); diff --git a/src/utils/themes/usercss/compiler.ts b/src/utils/themes/usercss/compiler.ts index 6e461799..059b789a 100644 --- a/src/utils/themes/usercss/compiler.ts +++ b/src/utils/themes/usercss/compiler.ts @@ -55,7 +55,11 @@ const preprocessors: { [preprocessor: string]: (text: string, vars: Record