Compare commits

...

70 commits

Author SHA1 Message Date
Lewis Crichton
85b510b881
merge: dev 2023-12-26 14:50:01 +00:00
Lewis Crichton
c25e8ac8c1
Merge branch 'dev' into feat/usercss 2023-12-13 23:20:31 +00:00
Lewis Crichton
34e1c83756
fix: text strings not being done properly 2023-11-30 19:23:26 +00:00
Lewis Crichton
ea864b9a00
fix: compiler not properly inserting booleans for less/stylus 2023-11-30 19:04:22 +00:00
Lewis Crichton
d7e5c06e83
Merge branch 'dev' into feat/usercss 2023-11-30 17:15:21 +00:00
Lewis Crichton
39860bd05c
fix: make more specific (@Nuckyz) 2023-11-28 19:52:41 +00:00
Lewis Crichton
e3aab2b864
Merge branch 'dev' into feat/usercss 2023-11-28 19:51:50 +00:00
Lewis Crichton
66a27a1e79
Merge branch 'dev' into feat/usercss 2023-11-28 18:58:41 +00:00
V
58c6611abe
VoiceMessages: fix preview being blank 2023-11-27 16:05:52 +01:00
Nuckyz
fc10bc1e69
Utility function for loading Discord chunks (#2017) 2023-11-27 02:56:57 -03:00
Lewis Crichton
f4b846375f
chore: ???? 2023-11-25 17:03:56 +00:00
Lewis Crichton
6881ddbea7
chore: shhhhhh 2023-11-25 17:02:40 +00:00
Lewis Crichton
c981325fb3
fix: colorpicker (sorta)
this now uses the lazy-loaded colorpicker, but this only works if said
colorpicker has already been loaded. need to force load before anything.
2023-11-25 17:00:10 +00:00
Lewis Crichton
cbdaf7daa6
Merge branch 'feat/usercss' of ssh://github.com/lewisakura/Vencord into feat/usercss 2023-11-25 16:28:39 +00:00
Lewis Crichton
f68351b31b
Merge branch 'dev' of ssh://github.com/Vendicated/Vencord into feat/usercss 2023-11-25 16:28:34 +00:00
Lewis Crichton
a911dd17b1
Merge branch 'dev' into feat/usercss 2023-11-25 16:28:22 +00:00
Nuckyz
867730a478
Simplify some components finds; Make undo of patch groups more clear 2023-11-24 23:14:18 -03:00
Lewis Crichton
828a882017
Merge branch 'dev' into feat/usercss 2023-11-21 23:49:25 +00:00
Lewis Crichton
a57ab38c8c
Merge branch 'dev' into feat/usercss 2023-11-09 01:36:20 +00:00
Lewis Crichton
d544d33564
Merge branch 'dev' into feat/usercss 2023-11-07 23:18:01 +00:00
zImPatrick
dc3591ba18
Fix FakeNitro sticker bypass (#1964) 2023-11-07 15:58:10 -03:00
Marvin Witt
c0f786804a
fix(dearrow): don't replace thumbnail if only original available (#1959) 2023-11-05 02:06:08 +01:00
Lewis Crichton
31fd035bd3
Merge branch 'dev' into feat/usercss 2023-10-26 21:56:29 +01:00
Lewis Crichton
4dbffcb8b8
merge: dev 2023-10-26 21:45:00 +01:00
Lewis Crichton
c12dd258a6
style: grr 2023-10-16 22:54:29 +01:00
Lewis Crichton
b6547b463b
feat: @vc-requiredPlugins 2023-10-16 22:53:37 +01:00
Lewis Crichton
b7cdb96e09
Merge branch 'dev' into feat/usercss 2023-10-16 21:21:44 +01:00
Lewis Crichton
ff32014613
Merge branch 'feat/usercss' of ssh://github.com/lewisakura/Vencord into feat/usercss 2023-10-13 16:22:21 +01:00
Lewis Crichton
791eaa06d4
merge: dev 2023-10-13 16:21:40 +01:00
Lewis Crichton
25857377b6
Merge branch 'dev' into feat/usercss 2023-10-01 10:12:46 +01:00
Lewis Crichton
eb31ad994e
merge: dev branch 2023-09-27 21:42:29 +01:00
Lewis Crichton
6fbe24a268
feat: don't parse if not able to compile 2023-09-25 19:06:36 +01:00
Lewis Crichton
5bc24a5d78
feat: guards to prevent compiling this on web 2023-09-25 19:00:03 +01:00
Lewis Crichton
91e093a21d
chore: purify 2023-09-25 18:49:38 +01:00
Lewis Crichton
f8232694e7
style: 2x2 switches 2023-09-25 18:39:23 +01:00
Lewis Crichton
03bc5cde22
feat: make colorpicker use props for height/width 2023-09-25 18:32:16 +01:00
Lewis Crichton
4325dcf02e
feat: make the color picker look prettier based on switch 2023-09-25 18:27:49 +01:00
Lewis Crichton
1179a9f5a1
fix: dedupe 2023-09-25 18:11:35 +01:00
Lewis Crichton
7105558640
chore: add back warning lost in merge 2023-09-25 18:06:50 +01:00
Lewis Crichton
c019a3cc10
merge: i think i did this right? 2023-09-25 18:05:26 +01:00
Lewis Crichton
a79fb2718b
chore: de-bdify 2023-09-15 19:42:01 +01:00
Lewis Crichton
06f2239b1a
Merge branch 'dev' into feat/usercss 2023-09-15 19:40:07 +01:00
Lewis Crichton
1be6738715
perf: memoize relatively intensively computed values 2023-09-10 14:23:19 +01:00
Lewis Crichton
12509f8157
chore: clean lol 2023-09-10 14:11:25 +01:00
Lewis Crichton
74f9b1a022
feat: each settings component handles state, + fix selects again lol 2023-09-10 14:09:00 +01:00
Lewis Crichton
482caf0c5b
style: use switch for special case handling 2023-09-10 13:55:51 +01:00
Lewis Crichton
b1bdc48769
fix: redundant padding character in usercss id 2023-09-10 13:51:45 +01:00
Lewis Crichton
141b1a7041
fix: missing styles 2023-09-10 13:43:41 +01:00
Lewis Crichton
d43eebe0e4
refactor: split components and modal and whatnot 2023-09-10 13:40:04 +01:00
Lewis Crichton
f2dc34e023
Merge branch 'feat/usercss' of ssh://github.com/lewisakura/Vencord into feat/usercss 2023-09-09 19:48:39 +01:00
Lewis Crichton
f596941f3a
feat: checkbox type to bools in compiled output 2023-09-09 19:48:32 +01:00
Lewis Crichton
e4f4802155
Merge branch 'dev' into feat/usercss 2023-09-09 10:57:00 +01:00
Lewis Crichton
b7bd5096b6
fix: select defaults not working 2023-09-09 10:53:49 +01:00
Lewis Crichton
9fdd2c7c17
feat: better colorpicker 2023-09-09 10:43:07 +01:00
Lewis Crichton
51059c29e7
feat: non-exact settings subscriptions for live recompile 2023-09-09 10:17:21 +01:00
Lewis Crichton
64848b2fbf
feat: use built in tinycolor 2023-09-09 10:00:41 +01:00
Lewis Crichton
b6e20680ff
feat: my suffering is neverending and all i can think of is popups and modals 2023-09-08 22:19:21 +01:00
Lewis Crichton
d361edc47d
style: u love refactors ignoring stuff 2023-09-08 16:36:22 +01:00
Lewis Crichton
7174d2e744
perf: move theme parsing out of natives to prevent duplicate dependencies 2023-09-08 16:35:37 +01:00
Lewis Crichton
9a23571b3e
feat: resiliency against bad usercss 2023-09-08 15:54:25 +01:00
Lewis Crichton
723191ba9b
feat: usercss compilation and better settings storage 2023-09-08 14:59:41 +01:00
Lewis Crichton
0cc420fb45
Merge branch 'dev' into feat/usercss 2023-09-07 16:36:21 +01:00
Lewis Crichton
a939034bc1
style: dont add unnecessary space 2023-09-07 16:07:45 +01:00
Lewis Crichton
b350087a7a
style: for if 2023-09-03 13:26:07 +01:00
Lewis Crichton
19a87e3e94
feat: ranges with units, loading vars from settings 2023-09-02 22:02:32 +01:00
Lewis Crichton
c0dff86cb2
fix: don't add empty :root{} 2023-09-02 21:34:55 +01:00
Lewis Crichton
0d66604be5
style: explicit ype qualifier 2023-09-02 21:29:44 +01:00
Lewis Crichton
b7fb178f1f
feat: inject css vars for usercss 2023-09-02 21:27:22 +01:00
Lewis Crichton
d689b3273b
style: clean up whackass imports 2023-09-02 18:01:55 +01:00
Lewis Crichton
2ef2baafbe
feat: initial usercss support
Parses UserCSS/UserStyle files (.user.css) but doesn't do anything
special yet with the variables. This is a first step towards
supporting UserCSS themes.
2023-09-02 17:51:17 +01:00
30 changed files with 1033 additions and 56 deletions

View file

@ -25,7 +25,7 @@ import * as DataStore from "../src/api/DataStore";
import { debounce } from "../src/utils";
import { EXTENSION_BASE_URL } from "../src/utils/web-metadata";
import { getTheme, Theme } from "../src/utils/discord";
import { getThemeInfo } from "../src/main/themes";
import { getThemeInfo } from "../src/utils/themes/bd";
// Discord deletes this so need to store in variable
const { localStorage } = window;

View file

@ -39,15 +39,19 @@
"gifenc": "github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3",
"monaco-editor": "^0.43.0",
"nanoid": "^4.0.2",
"usercss-meta": "^0.12.0",
"virtual-merge": "^1.0.1"
},
"devDependencies": {
"@types/chrome": "^0.0.246",
"@types/diff": "^5.0.3",
"@types/less": "^3.0.4",
"@types/lodash": "^4.14.194",
"@types/node": "^18.16.3",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.1",
"@types/stylus": "^0.48.39",
"@types/tinycolor2": "^1.4.3",
"@types/yazl": "^2.4.2",
"@typescript-eslint/eslint-plugin": "^5.59.1",
"@typescript-eslint/parser": "^5.59.1",
@ -75,7 +79,8 @@
"pnpm": {
"patchedDependencies": {
"eslint-plugin-path-alias@1.0.0": "patches/eslint-plugin-path-alias@1.0.0.patch",
"eslint@8.46.0": "patches/eslint@8.46.0.patch"
"eslint@8.46.0": "patches/eslint@8.46.0.patch",
"@types/less@3.0.4": "patches/@types__less@3.0.4.patch"
},
"peerDependencyRules": {
"ignoreMissing": [

View file

@ -0,0 +1,13 @@
diff --git a/index.d.ts b/index.d.ts
index eb4f07d47b932fb9cc8c8cd451ab107f648bd013..18a3e15a1997734e1773718e5be55d252ed9478c 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -306,7 +306,5 @@ interface LessStatic {
}
declare module "less" {
- export = less;
+ export = LessStatic;
}
-
-declare var less: LessStatic;

View file

@ -1,6 +1,9 @@
lockfileVersion: '6.0'
patchedDependencies:
'@types/less@3.0.4':
hash: krcufrsfhsuxuoj7hocqugs6zi
path: patches/@types__less@3.0.4.patch
eslint-plugin-path-alias@1.0.0:
hash: m6sma4g6bh67km3q6igf6uxaja
path: patches/eslint-plugin-path-alias@1.0.0.patch
@ -33,6 +36,9 @@ dependencies:
nanoid:
specifier: ^4.0.2
version: 4.0.2
usercss-meta:
specifier: ^0.12.0
version: 0.12.0
virtual-merge:
specifier: ^1.0.1
version: 1.0.1
@ -44,6 +50,9 @@ devDependencies:
'@types/diff':
specifier: ^5.0.3
version: 5.0.3
'@types/less':
specifier: ^3.0.4
version: 3.0.4(patch_hash=krcufrsfhsuxuoj7hocqugs6zi)
'@types/lodash':
specifier: ^4.14.194
version: 4.14.194
@ -56,6 +65,12 @@ devDependencies:
'@types/react-dom':
specifier: ^18.2.1
version: 18.2.1
'@types/stylus':
specifier: ^0.48.39
version: 0.48.39
'@types/tinycolor2':
specifier: ^1.4.3
version: 1.4.3
'@types/yazl':
specifier: ^2.4.2
version: 2.4.2
@ -564,6 +579,11 @@ packages:
resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==}
dev: true
/@types/less@3.0.4(patch_hash=krcufrsfhsuxuoj7hocqugs6zi):
resolution: {integrity: sha512-djlMpTdDF+tLaqVpK/0DWGNIr7BFjN8ykDLkgS0sQGYYLop51imRRE3foTjl+dMAH1zFE8bMZAG0VbYPEcSgsA==}
dev: true
patched: true
/@types/lodash@4.14.194:
resolution: {integrity: sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g==}
dev: true
@ -613,6 +633,16 @@ packages:
resolution: {integrity: sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==}
dev: true
/@types/stylus@0.48.39:
resolution: {integrity: sha512-98a0QrJorrq8+Vsan9yfxol2Qr6nvUWBeV3oYnSMks4QdLMebAzZvRd9IuoZOcnB6Erfjcjn1J2J+63MPCxJnw==}
dependencies:
'@types/node': 18.16.3
dev: true
/@types/tinycolor2@1.4.3:
resolution: {integrity: sha512-Kf1w9NE5HEgGxCRyIcRXR/ZYtDv0V8FVPtYHwLxl0O+maGX0erE77pQlD0gpP+/KByMZ87mOA79SjifhSB3PjQ==}
dev: true
/@types/yauzl@2.10.0:
resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==}
requiresBuild: true
@ -3309,6 +3339,11 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
/usercss-meta@0.12.0:
resolution: {integrity: sha512-zKrXCKdpeIwtVe87omxGo9URf+7mbozduMZEg79dmT4KB3XJwfIkEi/Uk0PcTwR/nZLtAK1+k7isgbGB/g6E7Q==}
engines: {node: '>=8.3'}
dev: false
/util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
dev: true

View file

@ -8,7 +8,6 @@ import { IpcEvents } from "@utils/IpcEvents";
import { IpcRes } from "@utils/types";
import { ipcRenderer } from "electron";
import { PluginIpcMappings } from "main/ipcPlugins";
import type { UserThemeHeader } from "main/themes";
function invoke<T = any>(event: IpcEvents, ...args: any[]) {
return ipcRenderer.invoke(event, ...args) as Promise<T>;
@ -33,7 +32,7 @@ export default {
uploadTheme: (fileName: string, fileData: string) => invoke<void>(IpcEvents.UPLOAD_THEME, fileName, fileData),
deleteTheme: (fileName: string) => invoke<void>(IpcEvents.DELETE_THEME, fileName),
getThemesDir: () => invoke<string>(IpcEvents.GET_THEMES_DIR),
getThemesList: () => invoke<Array<UserThemeHeader>>(IpcEvents.GET_THEMES_LIST),
getThemesList: () => invoke<Array<{ fileName: string; content: string; }>>(IpcEvents.GET_THEMES_LIST),
getThemeData: (fileName: string) => invoke<string | undefined>(IpcEvents.GET_THEME_DATA, fileName),
getSystemValues: () => invoke<Record<string, string>>(IpcEvents.GET_THEME_SYSTEM_VALUES),
},

View file

@ -75,6 +75,12 @@ export interface Settings {
settingsSync: boolean;
settingsSyncVersion: number;
};
userCssVars: {
[fileName: string]: {
[varName: string]: string;
};
};
}
const DefaultSettings: Settings = {
@ -107,7 +113,9 @@ const DefaultSettings: Settings = {
url: "https://api.vencord.dev/",
settingsSync: false,
settingsSyncVersion: 0
}
},
userCssVars: {}
};
try {
@ -125,7 +133,7 @@ const saveSettingsOnFrequentAction = debounce(async () => {
}
}, 60_000);
type SubscriptionCallback = ((newValue: any, path: string) => void) & { _paths?: Array<string>; };
type SubscriptionCallback = ((newValue: any, path: string) => void) & { _paths?: Array<string>; _exact?: boolean; };
const subscriptions = new Set<SubscriptionCallback>();
const proxyCache = {} as Record<string, any>;
@ -182,7 +190,12 @@ function makeProxy(settings: any, root = settings, path = ""): Settings {
const setPath = `${path}${path && "."}${p}`;
delete proxyCache[setPath];
for (const subscription of subscriptions) {
if (!subscription._paths || subscription._paths.includes(setPath)) {
if (
!subscription._paths ||
(subscription._exact
? subscription._paths.includes(setPath)
: subscription._paths.some(p => setPath.startsWith(p)))
) {
subscription(v, setPath);
}
}
@ -220,11 +233,14 @@ export const Settings = makeProxy(settings);
* @returns Settings
*/
// TODO: Representing paths as essentially "string[].join('.')" wont allow dots in paths, change to "paths?: string[][]" later
export function useSettings(paths?: UseSettings<Settings>[]) {
export function useSettings(paths?: UseSettings<Settings>[], exact = true) {
const [, forceUpdate] = React.useReducer(() => ({}), {});
const onUpdate: SubscriptionCallback = paths
? (value, path) => paths.includes(path as UseSettings<Settings>) && forceUpdate()
? (value, path) =>
(exact
? paths.includes(path as UseSettings<Settings>)
: paths.some(p => path.startsWith(p))) && forceUpdate()
: forceUpdate;
React.useEffect(() => {
@ -250,11 +266,12 @@ type ResolvePropDeep<T, P> = P extends "" ? T :
* @example addSettingsListener("", (newValue, path) => console.log(`${path} is now ${newValue}`))
* addSettingsListener("plugins.Unindent.enabled", v => console.log("Unindent is now", v ? "enabled" : "disabled"))
*/
export function addSettingsListener<Path extends keyof Settings>(path: Path, onUpdate: (newValue: Settings[Path], path: Path) => void): void;
export function addSettingsListener<Path extends string>(path: Path, onUpdate: (newValue: Path extends "" ? any : ResolvePropDeep<Settings, Path>, path: Path extends "" ? string : Path) => void): void;
export function addSettingsListener(path: string, onUpdate: (newValue: any, path: string) => void) {
export function addSettingsListener<Path extends keyof Settings>(path: Path, onUpdate: (newValue: Settings[Path], path: Path) => void, exact?: boolean): void;
export function addSettingsListener<Path extends string>(path: Path, onUpdate: (newValue: Path extends "" ? any : ResolvePropDeep<Settings, Path>, path: Path extends "" ? string : Path) => void, exact?: boolean): void;
export function addSettingsListener(path: string, onUpdate: (newValue: any, path: string) => void, exact = true) {
if (path)
((onUpdate as SubscriptionCallback)._paths ??= []).push(path);
(onUpdate as SubscriptionCallback)._exact = exact;
subscriptions.add(onUpdate);
}

View file

@ -256,6 +256,24 @@ export function DeleteIcon(props: IconProps) {
);
}
/**
* A plugin icon, created by CorellanStoma. https://github.com/CreArts-Community/Settings-Icons
*/
export function PluginIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "vc-plugin-icon")}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M20.5 11H19V7c0-1.1-.9-2-2-2h-4V3.5C13 2.12 11.88 1 10.5 1S8 2.12 8 3.5V5H4c-1.1 0-1.99.9-1.99 2v3.8H3.5c1.49 0 2.7 1.21 2.7 2.7s-1.21 2.7-2.7 2.7H2V20c0 1.1.9 2 2 2h3.8v-1.5c0-1.49 1.21-2.7 2.7-2.7s2.7 1.21 2.7 2.7V22H17c1.1 0 2-.9 2-2v-4h1.5c1.38 0 2.5-1.12 2.5-2.5S21.88 11 20.5 11z"
/>
</Icon>
);
}
export function PlusIcon(props: IconProps) {
return (
<Icon

View file

@ -16,23 +16,31 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { useSettings } from "@api/Settings";
import "./themesStyles.css";
import { Settings, useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import { Flex } from "@components/Flex";
import { DeleteIcon } from "@components/Icons";
import { CogWheel, DeleteIcon, PluginIcon } from "@components/Icons";
import { Link } from "@components/Link";
import { AddonCard } from "@components/VencordSettings/AddonCard";
import { SettingsTab, wrapTab } from "@components/VencordSettings/shared";
import { openInviteModal } from "@utils/discord";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { openModal } from "@utils/modal";
import { showItemInFolder } from "@utils/native";
import { useAwaiter } from "@utils/react";
import type { ThemeHeader } from "@utils/themes";
import { getThemeInfo, stripBOM, type UserThemeHeader } from "@utils/themes/bd";
import { usercssParse } from "@utils/themes/usercss";
import { findByPropsLazy, findLazy } from "@webpack";
import { Button, Card, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common";
import { UserThemeHeader } from "main/themes";
import { Button, Card, Forms, React, showToast, TabBar, TextArea, Tooltip, useEffect, useMemo, useRef, useState } from "@webpack/common";
import type { ComponentType, Ref, SyntheticEvent } from "react";
import type { UserstyleHeader } from "usercss-meta";
import { AddonCard } from "./AddonCard";
import { SettingsTab, wrapTab } from "./shared";
import { isPluginEnabled } from "../../plugins";
import { UserCSSSettingsModal } from "./UserCSSModal";
type FileInput = ComponentType<{
ref: Ref<HTMLInputElement>;
@ -47,6 +55,7 @@ const TextAreaProps = findLazy(m => typeof m.textarea === "string");
const cl = classNameFactory("vc-settings-theme-");
function Validator({ link }: { link: string; }) {
const [res, err, pending] = useAwaiter(() => fetch(link).then(res => {
if (res.status > 300) throw `${res.status} ${res.statusText}`;
@ -95,14 +104,73 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) {
);
}
interface ThemeCardProps {
interface OtherThemeCardProps {
theme: UserThemeHeader;
enabled: boolean;
onChange: (enabled: boolean) => void;
onDelete: () => void;
}
function ThemeCard({ theme, enabled, onChange, onDelete }: ThemeCardProps) {
interface UserCSSCardProps {
theme: UserstyleHeader;
enabled: boolean;
onChange: (enabled: boolean) => void;
onDelete: () => void;
}
function UserCSSThemeCard({ theme, enabled, onChange, onDelete }: UserCSSCardProps) {
const missingPlugins = useMemo(() =>
theme.requiredPlugins?.filter(p => !isPluginEnabled(p)), [theme]);
return (
<AddonCard
name={theme.name ?? "Unknown"}
description={theme.description}
author={theme.author ?? "Unknown"}
enabled={enabled}
setEnabled={onChange}
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" }}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<PluginIcon />
</div>
)}
</Tooltip>
)}
{theme.vars && (
<div style={{ cursor: "pointer" }} onClick={
() => openModal(modalProps =>
<UserCSSSettingsModal modalProps={modalProps} theme={theme} />)
}>
<CogWheel />
</div>
)}
{IS_WEB && (
<div style={{ cursor: "pointer", color: "var(--status-danger" }} onClick={onDelete}>
<DeleteIcon />
</div>
)}
</>
}
footer={
<Flex flexDirection="row" style={{ gap: "0.2em" }}>
{!!theme.homepageURL && <Link href={theme.homepageURL}>Homepage</Link>}
{!!(theme.homepageURL && theme.supportURL) && " • "}
{!!theme.supportURL && <Link href={theme.supportURL}>Support</Link>}
</Flex>
}
/>
);
}
function OtherThemeCard({ theme, enabled, onChange, onDelete }: OtherThemeCardProps) {
return (
<AddonCard
name={theme.name}
@ -149,7 +217,7 @@ function ThemesTab() {
const fileInputRef = useRef<HTMLInputElement>(null);
const [currentTab, setCurrentTab] = useState(ThemeTab.LOCAL);
const [themeText, setThemeText] = useState(settings.themeLinks.join("\n"));
const [userThemes, setUserThemes] = useState<UserThemeHeader[] | null>(null);
const [userThemes, setUserThemes] = useState<ThemeHeader[] | null>(null);
const [themeDir, , themeDirPending] = useAwaiter(VencordNative.themes.getThemesDir);
useEffect(() => {
@ -158,7 +226,57 @@ function ThemesTab() {
async function refreshLocalThemes() {
const themes = await VencordNative.themes.getThemesList();
setUserThemes(themes);
const themeInfo: ThemeHeader[] = [];
for (const { fileName, content } of themes) {
if (!fileName.endsWith(".css")) continue;
if ((!IS_WEB || "armcord" in window) && fileName.endsWith(".user.css")) {
// handle it as usercss
const header = await usercssParse(content, fileName);
themeInfo.push({
type: "usercss",
header
});
Settings.userCssVars[header.id] ??= {};
for (const [name, varInfo] of Object.entries(header.vars ?? {})) {
let normalizedValue = "";
switch (varInfo.type) {
case "text":
case "color":
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;
case "number":
normalizedValue = String(varInfo.default);
break;
}
Settings.userCssVars[header.id][name] ??= normalizedValue;
}
} else {
// presumably BD but could also be plain css
themeInfo.push({
type: "other",
header: getThemeInfo(stripBOM(content), fileName)
});
}
}
setUserThemes(themeInfo);
}
// When a local theme is enabled/disabled, update the settings
@ -252,19 +370,32 @@ function ThemesTab() {
</Card>
<div className={cl("grid")}>
{userThemes?.map(theme => (
<ThemeCard
key={theme.fileName}
enabled={settings.enabledThemes.includes(theme.fileName)}
onChange={enabled => onLocalThemeChange(theme.fileName, enabled)}
onDelete={async () => {
onLocalThemeChange(theme.fileName, false);
await VencordNative.themes.deleteTheme(theme.fileName);
refreshLocalThemes();
}}
theme={theme}
/>
))}
{userThemes?.map(({ type, header: theme }: ThemeHeader) => (
type === "other" ? (
<OtherThemeCard
key={theme.fileName}
enabled={settings.enabledThemes.includes(theme.fileName)}
onChange={enabled => onLocalThemeChange(theme.fileName, enabled)}
onDelete={async () => {
onLocalThemeChange(theme.fileName, false);
await VencordNative.themes.deleteTheme(theme.fileName);
refreshLocalThemes();
}}
theme={theme as UserThemeHeader}
/>
) : (
<UserCSSThemeCard
key={theme.fileName}
enabled={settings.enabledThemes.includes(theme.fileName)}
onChange={enabled => onLocalThemeChange(theme.fileName, enabled)}
onDelete={async () => {
onLocalThemeChange(theme.fileName, false);
await VencordNative.themes.deleteTheme(theme.fileName);
refreshLocalThemes();
}}
theme={theme as UserstyleHeader}
/>
)))}
</div>
</Forms.FormSection>
</>

View file

@ -0,0 +1,114 @@
/*
* 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 { Flex } from "@components/Flex";
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot } from "@utils/modal";
import { Text } from "@webpack/common";
import type { ReactNode } from "react";
import { UserstyleHeader } from "usercss-meta";
import { SettingBooleanComponent, SettingColorComponent, SettingNumberComponent, SettingRangeComponent, SettingSelectComponent, SettingTextComponent } from "./components";
interface UserCSSSettingsModalProps {
modalProps: ModalProps;
theme: UserstyleHeader;
}
export function UserCSSSettingsModal({ modalProps, theme }: UserCSSSettingsModalProps) {
// @ts-expect-error UseSettings<> can't determine this is a valid key
const themeSettings = useSettings(["userCssVars"], false).userCssVars[theme.id];
const controls: ReactNode[] = [];
for (const [name, varInfo] of Object.entries(theme.vars)) {
switch (varInfo.type) {
case "text": {
controls.push(
<SettingTextComponent
label={varInfo.label}
name={name}
themeSettings={themeSettings}
/>
);
break;
}
case "checkbox": {
controls.push(
<SettingBooleanComponent
label={varInfo.label}
name={name}
themeSettings={themeSettings}
/>
);
break;
}
case "color": {
controls.push(
<SettingColorComponent
label={varInfo.label}
name={name}
themeSettings={themeSettings}
/>
);
break;
}
case "number": {
controls.push(
<SettingNumberComponent
label={varInfo.label}
name={name}
themeSettings={themeSettings}
/>
);
break;
}
case "select": {
controls.push(
<SettingSelectComponent
label={varInfo.label}
name={name}
options={varInfo.options}
default={varInfo.default}
themeSettings={themeSettings}
/>
);
break;
}
case "range": {
controls.push(
<SettingRangeComponent
label={varInfo.label}
name={name}
default={varInfo.default}
min={varInfo.min}
max={varInfo.max}
step={varInfo.step}
themeSettings={themeSettings}
/>
);
break;
}
}
}
return (
<ModalRoot {...modalProps}>
<ModalHeader separator={false}>
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>Settings for {theme.name}</Text>
<ModalCloseButton onClick={modalProps.onClose} />
</ModalHeader>
<ModalContent>
<Flex flexDirection="column" style={{ gap: 12, marginBottom: 16 }}>{controls}</Flex>
</ModalContent>
</ModalRoot>
);
}

View file

@ -0,0 +1,39 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Forms, Switch, useState } from "@webpack/common";
interface Props {
label: string;
name: string;
themeSettings: Record<string, string>;
}
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;
}
return (
<Forms.FormSection>
<Switch
key={name}
value={value === "1"}
onChange={handleChange}
hideBorder
style={{ marginBottom: "0.5em" }}
>
{label}
</Switch>
</Forms.FormSection>
);
}

View file

@ -0,0 +1,56 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./colorStyles.css";
import { classNameFactory } from "@api/Styles";
import { findByCodeLazy, findComponentByCodeLazy } from "@webpack";
import { Forms, useMemo, useState } from "@webpack/common";
interface ColorPickerProps {
color: number | null;
showEyeDropper?: boolean;
onChange(value: number | null): void;
}
const ColorPicker = findComponentByCodeLazy<ColorPickerProps>(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)");
// TinyColor is completely unmangled and it's duplicated in two modules! Fun!
const TinyColor: tinycolor.Constructor = findByCodeLazy("this._gradientType=");
const cl = classNameFactory("vc-usercss-settings-color-");
interface Props {
label: string;
name: string;
themeSettings: Record<string, string>;
}
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]);
return (
<Forms.FormSection>
<div className={cl("swatch-row")}>
<span>{label}</span>
<ColorPicker
key={name}
color={normalizedValue}
onChange={handleChange}
/>
</div>
</Forms.FormSection>
);
}

View file

@ -0,0 +1,36 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Forms, TextInput, useState } from "@webpack/common";
interface Props {
label: string;
name: string;
themeSettings: Record<string, string>;
}
export function SettingNumberComponent({ label, name, themeSettings }: Props) {
const [value, setValue] = useState(themeSettings[name]);
function handleChange(value: string) {
setValue(value);
themeSettings[name] = value;
}
return (
<Forms.FormSection>
<Forms.FormTitle tag="h5">{label}</Forms.FormTitle>
<TextInput
type="number"
pattern="-?[0-9]+"
key={name}
value={value}
onChange={handleChange}
/>
</Forms.FormSection>
);
}

View file

@ -0,0 +1,56 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Forms, Slider, useMemo, useState } from "@webpack/common";
interface Props {
label: string;
name: string;
default: number;
min?: number;
max?: number;
step?: number;
themeSettings: Record<string, string>;
}
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;
}
const markers = useMemo(() => {
const markers: number[] = [];
// defaults taken from https://github.com/openstyles/stylus/wiki/Writing-UserCSS#default-value
for (let i = (min ?? 0); i <= (max ?? 10); i += (step ?? 1)) {
markers.push(i);
}
return markers;
}, [min, max, step]);
return (
<Forms.FormSection>
<Forms.FormTitle tag="h5">{label}</Forms.FormTitle>
<Slider
initialValue={parseInt(value, 10)}
defaultValue={def}
onValueChange={handleChange}
minValue={min}
maxValue={max}
markers={markers}
stickToMarkers={true}
/>
</Forms.FormSection>
);
}

View file

@ -0,0 +1,55 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { identity } from "@utils/misc";
import { ComponentTypes, Forms, Select, useMemo, useState } from "@webpack/common";
interface Props {
label: string;
name: string;
options: {
name: string;
label: string;
value: string;
}[];
default: string;
themeSettings: Record<string, string>;
}
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;
}
const opts = useMemo(() => options.map(option => ({
disabled: false,
key: option.name,
value: option.value,
default: def === option.name,
label: option.label
} satisfies ComponentTypes.SelectOption)), [options, def]);
return (
<Forms.FormSection>
<Forms.FormTitle tag="h5">{label}</Forms.FormTitle>
<Select
placeholder={label}
key={name}
options={opts}
closeOnSelect={true}
select={handleChange}
isSelected={v => v === value}
serialize={identity}
/>
</Forms.FormSection>
);
}

View file

@ -0,0 +1,34 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Forms, TextInput, useState } from "@webpack/common";
interface Props {
label: string;
name: string;
themeSettings: Record<string, string>;
}
export function SettingTextComponent({ label, name, themeSettings }: Props) {
const [value, setValue] = useState(themeSettings[name]);
function handleChange(value: string) {
setValue(value);
themeSettings[name] = value;
}
return (
<Forms.FormSection>
<Forms.FormTitle tag="h5">{label}</Forms.FormTitle>
<TextInput
key={name}
value={value}
onChange={handleChange}
/>
</Forms.FormSection>
);
}

View file

@ -0,0 +1,19 @@
.vc-usercss-settings-color-swatch-row {
display: flex;
flex-direction: row;
width: 100%;
align-items: center;
}
.vc-usercss-settings-color-swatch-row > span {
display: block;
flex: 1;
overflow: hidden;
margin-top: 0;
margin-bottom: 0;
color: var(--header-primary);
line-height: 24px;
font-size: 16px;
font-weight: 500;
word-wrap: break-word;
}

View file

@ -0,0 +1,12 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export * from "./SettingBooleanComponent";
export * from "./SettingColorComponent";
export * from "./SettingNumberComponent";
export * from "./SettingRangeComponent";
export * from "./SettingSelectComponent";
export * from "./SettingTextComponent";

View file

@ -17,7 +17,6 @@
*/
import "./settingsStyles.css";
import "./themesStyles.css";
import ErrorBoundary from "@components/ErrorBoundary";
import { handleComponentFailed } from "@components/handleComponentFailed";

View file

@ -29,7 +29,6 @@ import { join, normalize } from "path";
import monacoHtml from "~fileContent/monacoWin.html;base64";
import { getThemeInfo, stripBOM, UserThemeHeader } from "./themes";
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, SETTINGS_FILE, THEMES_DIR } from "./utils/constants";
import { makeLinksOpenExternally } from "./utils/externalLinks";
@ -47,21 +46,11 @@ function readCss() {
return readFile(QUICKCSS_PATH, "utf-8").catch(() => "");
}
async function listThemes(): Promise<UserThemeHeader[]> {
const files = await readdir(THEMES_DIR).catch(() => []);
const themeInfo: UserThemeHeader[] = [];
for (const fileName of files) {
if (!fileName.endsWith(".css")) continue;
const data = await getThemeData(fileName).then(stripBOM).catch(() => null);
if (data == null) continue;
themeInfo.push(getThemeInfo(data, fileName));
}
return themeInfo;
function listThemes(): Promise<{ fileName: string; content: string; }[]> {
return readdir(THEMES_DIR)
.then(files =>
Promise.all(files.map(async fileName => ({ fileName, content: await getThemeData(fileName) }))))
.catch(() => []);
}
function getThemeData(fileName: string) {

View file

@ -101,7 +101,7 @@ export default definePlugin({
{
section: "VencordThemes",
label: "Themes",
element: require("@components/VencordSettings/ThemesTab").default,
element: require("@components/ThemeSettings/ThemesTab").default,
className: "vc-themes"
},
!IS_UPDATER_DISABLED && {

View file

@ -16,6 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import type StylusRenderer = require("stylus/lib/renderer");
import type LessStatic from "less";
import { makeLazy } from "./lazy";
import { EXTENSION_BASE_URL } from "./web-metadata";
@ -84,3 +87,18 @@ export const shikiOnigasmSrc = "https://unpkg.com/@vap/shiki@0.10.3/dist/onig.wa
// @ts-expect-error
export const getStegCloak = /* #__PURE__*/ makeLazy(() => import("https://unpkg.com/stegcloak-dist@1.0.0/index.js"));
export const getStylus = /* #__PURE__*/ makeLazy(async () => {
const stylusScript = await fetch("https://unpkg.com/stylus-lang-bundle@0.58.1/dist/stylus-renderer.min.js").then(r => r.text());
// the stylus bundle doesn't have a header that checks for export conditions so we can just patch the script to
// return the renderer itself
const patchedScript = stylusScript.replace("var StylusRenderer=", "return ");
return Function(patchedScript)() as typeof StylusRenderer;
});
export const getLess = /* #__PURE__*/ makeLazy(async () => {
const lessScript = await fetch("https://unpkg.com/less@4.2.0/dist/less.min.js").then(r => r.text());
const module = { exports: {} };
Function("module", "exports", lessScript)(module, module.exports);
return module.exports as LessStatic;
});

View file

@ -17,6 +17,9 @@
*/
import { addSettingsListener, Settings } from "@api/Settings";
import { Toasts } from "@webpack/common";
import { compileUsercss } from "./themes/usercss/compiler";
let style: HTMLStyleElement;
@ -65,12 +68,36 @@ async function initThemes() {
for (const theme of enabledThemes) {
const themeData = await VencordNative.themes.getThemeData(theme);
if (!themeData) continue;
const blob = new Blob([themeData], { type: "text/css" });
links.push(URL.createObjectURL(blob));
}
} else {
const localThemes = enabledThemes.map(theme => `vencord:///themes/${theme}?v=${Date.now()}`);
links.push(...localThemes);
for (const theme of enabledThemes) if (!theme.endsWith(".user.css")) {
links.push(`vencord:///themes/${theme}?v=${Date.now()}`);
}
}
if (!IS_WEB || "armcord" in window) {
for (const theme of enabledThemes) if (theme.endsWith(".user.css")) {
// 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
}
});
continue;
}
const blob = new Blob([css], { type: "text/css" });
links.push(URL.createObjectURL(blob));
}
}
themesStyle.textContent = links.map(link => `@import url("${link.trim()}");`).join("\n");
@ -85,6 +112,7 @@ document.addEventListener("DOMContentLoaded", () => {
addSettingsListener("themeLinks", initThemes);
addSettingsListener("enabledThemes", initThemes);
addSettingsListener("userCssVars", initThemes, false);
if (!IS_WEB)
VencordNative.quickCss.addThemeChangeListener(initThemes);

17
src/utils/themes/index.ts Normal file
View file

@ -0,0 +1,17 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import type { UserstyleHeader } from "usercss-meta";
import type { UserThemeHeader } from "./bd";
export type ThemeHeader = {
type: "other";
header: UserThemeHeader;
} | {
type: "usercss";
header: UserstyleHeader;
};

View file

@ -0,0 +1,109 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Settings } from "@api/Settings";
import { getLess, getStylus } from "@utils/dependencies";
import { Logger } from "@utils/Logger";
import { usercssParse } from ".";
const UserCSSLogger = new Logger("UserCSS:Compiler", "#d2acf5");
const preprocessors: { [preprocessor: string]: (text: string, vars: Record<string, string>) => Promise<string>; } = {
async default(text: string, vars: Record<string, string>) {
const variables = Object.entries(vars)
.map(([name, value]) => `--${name}: ${value}`)
.join("; ");
return `/* ==Vencord== */\n:root{${variables}}\n/* ==/Vencord== */${text}`;
},
async uso(text: string, vars: Record<string, string>) {
for (const [k, v] of Object.entries(vars)) {
text = text.replace(new RegExp(`\\/\\*\\[\\[${k}\\]\\]\\*\\/`, "g"), v);
}
return text;
},
async stylus(text: string, vars: Record<string, string>) {
const StylusRenderer = await getStylus();
const variables = Object.entries(vars)
.map(([name, value]) => `${name} = ${value}`)
.join("\n");
const stylusDoc = `// ==Vencord==\n${variables}\n// ==/Vencord==\n${text}`;
return new StylusRenderer(stylusDoc).render();
},
async less(text: string, vars: Record<string, string>) {
const less = await getLess();
const variables = Object.entries(vars)
.map(([name, value]) => `@${name}: ${value};`)
.join("\n");
const lessDoc = `// ==Vencord==\n${variables}\n// ==/Vencord==\n${text}`;
return less.render(lessDoc).then(r => r.css);
}
};
export async function compileUsercss(fileName: string) {
const themeData = await VencordNative.themes.getThemeData(fileName);
if (!themeData) return null;
// UserCSS preprocessor order look like this:
// - use the preprocessor defined
// - if variables are set, `uso`
// - otherwise, `default`
const { vars = {}, preprocessor = Object.keys(vars).length > 0 ? "uso" : "default", id } = await usercssParse(themeData, fileName);
const preprocessorFn = preprocessors[preprocessor];
if (!preprocessorFn) {
UserCSSLogger.error("File", fileName, "requires preprocessor", preprocessor, "which isn't known to Vencord");
return null;
}
const varsToPass = {};
for (const [k, v] of Object.entries(vars)) {
varsToPass[k] = Settings.userCssVars[id]?.[k] ?? v.default;
switch (v.type) {
case "checkbox": {
if (["less", "stylus"].includes(preprocessor)) {
varsToPass[k] = varsToPass[k] === "1" ? "true" : "false";
}
break;
}
case "range": {
varsToPass[k] = `${varsToPass[k]}${v.units ?? "px"}`;
break;
}
case "text": {
if (preprocessor === "stylus") {
varsToPass[k] = `"${varsToPass[k].replace(/"/g, "\" + '\"' + \"")}"`;
} else {
varsToPass[k] = `"${varsToPass[k].replace(/\//g, "\\\\").replace(/"/g, '\\"')}"`;
}
break;
}
}
}
try {
return await preprocessorFn(themeData, varsToPass);
} catch (error) {
UserCSSLogger.error("File", fileName, "failed to compile with preprocessor", preprocessor, error);
return null;
}
}

View file

@ -0,0 +1,44 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Logger } from "@utils/Logger";
import { parse as originalParse, UserstyleHeader } from "usercss-meta";
const UserCSSLogger = new Logger("UserCSS", "#d2acf5");
export async function usercssParse(text: string, fileName: string): Promise<UserstyleHeader> {
const { metadata, errors } = originalParse(text.replace(/\r/g, ""), {
allowErrors: true,
unknownKey: "assign"
});
if (errors.length) {
UserCSSLogger.warn("Parsed", fileName, "with errors:", errors);
}
const requiredPlugins = metadata["vc-requiredPlugins"]?.split(",").map(p => p.trim());
return {
...metadata,
fileName,
id: await getUserCssId(metadata),
requiredPlugins
};
}
export async function getUserCssId(header: UserstyleHeader): Promise<string> {
const encoder = new TextEncoder();
const nameBuf = encoder.encode(header.name);
const namespaceBuf = encoder.encode(header.namespace);
const nameHash = new Uint8Array(await window.crypto.subtle.digest("SHA-256", nameBuf));
const namespaceHash = new Uint8Array(await window.crypto.subtle.digest("SHA-256", namespaceBuf));
const idHash = await window.crypto.subtle.digest("SHA-256", new Uint8Array([...nameHash, ...namespaceHash]));
return window.btoa(String.fromCharCode(...new Uint8Array(idHash))).substring(0, 43); // base64 adds one more padding character
}

View file

@ -0,0 +1,134 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
declare module "usercss-meta" {
import { Simplify } from "type-fest";
export type UserCSSVariable = Simplify<{ name: string; label: string; } & (
| {
type: "text";
default: string;
}
| {
type: "color";
// Hex, rgb(), rgba()
default: string;
}
| {
type: "checkbox";
default: boolean;
}
| {
type: "range";
default: number;
min?: number;
max?: number;
step?: number;
units?: string;
}
| {
type: "number";
default: number;
}
| {
type: "select";
default: string;
options: { name: string; label: string; value: string; }[];
}
)>;
export interface UserstyleHeader {
/**
* The unique ID of the UserCSS style.
*
* @vencord Specific to Vencord, not part of the original module.
*/
id: string;
/**
* The file name of the UserCSS style.
*
* @vencord Specific to Vencord, not part of the original module.
*/
fileName: string;
/**
* The required plugins for this style.
*
* @vencord Specific to Vencord, not part of the original module.
* @see {@link vc-requiredPlugins}
*/
requiredPlugins?: string[];
/**
* The name of your style.
*
* The combination of {@link name} and {@link namespace} must be unique.
*/
name: string;
/**
* The namespace of the style. Helps to distinguish between styles with the same name.
*
* The combination of {@link name} and {@link namespace} must be unique.
*/
namespace: string;
/**
* The version of your style.
*/
version: string;
/**
* A short significant description.
*/
description?: string;
/**
* The author of the style.
*/
author?: string;
/**
* The project's homepage.
*
* This is not an update URL. See {@link updateURL}.
*/
homepageURL?: string;
/**
* The URL the user can report issues to the style author.
*/
supportURL?: string;
/**
* The URL used when updating the style.
*/
updateURL?: string;
/**
* The SPDX license identifier for this style. If none is included, the style is assumed to be All Rights Reserved.
*/
license?: string;
/**
* The CSS preprocessor used to write this style.
*/
preprocessor?: "default" | "uso" | "less" | "stylus";
/**
* A list of variables the style defines.
*/
vars: Record<string, UserCSSVariable>;
/**
* Required plugins for this style to work. Comma-separated list of plugin names.
*
* @vencord This is a Vencord-specific extension, however we wish for this to become a standard for client mods
* to implement, hence the more generic namespaced name.
*/
"vc-requiredPlugins"?: string;
}
type UserCSSParseOptions = {
allowErrors: boolean;
unknownKey: "assign";
};
export function parse(text: string, options: UserCSSParseOptions): { metadata: UserstyleHeader; errors: { code: string; args: any; }[]; };
}

View file

@ -191,7 +191,7 @@ export type TextArea = ComponentType<PropsWithRef<Omit<HTMLProps<HTMLTextAreaEle
onChange(v: string): void;
}>>;
interface SelectOption {
export interface SelectOption {
disabled?: boolean;
value: any;
label: string;