Merge branch 'dev' into translation

This commit is contained in:
thororen1234 2026-04-19 13:29:09 -04:00
commit 2d6baf00db
No known key found for this signature in database
436 changed files with 4878 additions and 1667 deletions

View file

@ -23,10 +23,10 @@ jobs:
- uses: pnpm/action-setup@v3
- name: Use Node.js 20
- name: Use Node.js 24
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 24
cache: "pnpm"
- name: Install dependencies
@ -72,6 +72,7 @@ jobs:
gh release upload latest --clobber dist/release/*
- name: Upload Plugin JSONs to Equibored repo
if: github.repository == 'Equicord/Equicord'
run: |
git config --global user.name "GitHub-Actions"
git config --global user.email actions@github.com

View file

@ -1,4 +1,4 @@
name: Build
name: Build Branches
on:
push:
@ -28,6 +28,7 @@ jobs:
Build:
name: Build Equicord
runs-on: ubuntu-latest
if: github.repository == 'Equicord/Equicord'
steps:
- uses: actions/checkout@v4
@ -37,11 +38,11 @@ jobs:
- uses: pnpm/action-setup@v3
if: github.ref_name != 'bun'
- name: Use Node.js 20
- name: Use Node.js 24
if: github.ref_name != 'bun'
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 24
cache: "pnpm"
- name: Install dependencies
@ -64,9 +65,8 @@ jobs:
pnpm generateVencordPluginJson dist/vencordplugins.json
pnpm generateDevsList dist/devs.json
- uses: actions/checkout@v4
if: github.ref_name == 'bun'
- uses: oven-sh/setup-bun@v2
if: github.ref_name == 'bun'
with:
bun-version: latest
- name: Install dependencies

View file

@ -20,10 +20,10 @@ jobs:
- uses: pnpm/action-setup@v3
- name: Use Node.js 20
- name: Use Node.js 24
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 24
cache: "pnpm"
- name: Install dependencies
@ -50,7 +50,7 @@ jobs:
cp browser/browser.* release
cp Equicord.user.{js,js.LEGAL.txt} release
# copy the plugin data jsons, the extension zips and the desktop/vesktop asars
# copy the plugin data jsons, the extension zips and the desktop/equibop asars
cp *.{json,zip,asar} release
# legacy un-asared files

View file

@ -62,11 +62,11 @@ jobs:
if: steps.check.outputs.release == 'true'
uses: pnpm/action-setup@v3
- name: Use Node.js 20
- name: Use Node.js 24
if: steps.check.outputs.release == 'true'
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 24
cache: "pnpm"
- name: Install dependencies

View file

@ -33,10 +33,10 @@ jobs:
- uses: pnpm/action-setup@v3 # Install pnpm using packageManager key in package.json
- name: Use Node.js 20
- name: Use Node.js 24
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 24
cache: "pnpm"
- name: Install dependencies

View file

@ -18,10 +18,10 @@ jobs:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3 # Install pnpm using packageManager key in package.json
- name: Use Node.js 20
- name: Use Node.js 24
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 24
cache: "pnpm"
- name: Install dependencies

View file

@ -1,7 +1,7 @@
{
"name": "equicord",
"private": "true",
"version": "1.14.6.2",
"version": "1.14.7.1",
"description": "The other cutest Discord client mod",
"homepage": "https://github.com/Equicord/Equicord#readme",
"bugs": {
@ -55,47 +55,47 @@
"gifenc": "github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3",
"idb": "8.0.3",
"jsqr": "1.4.0",
"monaco-editor": "^0.54.0",
"monaco-editor": "^0.55.1",
"nanoid": "^5.1.6",
"native-file-system-adapter": "^3.0.1",
"usercss-meta": "^0.12.0",
"virtual-merge": "^1.0.1"
"virtual-merge": "^1.0.1",
"yaml-js": "^0.3.1"
},
"devDependencies": {
"@electron/asar": "^4.0.1",
"@stylistic/eslint-plugin": "^5.6.0",
"@types/adm-zip": "^0.5.7",
"@types/chrome": "^0.1.30",
"@types/diff": "^7.0.2",
"@types/lodash": "^4.17.20",
"@types/node": "^24.10.1",
"@electron/asar": "^4.2.0",
"@stylistic/eslint-plugin": "^5.10.0",
"@types/chrome": "^0.1.40",
"@types/lodash": "^4.17.24",
"@types/node": "^25.6.0",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@types/yazl": "^3.3.0",
"@types/yazl": "^3.3.1",
"@vencord/discord-types": "link:packages/discord-types",
"diff": "^8.0.2",
"esbuild": "^0.27.0",
"eslint": "9.39.1",
"diff": "^9.0.0",
"esbuild": "^0.28.0",
"eslint": "10.2.0",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-simple-header": "^1.2.1",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-unused-imports": "^4.3.0",
"eslint-plugin-simple-import-sort": "^13.0.0",
"eslint-plugin-unused-imports": "^4.4.1",
"highlight.js": "11.11.1",
"html-minifier-terser": "^7.2.0",
"moment": "^2.22.2",
"p-limit": "^7.3.0",
"puppeteer-core": "^24.30.0",
"standalone-electron-types": "^34.2.0",
"stylelint": "^16.25.0",
"stylelint-config-standard": "^39.0.1",
"puppeteer-core": "^24.41.0",
"standalone-electron-types": "^41.2.1",
"stylelint": "^17.8.0",
"stylelint-config-standard": "^40.0.0",
"svgo": "^4.0.1",
"ts-patch": "^3.3.0",
"ts-pattern": "^5.6.0",
"tsx": "^4.20.6",
"type-fest": "^5.2.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.47.0",
"typescript-transform-paths": "^3.5.5",
"ts-pattern": "^5.9.0",
"tsx": "^4.21.0",
"type-fest": "^5.5.0",
"typescript": "^6.0.2",
"typescript-eslint": "^8.58.2",
"typescript-transform-paths": "^3.5.6",
"zip-local": "^0.3.5"
},
"packageManager": "pnpm@10.33.0",
@ -126,4 +126,4 @@
"engines": {
"node": ">=18"
}
}
}

1857
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -77,7 +77,7 @@ const globNativesPlugin = {
});
build.onLoad({ filter, namespace: "import-natives" }, async () => {
const pluginDirs = ["plugins", "userplugins", "equicordplugins"];
const pluginDirs = ["plugins", "equicordplugins", "userplugins"];
let code = "";
let natives = "\n";
let i = 0;

View file

@ -147,7 +147,7 @@ export const globPlugins = kind => ({
});
build.onLoad({ filter, namespace: "import-plugins" }, async () => {
const pluginDirs = ["plugins/_api", "plugins/_core", "plugins", "userplugins", "equicordplugins", "equicordplugins/_api"];
const pluginDirs = ["plugins/_api", "plugins/_core", "plugins", "equicordplugins", "equicordplugins/_api", "userplugins"];
let code = "";
let pluginsCode = "\n";
let metaCode = "\n";

View file

@ -38,6 +38,7 @@ export interface PluginData {
name: string;
description: string;
tags: string[];
searchTerms: string[];
authors: Dev[];
dependencies: string[];
hasPatches: boolean;
@ -185,7 +186,8 @@ export async function parseFile(fileName: string) {
enabledByDefault: false,
required: false,
isModified: false,
tags: [] as string[]
tags: [] as string[],
searchTerms: [] as string[],
} as PluginData;
for (const prop of pluginObj.properties) {
@ -244,9 +246,10 @@ export async function parseFile(fileName: string) {
});
break;
case "tags":
if (!isArrayLiteralExpression(value)) throw fail("tags is not an array literal");
data.tags = value.elements.map(e => {
if (!isStringLiteral(e)) throw fail("tags array contains non-string literals");
case "searchTerms":
if (!isArrayLiteralExpression(value)) throw fail(`${key} is not an array literal`);
data[key] = value.elements.map(e => {
if (!isStringLiteral(e)) throw fail(`${key} array contains non-string literals`);
return e.text;
});
break;

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { findByCodeLazy, findLazy } from "webpack";
import { findByCodeLazy, findLazy } from "@webpack";
let defaultSounds: null | string[] = null;
const findDefaultSounds = findLazy(module => module.resolve && module.id && module.keys().some(key => key.endsWith(".mp3")), false);

View file

@ -18,97 +18,31 @@
* limitations under the License.
*/
import { Logger } from "@utils/Logger";
const logger = new Logger("DataStore");
let loggedFailure = false;
const memoryDatabases = new Map<string, Map<string, Map<IDBValidKey, unknown>>>();
interface MemoryStore {
__memoryStore: true;
get<T>(key: IDBValidKey): T | undefined;
put(value: unknown, key: IDBValidKey): void;
delete(key: IDBValidKey): void;
clear(): void;
getAllKeys<KeyType extends IDBValidKey>(): KeyType[];
getAll<T>(): T[];
entries<KeyType extends IDBValidKey, ValueType>(): [KeyType, ValueType][];
}
type StoreLike = IDBObjectStore | MemoryStore;
function handleFailure(error: unknown) {
if (loggedFailure) return;
loggedFailure = true;
logger.error("IndexedDB unavailable, using empty storage.", error);
}
function withFailureFallback<T>(promise: Promise<T>, fallback: T): Promise<T> {
return promise.catch(error => {
handleFailure(error);
return fallback;
});
}
function getMemoryStore(dbName: string, storeName: string): MemoryStore {
let database = memoryDatabases.get(dbName);
if (!database) {
database = new Map();
memoryDatabases.set(dbName, database);
}
let data = database.get(storeName);
if (!data) {
data = new Map<IDBValidKey, unknown>();
database.set(storeName, data);
}
return {
__memoryStore: true,
get: <T>(key: IDBValidKey) => data.get(key) as T | undefined,
put: (value, key) => { data.set(key, value); },
delete: key => { data.delete(key); },
clear: () => { data.clear(); },
getAllKeys: <KeyType extends IDBValidKey>() => Array.from(data.keys()) as KeyType[],
getAll: <T>() => Array.from(data.values()) as T[],
entries: <KeyType extends IDBValidKey, ValueType>() => Array.from(data.entries()) as [KeyType, ValueType][]
};
}
function isMemoryStore(store: StoreLike): store is MemoryStore {
return "__memoryStore" in store;
}
export function promisifyRequest<T = undefined>(
request: IDBRequest<T> | IDBTransaction,
): Promise<T> {
return new Promise<T>((resolve, reject) => {
const target = request as IDBRequest<T> & IDBTransaction;
target.oncomplete = target.onsuccess = () => resolve(target.result);
target.onabort = target.onerror = () => reject(target.error);
// @ts-expect-error - file size hacks
request.oncomplete = request.onsuccess = () => resolve(request.result);
// @ts-expect-error - file size hacks
request.onabort = request.onerror = () => reject(request.error);
});
}
export function createStore(dbName: string, storeName: string): UseStore {
const request = indexedDB.open(dbName);
request.onupgradeneeded = () => request.result.createObjectStore(storeName);
const dbp = promisifyRequest(request).catch(error => {
handleFailure(error);
return null;
});
const memoryStore = getMemoryStore(dbName, storeName);
const dbp = promisifyRequest(request);
return async (txMode, callback) => {
const db = await dbp;
if (!db) return callback(memoryStore);
return callback(db.transaction(storeName, txMode).objectStore(storeName));
};
return (txMode, callback) =>
dbp.then(db =>
callback(db.transaction(storeName, txMode).objectStore(storeName)),
);
}
export type UseStore = <T>(
txMode: IDBTransactionMode,
callback: (store: StoreLike) => T | PromiseLike<T>,
callback: (store: IDBObjectStore) => T | PromiseLike<T>,
) => Promise<T>;
let defaultGetStoreFunc: UseStore | undefined;
@ -120,68 +54,88 @@ function defaultGetStore() {
return defaultGetStoreFunc;
}
/**
* Get a value by its key.
*
* @param key
* @param customStore Method to get a custom store. Use with caution (see the docs).
*/
export function get<T = any>(
key: IDBValidKey,
customStore = defaultGetStore(),
): Promise<T | undefined> {
return withFailureFallback(customStore("readonly", store => {
if (isMemoryStore(store)) return store.get(key) as T | undefined;
return promisifyRequest(store.get(key));
}), undefined);
return customStore("readonly", store => promisifyRequest(store.get(key)));
}
/**
* Set a value with a key.
*
* @param key
* @param value
* @param customStore Method to get a custom store. Use with caution (see the docs).
*/
export function set(
key: IDBValidKey,
value: any,
customStore = defaultGetStore(),
): Promise<void> {
return withFailureFallback(customStore("readwrite", store => {
return customStore("readwrite", store => {
store.put(value, key);
if (isMemoryStore(store)) return;
return promisifyRequest(store.transaction);
}), undefined);
});
}
/**
* Set multiple values at once. This is faster than calling set() multiple times.
* It's also atomic – if one of the pairs can't be added, none will be added.
*
* @param entries Array of entries, where each entry is an array of `[key, value]`.
* @param customStore Method to get a custom store. Use with caution (see the docs).
*/
export function setMany(
entries: [IDBValidKey, any][],
customStore = defaultGetStore(),
): Promise<void> {
return withFailureFallback(customStore("readwrite", store => {
return customStore("readwrite", store => {
entries.forEach(entry => store.put(entry[1], entry[0]));
if (isMemoryStore(store)) return;
return promisifyRequest(store.transaction);
}), undefined);
});
}
/**
* Get multiple values by their keys
*
* @param keys
* @param customStore Method to get a custom store. Use with caution (see the docs).
*/
export function getMany<T = any>(
keys: IDBValidKey[],
customStore = defaultGetStore(),
): Promise<T[]> {
return withFailureFallback(customStore("readonly", store => {
if (isMemoryStore(store)) {
return keys.map(key => store.get(key) as T | undefined);
}
return Promise.all(keys.map(key => promisifyRequest(store.get(key))));
}), []);
return customStore("readonly", store =>
Promise.all(keys.map(key => promisifyRequest(store.get(key)))),
);
}
/**
* Update a value. This lets you see the old value and update it as an atomic operation.
*
* @param key
* @param updater A callback that takes the old value and returns a new value.
* @param customStore Method to get a custom store. Use with caution (see the docs).
*/
export function update<T = any>(
key: IDBValidKey,
updater: (oldValue: T | undefined) => T,
customStore = defaultGetStore(),
): Promise<void> {
return withFailureFallback(customStore(
return customStore(
"readwrite",
store => {
if (isMemoryStore(store)) {
store.put(updater(store.get(key) as T | undefined), key);
return;
}
return new Promise((resolve, reject) => {
store =>
// Need to create the promise manually.
// If I try to chain promises, the transaction closes in browsers
// that use a promise polyfill (IE10/11).
new Promise((resolve, reject) => {
store.get(key).onsuccess = function () {
try {
store.put(updater(this.result), key);
@ -190,42 +144,52 @@ export function update<T = any>(
reject(err);
}
};
});
},
), undefined);
}),
);
}
/**
* Delete a particular key from the store.
*
* @param key
* @param customStore Method to get a custom store. Use with caution (see the docs).
*/
export function del(
key: IDBValidKey,
customStore = defaultGetStore(),
): Promise<void> {
return withFailureFallback(customStore("readwrite", store => {
return customStore("readwrite", store => {
store.delete(key);
if (isMemoryStore(store)) return;
return promisifyRequest(store.transaction);
}), undefined);
});
}
/**
* Delete multiple keys at once.
*
* @param keys List of keys to delete.
* @param customStore Method to get a custom store. Use with caution (see the docs).
*/
export function delMany(
keys: IDBValidKey[],
customStore = defaultGetStore(),
): Promise<void> {
return withFailureFallback(customStore("readwrite", store => {
keys.forEach(key => store.delete(key));
if (isMemoryStore(store)) return;
return customStore("readwrite", (store: IDBObjectStore) => {
keys.forEach((key: IDBValidKey) => store.delete(key));
return promisifyRequest(store.transaction);
}), undefined);
});
}
/**
* Clear all values in the store.
*
* @param customStore Method to get a custom store. Use with caution (see the docs).
*/
export function clear(customStore = defaultGetStore()): Promise<void> {
return withFailureFallback(customStore("readwrite", store => {
return customStore("readwrite", store => {
store.clear();
if (isMemoryStore(store)) return;
return promisifyRequest(store.transaction);
}), undefined);
});
}
function eachCursor(
@ -240,12 +204,16 @@ function eachCursor(
return promisifyRequest(store.transaction);
}
/**
* Get all keys in the store.
*
* @param customStore Method to get a custom store. Use with caution (see the docs).
*/
export function keys<KeyType extends IDBValidKey>(
customStore = defaultGetStore(),
): Promise<KeyType[]> {
return withFailureFallback(customStore("readonly", store => {
if (isMemoryStore(store)) return store.getAllKeys<KeyType>();
return customStore("readonly", store => {
// Fast path for modern browsers
if (store.getAllKeys) {
return promisifyRequest(
store.getAllKeys() as unknown as IDBRequest<KeyType[]>,
@ -253,45 +221,59 @@ export function keys<KeyType extends IDBValidKey>(
}
const items: KeyType[] = [];
return eachCursor(store, cursor =>
items.push(cursor.key as KeyType),
).then(() => items);
}), [] as KeyType[]);
});
}
/**
* Get all values in the store.
*
* @param customStore Method to get a custom store. Use with caution (see the docs).
*/
export function values<T = any>(customStore = defaultGetStore()): Promise<T[]> {
return withFailureFallback(customStore("readonly", store => {
if (isMemoryStore(store)) return store.getAll<T>();
return customStore("readonly", store => {
// Fast path for modern browsers
if (store.getAll) {
return promisifyRequest(store.getAll() as IDBRequest<T[]>);
}
const items: T[] = [];
return eachCursor(store, cursor => items.push(cursor.value as T)).then(
() => items,
);
}), [] as T[]);
});
}
/**
* Get all entries in the store. Each entry is an array of `[key, value]`.
*
* @param customStore Method to get a custom store. Use with caution (see the docs).
*/
export function entries<KeyType extends IDBValidKey, ValueType = any>(
customStore = defaultGetStore(),
): Promise<[KeyType, ValueType][]> {
return withFailureFallback(customStore("readonly", store => {
if (isMemoryStore(store)) return store.entries<KeyType, ValueType>();
return customStore("readonly", store => {
// Fast path for modern browsers
// (although, hopefully we'll get a simpler path some day)
if (store.getAll && store.getAllKeys) {
return Promise.all([
promisifyRequest(
store.getAllKeys() as unknown as IDBRequest<KeyType[]>,
),
promisifyRequest(store.getAll() as IDBRequest<ValueType[]>),
]).then(([keys, values]) => keys.map((key, i) => [key, values[i]] as [KeyType, ValueType]));
]).then(([keys, values]) => keys.map((key, i) => [key, values[i]]));
}
const items: [KeyType, ValueType][] = [];
return eachCursor(store, cursor =>
items.push([cursor.key as KeyType, cursor.value]),
).then(() => items);
}), [] as [KeyType, ValueType][]);
return customStore("readonly", store =>
eachCursor(store, cursor =>
items.push([cursor.key as KeyType, cursor.value]),
).then(() => items),
);
});
}

View file

@ -28,6 +28,7 @@ import { addMessagePopoverButton, removeMessagePopoverButton } from "@api/Messag
import { addNicknameIcon, removeNicknameIcon } from "@api/NicknameIcons";
import { Settings, SettingsStore } from "@api/Settings";
import { disableStyle, enableStyle } from "@api/Styles";
import { traceFunction } from "@debug/Tracer";
import { Logger } from "@utils/Logger";
import { onlyOnce } from "@utils/onlyOnce";
import { canonicalizeFind, canonicalizeReplacement } from "@utils/patches";
@ -39,7 +40,6 @@ import { patches } from "@webpack/patcher";
import Plugins from "~plugins";
export { Plugins as plugins };
import { traceFunction } from "../debug/Tracer";
import { addAudioProcessor, removeAudioProcessor } from "./AudioPlayer";
import { addChannelToolbarButton, addHeaderBarButton, removeChannelToolbarButton, removeHeaderBarButton } from "./HeaderBar";
import { addProfileCollection, removeProfileCollection } from "./ProfileCollections";

View file

@ -16,13 +16,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { React, TextInput } from "@webpack/common";
import { React, TextInput, useState } from "@webpack/common";
interface TextInputProps {
/**
* WARNING: Changing this between renders will have no effect!
*/
value: string;
initialValue: string;
/**
* This will only be called if the new value passed validate()
*/
@ -41,13 +41,15 @@ interface TextInputProps {
* A very simple wrapper around Discord's TextInput that validates input and shows
* the user an error message and only calls your onChange when the input is valid
*/
export function CheckedTextInput({ value: initialValue, onChange, validate, placeholder }: TextInputProps) {
const [value, setValue] = React.useState(initialValue);
const [error, setError] = React.useState<string>();
export function CheckedTextInput({ initialValue, onChange, validate, placeholder }: TextInputProps) {
const [value, setValue] = useState(initialValue);
const [error, setError] = useState<string>();
function handleChange(v: string) {
setValue(v);
const res = validate(v);
if (res === true) {
setError(void 0);
onChange(v);

View file

@ -9,6 +9,7 @@ import "./colorStyles.css";
import { classNameFactory } from "@utils/css";
import { findByCodeLazy } from "@webpack";
import { ColorPicker, useMemo } from "@webpack/common";
import type tinycolor from "tinycolor2";
// TinyColor is completely unmangled and it's duplicated in two modules! Fun!
const TinyColor: tinycolor.Constructor = findByCodeLazy("this._gradientType=");

View file

@ -6,13 +6,13 @@
import { showNotice } from "@api/Notices";
import { isPluginEnabled, pluginRequiresRestart, startDependenciesRecursive, startPlugin, stopPlugin } from "@api/PluginManager";
import { Settings } from "@api/Settings";
import { CogWheel, InfoIcon } from "@components/Icons";
import { AddonCard } from "@components/settings/AddonCard";
import { classNameFactory } from "@utils/css";
import { Logger } from "@utils/Logger";
import { OptionType, Plugin } from "@utils/types";
import { React, showToast, Toasts } from "@webpack/common";
import { Settings } from "Vencord";
import { PluginMeta } from "~plugins";

View file

@ -60,5 +60,25 @@
}
.vc-plugin-modal-info {
display: flex;
gap: 1em;
justify-content: space-between;
align-items: start;
}
.vc-plugin-modal-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-block: 8px 16px;
}
.vc-plugin-modal-tag {
align-items: center;
}
border: 1px solid var(--border-normal);
border-radius: 9999px;
display: flex;
justify-content: center;
padding: 6px 12px;
color: var(--text-default);
}

View file

@ -32,7 +32,7 @@ import { proxyLazy } from "@utils/lazy";
import { Margins } from "@utils/margins";
import { classes, isObjectEmpty } from "@utils/misc";
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { OptionType, Plugin } from "@utils/types";
import { OptionType, Plugin, PluginTag } from "@utils/types";
import { User } from "@vencord/discord-types";
import { findComponentByCodeLazy, findCssClassesLazy } from "@webpack";
import { Clickable, FluxDispatcher, React, Toasts, Tooltip, useEffect, useMemo, UserStore, UserSummaryItem, UserUtils, useState } from "@webpack/common";
@ -74,6 +74,16 @@ export function makeDummyUser(user: { username: string; id?: string; avatar?: st
return newUser;
}
function PluginTags({ tags }: { tags: PluginTag[]; }) {
return (
<div className={cl("tags")}>
{tags.map(tag => (
<div key={tag} className={cl("tag")}>{tag}</div>
))}
</div>
);
}
export default function PluginModal({ plugin, onRestartNeeded, onClose, transitionState }: PluginModalProps) {
const pluginSettings = useSettings([`plugins.${plugin.name}.*`]).plugins[plugin.name];
const hasSettings = Boolean(pluginSettings && plugin.options && !isObjectEmpty(plugin.options));
@ -167,6 +177,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
<div className={cl("header-content")}>
<BaseText size="lg" weight="semibold" className={cl("title")}>{plugin.name}</BaseText>
<BaseText size="sm" className={cl("description")}>{plugin.description}</BaseText>
{!!plugin.tags?.length && <PluginTags tags={plugin.tags} />}
{!!plugin.settingsAboutComponent && (
<div className={Margins.top8}>
<ErrorBoundary message="An error occurred while rendering this plugin's custom Info Component">

View file

@ -37,7 +37,8 @@ import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { useAwaiter, useIntersection } from "@utils/react";
import { t } from "@utils/translation";
import { Alerts, lodash, Parser, React, Select, TextInput, Toasts, Tooltip, useCallback, useMemo, useState } from "@webpack/common";
import { PluginTag, PluginTags } from "@utils/types";
import { Alerts, lodash, Parser, React, SearchableSelect, Select, TextInput, Toasts, Tooltip, useCallback, useMemo, useRef, useState } from "@webpack/common";
import { JSX } from "react";
import Plugins, { ExcludedPlugins, PluginMeta } from "~plugins";
@ -144,7 +145,8 @@ function ExcludedPluginsList({ search }: { search: string; }) {
export default function PluginSettings() {
const settings = useSettings();
const changes = React.useMemo(() => new ChangeList<string>(), []);
const changeRef = useRef<ChangeList<string>>(null);
const changes = changeRef.current ??= new ChangeList<string>();
React.useEffect(() => {
return () => {
@ -195,25 +197,13 @@ export default function PluginSettings() {
const hasUserPlugins = useMemo(() => !IS_STANDALONE && Object.values(PluginMeta).some(m => m.userPlugin), []);
const [searchValue, setSearchValue] = useState({ value: "", status: SearchStatus.ALL });
const [searchInput, setSearchInput] = useState("");
const debouncedSetSearch = useMemo(
() => debounce((query: string) => setSearchValue(prev => ({ ...prev, value: query })), 150),
[]
);
const [searchValue, setSearchValue] = useState({ value: "", tags: [] as PluginTag[], status: SearchStatus.ALL });
const search = searchValue.value.toLowerCase();
const onSearch = useCallback((query: string) => {
setSearchInput(query);
debouncedSetSearch(query);
}, [debouncedSetSearch]);
const onStatusChange = useCallback((status: SearchStatus) => {
setSearchValue(prev => ({ ...prev, status }));
}, []);
const onSearch = (query: string) => setSearchValue(prev => ({ ...prev, value: query }));
const pluginFilter = useCallback((plugin: typeof Plugins[keyof typeof Plugins], newPluginsSet: Set<string> | null) => {
const { status } = searchValue;
const { status, tags } = searchValue;
const enabled = isPluginEnabled(plugin.name);
switch (status) {
@ -240,12 +230,14 @@ export default function PluginSettings() {
break;
}
if (tags.length && tags.some(t => !plugin.tags?.includes(t))) return false;
if (!search.length) return true;
return (
plugin.name.toLowerCase().includes(search.replace(/\s+/g, "")) ||
plugin.description.toLowerCase().includes(search) ||
plugin.tags?.some(t => t.toLowerCase().includes(search))
plugin.searchTerms?.some(t => t.toLowerCase().includes(search))
);
}, [searchValue, search]);
@ -407,31 +399,45 @@ export default function PluginSettings() {
Filters
</HeadingTertiary>
<div className={classes(Margins.bottom20, cl("filter-controls"))}>
<ErrorBoundary noop>
<TextInput autoFocus value={searchInput} placeholder="Search for a plugin..." onChange={onSearch} />
</ErrorBoundary>
<div>
<ErrorBoundary noop>
<Select
options={[
{ label: "Show All", value: SearchStatus.ALL, default: true },
{ label: "Show Enabled", value: SearchStatus.ENABLED },
{ label: "Show Disabled", value: SearchStatus.DISABLED },
{ label: "Show Equicord", value: SearchStatus.EQUICORD },
{ label: "Show Vencord", value: SearchStatus.VENCORD },
{ label: "Show New", value: SearchStatus.NEW },
hasUserPlugins && { label: "Show UserPlugins", value: SearchStatus.USER_PLUGINS },
{ label: "Show API Plugins", value: SearchStatus.API_PLUGINS },
].filter(isTruthy)}
serialize={String}
select={onStatusChange}
isSelected={v => v === searchValue.status}
closeOnSelect={true}
/>
</ErrorBoundary>
<ErrorBoundary noop>
<TextInput
inputClassName={cl("filter-control")}
placeholder="Search for a plugin..."
value={searchValue.value}
onChange={onSearch}
autoFocus
/>
</ErrorBoundary>
<ErrorBoundary noop>
<div className={classes(Margins.bottom20, Margins.top8, cl("filter-controls"))}>
<Select
options={[
{ label: "Show All", value: SearchStatus.ALL, default: true },
{ label: "Show Enabled", value: SearchStatus.ENABLED },
{ label: "Show Disabled", value: SearchStatus.DISABLED },
{ label: "Show Equicord", value: SearchStatus.EQUICORD },
{ label: "Show Vencord", value: SearchStatus.VENCORD },
{ label: "Show New", value: SearchStatus.NEW },
hasUserPlugins && { label: "Show UserPlugins", value: SearchStatus.USER_PLUGINS },
{ label: "Show API Plugins", value: SearchStatus.API_PLUGINS },
].filter(isTruthy)}
serialize={String}
select={status => setSearchValue(prev => ({ ...prev, status }))}
isSelected={v => v === searchValue.status}
closeOnSelect={true}
placeholder="Filter by Type"
/>
<SearchableSelect
options={PluginTags.map(tag => ({ label: tag, value: tag }))}
value={searchValue.tags}
onChange={tags => setSearchValue(prev => ({ ...prev, tags }))}
closeOnSelect={false}
placeholder="Filter by Tags"
multi
/>
</div>
</div>
</ErrorBoundary>
<HeadingTertiary className={Margins.top20}>Plugins</HeadingTertiary>
@ -457,6 +463,7 @@ export default function PluginSettings() {
<HeadingTertiary className={classes(Margins.top20, Margins.bottom8)}>
Required Plugins
</HeadingTertiary>
<div className={cl("grid")}>
{requiredPlugins.length
? requiredPlugins

View file

@ -45,7 +45,12 @@
display: grid;
height: 40px;
gap: 10px;
grid-template-columns: 1fr 150px;
grid-template-columns: 1fr 1fr;
align-items: center;
}
.vc-plugins-filter-control {
height: 40px;
}
.vc-plugins-dep-name {

View file

@ -137,7 +137,7 @@ function CloudTab() {
<div style={{ flex: 1 }}>
<CheckedTextInput
key={`backendUrl-${inputKey}`}
value={cloud.url}
initialValue={cloud.url}
onChange={async v => {
cloud.url = v;
cloud.authenticated = false;

View file

@ -20,6 +20,7 @@ import { Paragraph } from "@components/Paragraph";
import { openContributorModal, openPluginModal, SettingsTab, wrapTab } from "@components/settings";
import { QuickAction, QuickActionCard } from "@components/settings/QuickAction";
import { SpecialCard } from "@components/settings/SpecialCard";
import BadgeAPI from "@plugins/_api/badges";
import { gitRemote } from "@shared/vencordUserAgent";
import { DONOR_ROLE_ID, GUILD_ID, IS_MAC, IS_WINDOWS, VC_DONOR_ROLE_ID, VC_GUILD_ID } from "@utils/constants";
import { classNameFactory } from "@utils/css";
@ -27,8 +28,7 @@ import { Margins } from "@utils/margins";
import { identity, isAnyPluginDev } from "@utils/misc";
import { relaunch } from "@utils/native";
import { t } from "@utils/translation";
import { GuildMemberStore, React, Select, UserStore } from "@webpack/common";
import BadgeAPI from "plugins/_api/badges";
import { Alerts, GuildMemberStore, React, Select, UserStore } from "@webpack/common";
import { DonateButtonComponent } from "./DonateButton";
import { openNotificationSettingsModal } from "./NotificationSettings";
@ -68,28 +68,28 @@ function EquicordSettings() {
warning: { enabled: boolean; message?: string; };
}
> = [
{
key: "useQuickCss",
title: t("vencord.settings.useQuickCss.title"),
description: t("vencord.settings.useQuickCss.description"),
restartRequired: true,
warning: { enabled: false },
},
!IS_WEB && {
key: "enableReactDevtools",
title: t("vencord.settings.enableReactDevtools.title"),
description: t("vencord.settings.enableReactDevtools.description"),
restartRequired: true,
warning: { enabled: false },
},
(!IS_WEB && !IS_DISCORD_DESKTOP || !IS_WINDOWS) && {
key: "mainWindowFrameless",
title: t("equicord.mainWindowFrameless.title"),
description: t("equicord.mainWindowFrameless.description"),
restartRequired: true,
warning: { enabled: false },
},
!IS_WEB &&
{
key: "useQuickCss",
title: t("vencord.settings.useQuickCss.title"),
description: t("vencord.settings.useQuickCss.description"),
restartRequired: true,
warning: { enabled: false },
},
!IS_WEB && {
key: "enableReactDevtools",
title: t("vencord.settings.enableReactDevtools.title"),
description: t("vencord.settings.enableReactDevtools.description"),
restartRequired: true,
warning: { enabled: false },
},
(!IS_WEB && !IS_DISCORD_DESKTOP || !IS_WINDOWS) && {
key: "mainWindowFrameless",
title: t("equicord.mainWindowFrameless.title"),
description: t("equicord.mainWindowFrameless.description"),
restartRequired: true,
warning: { enabled: false },
},
!IS_WEB &&
(!IS_DISCORD_DESKTOP || !IS_WINDOWS
? {
key: "frameless",
@ -106,34 +106,34 @@ function EquicordSettings() {
warning: { enabled: false },
}
),
!IS_WEB && {
key: "transparent",
title: t("vencord.settings.transparent.title"),
description: t("vencord.settings.transparent.description"),
restartRequired: true,
warning: {
enabled: true,
message: IS_WINDOWS
? t("vencord.settings.transparent.noteWindows")
: t("vencord.settings.transparent.note"),
},
!IS_WEB && {
key: "transparent",
title: t("vencord.settings.transparent.title"),
description: t("vencord.settings.transparent.description"),
restartRequired: true,
warning: {
enabled: true,
message: IS_WINDOWS
? t("vencord.settings.transparent.noteWindows")
: t("vencord.settings.transparent.note"),
},
IS_DISCORD_DESKTOP && {
key: "disableMinSize",
title: t("vencord.settings.disableMinSize.title"),
description: t("vencord.settings.disableMinSize.description"),
restartRequired: true,
warning: { enabled: false },
},
!IS_WEB &&
},
IS_DISCORD_DESKTOP && {
key: "disableMinSize",
title: t("vencord.settings.disableMinSize.title"),
description: t("vencord.settings.disableMinSize.description"),
restartRequired: true,
warning: { enabled: false },
},
!IS_WEB &&
IS_WINDOWS && {
key: "winCtrlQ",
title: t("vencord.settings.winCtrlQ.title"),
description: t("vencord.settings.winCtrlQ.description"),
restartRequired: true,
warning: { enabled: false },
},
];
key: "winCtrlQ",
title: t("vencord.settings.winCtrlQ.title"),
description: t("vencord.settings.winCtrlQ.description"),
restartRequired: true,
warning: { enabled: false },
},
];
return (
<SettingsTab>
@ -250,7 +250,19 @@ function EquicordSettings() {
<FormSwitch
key={s.key}
value={settings[s.key]}
onChange={v => (settings[s.key] = v)}
onChange={v => {
settings[s.key] = v;
if (s.restartRequired) {
Alerts.show({
title: "Restart Required",
body: "A restart is required to apply this change",
confirmText: "Restart now",
cancelText: "Later!",
onConfirm: relaunch
});
}
}}
title={s.title}
description={
s.warning.enabled ? (

View file

@ -9,8 +9,8 @@ import { canonicalizeMatch } from "@utils/patches";
import { ModuleFactory } from "@vencord/discord-types/webpack";
import * as Webpack from "@webpack";
import { wreq } from "@webpack";
import { AnyModuleFactory } from "@webpack/types";
import pLimit from "p-limit";
import { AnyModuleFactory } from "webpack";
function getWebpackChunkMap() {
const sym = Symbol();

View file

@ -26,6 +26,7 @@ export interface ReporterData {
};
failedWebpack: Record<TypeWebpackSearchHistory, string[][]>;
}
export const reporterData: ReporterData = {
failedPatches: {
foundNoModule: [],

View file

@ -23,8 +23,8 @@ export default definePlugin({
find: ".DISPLAY_NAME_STYLES_COACHMARK)",
replacement: [
{
match: /(?<=className:(\i)\.\i,style:\i,)children:\[/,
replace: "children:[...$self.renderButtons(arguments[0],$1),"
match: /children:\[(?=.{0,50}accountContainerRef:\i)/,
replace: "children:[...$self.renderButtons(arguments[0]),"
},
// fix discord weird shrink with extra buttons
{

View file

@ -11,6 +11,7 @@ import definePlugin from "@utils/types";
export default definePlugin({
name: "AltKrispSwitch",
description: t("equicord.altKrispSwitch.description"),
tags: ["Customisation", "Voice"],
authors: [Devs.newwares],
patches: [
{

View file

@ -12,6 +12,7 @@ import definePlugin from "@utils/types";
export default definePlugin({
name: "AlwaysExpandProfiles",
description: t("equicord.alwaysExpandProfiles.description"),
tags: ["Appearance", "Utility"],
authors: [Devs.thororen],
patches: [
{

View file

@ -194,6 +194,7 @@ async function playSound(buffer: AudioBuffer, volume: number) {
export default definePlugin({
name: "Animalese",
description: t("equicord.animalese.description"),
tags: ["Customisation", "Fun"],
authors: [EquicordDevs.ryanamay, EquicordDevs.Mocha],
settings,

View file

@ -14,6 +14,7 @@ export default definePlugin({
name: "AtSomeone",
authors: [Devs.Joona],
description: t("equicord.atSomeone.description"),
tags: ["Chat", "Fun"],
patches: [
{
find: ".LAUNCHABLE_APPLICATIONS;",

View file

@ -210,6 +210,7 @@ function handlePaste(event: ClipboardEvent) {
export default definePlugin({
name: "AutoZipper",
description: t("equicord.autoZipper.description"),
tags: ["Chat", "Organisation"],
authors: [EquicordDevs.SSnowly],
settings,

View file

@ -45,6 +45,7 @@ const DATASTORE_KEY = "bannersEverywhere";
export default definePlugin({
name: "BannersEverywhere",
description: t("equicord.bannersEverywhere.description"),
tags: ["Appearance", "Customisation"],
authors: [Devs.ImLvna, Devs.AutumnVN],
settings,
patches: [

View file

@ -108,6 +108,8 @@ const settings = definePluginSettings({
export default definePlugin({
name: "DecodeBase64",
description: t("equicord.baseDecoder.description"),
dependencies: ["MessagePopoverAPI"],
tags: ["Appearance", "Customisation", "Chat"],
authors: [EquicordDevs.ThePirateStoner],
settings,
messagePopoverButton: {

View file

@ -21,7 +21,7 @@ export default definePlugin({
name: "BetterActivities",
description: t("equicord.betterActivities.description"),
authors: [Devs.D3SOX, Devs.Arjix, Devs.AutumnVN, Devs.thororen],
tags: ["activity"],
tags: ["Activity"],
settings,
patchActivityList,
showAllActivitiesComponent,
@ -46,7 +46,7 @@ export default definePlugin({
// Show all activities in the user popout/sidebar
find: /onOpenUserProfileModal:\i,onClose:\i\}\),null/,
replacement: {
match: /((\i)=.{0,10}(\i)\.id\).*?,onInteraction:\i\}\),).{0,250}onClose:\i\}\)/,
match: /((\i)=.{0,10}(\i)\.id\).*?,onOpenUserProfileModal:\i\}\),).{0,250}onClose:\i\}\)/,
replace: "$1$self.showAllActivitiesComponent({ activity: $2, user: $3 })"
},
predicate: () => settings.store.userPopout

View file

@ -275,6 +275,7 @@ const settings = definePluginSettings({
export default definePlugin({
name: "BetterAudioPlayer",
description: t("equicord.betterAudioPlayer.description"),
tags: ["Appearance", "Media", "Voice"],
authors: [EquicordDevs.creations],
settings,

View file

@ -75,6 +75,7 @@ const settings = definePluginSettings({
export default definePlugin({
name: "BetterBanReasons",
description: t("equicord.betterBanReasons.description"),
tags: ["Appearance", "Customisation"],
authors: [Devs.Inbestigator],
patches: [
{

View file

@ -18,6 +18,7 @@ let updateFunc = (v: any) => { };
export default definePlugin({
name: "BetterBlockedUsers",
description: t("equicord.betterBlockedUsers.description"),
tags: ["Appearance", "Shortcuts"],
authors: [EquicordDevs.TheArmagan],
patches: [
{

View file

@ -34,6 +34,8 @@ function fetchIndex(target: object) {
export default definePlugin({
name: "BetterCommands",
description: t("equicord.betterCommands.description"),
dependencies: ["CommandsAPI"],
tags: ["Appearance", "Commands", "Shortcuts"],
authors: [Devs.thororen],
settings,
patches: [

View file

@ -35,6 +35,7 @@ function lurk(id: string) {
export default definePlugin({
name: "BetterInvites",
description: t("equicord.betterInvites.description"),
tags: ["Appearance", "Customisation", "Chat", "Servers"],
authors: [EquicordDevs.iamme, Devs.thororen],
patches: [
{

View file

@ -13,6 +13,7 @@ export default definePlugin({
name: "BetterPlusReacts",
authors: [Devs.Joona],
description: t("equicord.betterPlusReacts.description"),
tags: ["Chat", "Emotes"],
patches: [
{
find: ".SLASH_COMMAND_USED,",

View file

@ -71,6 +71,7 @@ export function containsBlockedKeywords(message: Message) {
export default definePlugin({
name: "BlockKeywords",
description: t("equicord.blockKeywords.description"),
tags: ["Appearance", "Customisation", "Privacy"],
authors: [EquicordDevs.catcraft],
patches: [
{

View file

@ -11,6 +11,7 @@ import definePlugin from "@utils/types";
export default definePlugin({
name: "BlockKrisp",
description: t("equicord.blockKrisp.description"),
tags: ["Privacy", "Utility", "Voice"],
authors: [Devs.D3SOX],
patches: [
// Block loading modules on Desktop

View file

@ -11,6 +11,7 @@ import definePlugin from "@utils/types";
export default definePlugin({
name: "BypassPinPrompt",
description: t("equicord.bypassPinPrompt.description"),
tags: ["Shortcuts"],
authors: [Devs.thororen],
patches: [
...[

View file

@ -158,6 +158,7 @@ const settings = definePluginSettings({
export default definePlugin({
name: "BypassStatus",
description: t("equicord.bypassStatus.description"),
tags: ["Activity", "Customisation", "Notifications", "Servers"],
authors: [Devs.Inbestigator],
dependencies: ["AudioPlayerAPI"],
flux: {

View file

@ -33,6 +33,7 @@ function renderBadge(id: number, title: string) {
export default definePlugin({
name: "ChannelBadges",
description: t("equicord.channelBadges.description"),
tags: ["Appearance", "Customisation", "Servers"],
authors: [EquicordDevs.creations, Devs.thororen],
settings,
patches: [

View file

@ -16,8 +16,8 @@ import { ChannelStore, Menu } from "@webpack/common";
import { JSX } from "react";
import ChannelsTabsContainer from "./components/ChannelTabsContainer";
import { BasicChannelTabsProps, createTab, handleChannelSwitch, settings } from "./util";
import * as ChannelTabsUtils from "./util";
import { BasicChannelTabsProps, createTab, handleChannelSwitch, settings } from "./util";
const contextMenuPatch: NavContextMenuPatchCallback = (children, props: { channel: Channel, messageId?: string; }) => {
const { channel, messageId } = props;
@ -48,6 +48,7 @@ const contextMenuPatch: NavContextMenuPatchCallback = (children, props: { channe
export default definePlugin({
name: "ChannelTabs",
description: t("equicord.channelTabs.description"),
tags: ["Appearance", "Customisation", "Organisation", "Servers"],
authors: [Devs.TheSun, Devs.TheKodeToad, EquicordDevs.keifufu, Devs.Nickyux, EquicordDevs.DiabeloDEV, EquicordDevs.justjxke],
dependencies: ["ContextMenuAPI"],
contextMenus: {

View file

@ -13,6 +13,7 @@ export default definePlugin({
name: "CleanChannelName",
authors: [Devs.AutumnVN],
description: t("equicord.cleanChannelName.description"),
tags: ["Appearance", "Customisation", "Chat", "Emotes", "Servers"],
patches: [
{
find: "loadAllGuildAndPrivateChannelsFromDisk(){",

View file

@ -11,6 +11,7 @@ import definePlugin from "@utils/types";
export default definePlugin({
name: "CleanerChannelGroups",
description: t("equicord.cleanerChannelGroups.description"),
tags: ["Appearance", "Customisation", "Chat", "Organisation", "Servers"],
authors: [EquicordDevs.justjxke],
patches: [
{

View file

@ -159,6 +159,7 @@ const WrappedClickableRole = ErrorBoundary.wrap(ClickableRole, { noop: true });
export default definePlugin({
name: "ClickableRoles",
description: t("equicord.clickableRoles.description"),
tags: ["Appearance", "Roles"],
authors: [Devs.prism],
patches: [

View file

@ -143,7 +143,7 @@ function activeNowView(cards) {
export default definePlugin({
name: "ClientSideBlock",
description: t("equicord.clientSideBlock.description"),
tags: ["blocked", "block", "hide", "hidden", "noblockedmessages"],
tags: ["Utility"],
authors: [Devs.Samwich, EquicordDevs.KamiRu],
settings,
activeNowView,

View file

@ -73,6 +73,7 @@ migratePluginSettings("ClipsEnhancements", "TimelessClips");
export default definePlugin({
name: "ClipsEnhancements",
description: t("equicord.clipsEnhancements.description"),
tags: ["Activity", "Media", "Utility"],
authors: [Devs.niko, Devs.Joona, EquicordDevs.keircn],
settings,
patches: [

View file

@ -0,0 +1,150 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2026 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { ChannelToolbarButton } from "@api/HeaderBar";
import ErrorBoundary from "@components/ErrorBoundary";
import { EquicordDevs } from "@utils/constants";
import { classNameFactory } from "@utils/css";
import { classes } from "@utils/misc";
import definePlugin from "@utils/types";
import { ContextMenuApi, Menu } from "@webpack/common";
import type { ReactNode, SVGProps } from "react";
import { PanelId, panelRegistry, settings, toolbarPanelOrder } from "./settings";
import managedStyle from "./style.css?managed";
const cl = classNameFactory("vc-collapsible-ui-");
type StateStore = ReturnType<typeof settings.use>;
function PanelsIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 24 24" fill="none" {...props}>
<path fill="currentColor" d="M3 5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v3H3V5Zm0 5h6v11H5a2 2 0 0 1-2-2V10Zm8 0h10v9a2 2 0 0 1-2 2H11V10Zm2-5h8v3h-8V5Z" />
</svg>
);
}
function getStateClasses(store: StateStore) {
const classNames = [cl("state"), cl(`transition-${store.transitionMs}`)];
for (const panelId of toolbarPanelOrder) {
const panel = panelRegistry[panelId];
if (store[panel.collapsedKey]) classNames.push(cl(`${panel.classId}-collapsed`));
}
return classes(...classNames);
}
function togglePanel(panelId: PanelId) {
const key = panelRegistry[panelId].collapsedKey;
settings.store[key] = !settings.store[key];
}
const ToolbarMenu = ErrorBoundary.wrap(({ onClose }: { onClose(): void; }) => {
const store = settings.use();
return (
<Menu.Menu navId="vc-collapsible-ui-toolbar-menu" onClose={onClose} aria-label="Collapsible UI">
{toolbarPanelOrder.map(panelId => {
const panel = panelRegistry[panelId];
const collapsed = store[panel.collapsedKey];
return (
<Menu.MenuCheckboxItem
key={panelId}
id={`vc-collapsible-ui-${panel.classId}`}
label={panel.label}
checked={!collapsed}
action={() => togglePanel(panelId)}
/>
);
})}
</Menu.Menu>
);
}, { noop: true });
const ToolbarButtons = ErrorBoundary.wrap(() => {
const store = settings.use();
const anyCollapsed = toolbarPanelOrder.some(panelId => store[panelRegistry[panelId].collapsedKey]);
return (
<>
<span aria-hidden className={getStateClasses(store)} style={{ display: "none" }} />
<ChannelToolbarButton
icon={PanelsIcon}
tooltip="Collapsible UI"
aria-label="Collapsible UI"
selected={anyCollapsed}
onClick={event => ContextMenuApi.openContextMenu(event, () => <ToolbarMenu onClose={ContextMenuApi.closeContextMenu} />)}
onContextMenu={event => ContextMenuApi.openContextMenu(event, () => <ToolbarMenu onClose={ContextMenuApi.closeContextMenu} />)}
/>
</>
);
}, { noop: true });
const ChatButtonsRow = ErrorBoundary.wrap(({ buttons }: { buttons: ReactNode[]; }) => {
const { chatButtonsCollapsed } = settings.use();
if (buttons.length === 0) return <>{buttons}</>;
return (
<div
className={classes(
cl("chat-buttons"),
chatButtonsCollapsed && cl("chat-buttons-collapsed")
)}
>
<div className={cl("chat-buttons-items")}>
{buttons}
</div>
</div>
);
}, { noop: true });
export default definePlugin({
name: "CollapsibleUI",
description: "Native collapsible channel, member, chat button, and user area surfaces.",
tags: ["Appearance", "Customisation", "Chat", "Servers"],
dependencies: ["HeaderBarAPI"],
authors: [EquicordDevs.benjii],
searchTerms: ["ui", "sidebar", "layout"],
managedStyle,
settings,
headerBarButton: {
icon: PanelsIcon,
location: "channeltoolbar",
priority: 25,
render: () => <ToolbarButtons />,
},
patches: [
{
find: ".DISPLAY_NAME_STYLES_COACHMARK)",
replacement: {
match: /className:(\i\.\i),(?=style:(\i),children:\[)/,
replace: "className:$self.userAreaControlsClass($1),"
}
},
{
find: '"sticker")',
replacement: {
match: /(?<="div",\{.{0,15}children:)(.+?)\}/,
replace: "$self.wrapChatButtons($1)}"
}
}
],
wrapChatButtons(buttons: ReactNode[]) {
if (!Array.isArray(buttons) || buttons.length === 0) return buttons;
return <ChatButtonsRow buttons={buttons} />;
},
userAreaControlsClass(className: string) {
return classes(className, cl("user-area-controls"));
},
});

View file

@ -0,0 +1,70 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2026 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import { OptionType } from "@utils/types";
export const transitionMarkers = [0, 80, 120, 160, 200, 260, 320];
export const panelRegistry = {
channelList: {
id: "channelList",
classId: "channel-list",
label: "Channel List",
collapsedKey: "channelListCollapsed",
},
membersList: {
id: "membersList",
classId: "members-list",
label: "Members List",
collapsedKey: "membersListCollapsed",
},
chatButtons: {
id: "chatButtons",
classId: "chat-buttons",
label: "Message Buttons",
collapsedKey: "chatButtonsCollapsed",
},
userArea: {
id: "userArea",
classId: "user-area",
label: "User Area",
collapsedKey: "userAreaCollapsed",
},
} as const;
export type PanelId = keyof typeof panelRegistry;
export const toolbarPanelOrder = ["channelList", "membersList", "chatButtons", "userArea"] as const satisfies readonly PanelId[];
export const settings = definePluginSettings({
transitionMs: {
type: OptionType.SLIDER,
description: "Panel transition speed in milliseconds.",
default: 160,
markers: transitionMarkers,
},
channelListCollapsed: {
type: OptionType.BOOLEAN,
description: "Persist the channel list as collapsed.",
default: false,
},
membersListCollapsed: {
type: OptionType.BOOLEAN,
description: "Persist the members list as collapsed.",
default: false,
},
chatButtonsCollapsed: {
type: OptionType.BOOLEAN,
description: "Persist the message button row as collapsed.",
default: false,
},
userAreaCollapsed: {
type: OptionType.BOOLEAN,
description: "Persist the user area as collapsed.",
default: false,
},
});

View file

@ -0,0 +1,213 @@
body:has(.vc-collapsible-ui-state) {
--vc-cui-chat-buttons-width: min(320px, 36vw);
--vc-cui-collapsed-size: 12px;
--vc-cui-hover-peek: 18px;
--vc-cui-hover-delay: 120ms;
--vc-cui-user-area-collapsed-width: 52px;
--vc-cui-transition: 160ms;
--vc-cui-ease: cubic-bezier(.2, 0, 0, 1);
}
body:has(.vc-collapsible-ui-transition-0) {
--vc-cui-transition: 0ms;
}
body:has(.vc-collapsible-ui-transition-80) {
--vc-cui-transition: 80ms;
}
body:has(.vc-collapsible-ui-transition-120) {
--vc-cui-transition: 120ms;
}
body:has(.vc-collapsible-ui-transition-160) {
--vc-cui-transition: 160ms;
}
body:has(.vc-collapsible-ui-transition-200) {
--vc-cui-transition: 200ms;
}
body:has(.vc-collapsible-ui-transition-260) {
--vc-cui-transition: 260ms;
}
body:has(.vc-collapsible-ui-transition-320) {
--vc-cui-transition: 320ms;
}
body:has(.vc-collapsible-ui-channel-list-collapsed):has([class*="sidebarList_"]:hover) [class*="sidebarList_"] {
transition-delay: var(--vc-cui-hover-delay);
}
body:has(.vc-collapsible-ui-channel-list-collapsed):has([class*="sidebarList_"]:hover) [class*="sidebarList_"] > * {
opacity: 1;
pointer-events: auto;
transition: opacity var(--vc-cui-transition) var(--vc-cui-ease);
transition-delay: var(--vc-cui-hover-delay);
}
body:has(.vc-collapsible-ui-channel-list-collapsed):not(:has([class*="sidebarList_"]:hover), :has([class*="sidebar_"] > [class*="panels_"]:hover)) [class*="sidebarList_"] {
width: var(--vc-cui-collapsed-size) !important;
inline-size: var(--vc-cui-collapsed-size) !important;
min-width: var(--vc-cui-collapsed-size) !important;
min-inline-size: var(--vc-cui-collapsed-size) !important;
max-width: var(--vc-cui-collapsed-size) !important;
max-inline-size: var(--vc-cui-collapsed-size) !important;
flex: 0 0 var(--vc-cui-collapsed-size) !important;
overflow: hidden !important;
transition:
width var(--vc-cui-transition) var(--vc-cui-ease),
inline-size var(--vc-cui-transition) var(--vc-cui-ease),
min-width var(--vc-cui-transition) var(--vc-cui-ease),
min-inline-size var(--vc-cui-transition) var(--vc-cui-ease),
max-width var(--vc-cui-transition) var(--vc-cui-ease),
max-inline-size var(--vc-cui-transition) var(--vc-cui-ease),
flex-basis var(--vc-cui-transition) var(--vc-cui-ease) !important;
}
body:has(.vc-collapsible-ui-channel-list-collapsed):not(:has([class*="sidebarList_"]:hover), :has([class*="sidebar_"] > [class*="panels_"]:hover)) [class*="sidebarList_"] > * {
opacity: 0;
pointer-events: none;
transition: opacity var(--vc-cui-transition) var(--vc-cui-ease);
}
body:has(.vc-collapsible-ui-channel-list-collapsed):not(:has([class*="sidebar_"] > [class*="panels_"]:hover)) [class*="sidebar_"] > [class*="panels_"] {
width: var(--vc-cui-user-area-collapsed-width) !important;
inline-size: var(--vc-cui-user-area-collapsed-width) !important;
min-width: var(--vc-cui-user-area-collapsed-width) !important;
min-inline-size: var(--vc-cui-user-area-collapsed-width) !important;
max-width: var(--vc-cui-user-area-collapsed-width) !important;
max-inline-size: var(--vc-cui-user-area-collapsed-width) !important;
overflow: hidden !important;
transition:
width var(--vc-cui-transition) var(--vc-cui-ease),
inline-size var(--vc-cui-transition) var(--vc-cui-ease),
min-width var(--vc-cui-transition) var(--vc-cui-ease),
min-inline-size var(--vc-cui-transition) var(--vc-cui-ease),
max-width var(--vc-cui-transition) var(--vc-cui-ease),
max-inline-size var(--vc-cui-transition) var(--vc-cui-ease) !important;
}
body:has(.vc-collapsible-ui-channel-list-collapsed):not(:has([class*="sidebar_"] > [class*="panels_"]:hover)) .vc-collapsible-ui-user-area-controls {
max-width: 0 !important;
min-width: 0 !important;
overflow: hidden !important;
opacity: 0;
pointer-events: none;
transition:
max-width var(--vc-cui-transition) var(--vc-cui-ease),
opacity var(--vc-cui-transition) var(--vc-cui-ease) !important;
}
body:has(.vc-collapsible-ui-channel-list-collapsed):has([class*="sidebar_"] > [class*="panels_"]:hover) .vc-collapsible-ui-user-area-controls {
opacity: 1;
pointer-events: auto;
}
body:has(.vc-collapsible-ui-members-list-collapsed) [class*="membersWrap"] {
overflow: hidden !important;
transition:
width var(--vc-cui-transition) var(--vc-cui-ease),
min-width var(--vc-cui-transition) var(--vc-cui-ease),
max-width var(--vc-cui-transition) var(--vc-cui-ease) !important;
}
body:has(.vc-collapsible-ui-members-list-collapsed):has([class*="membersWrap"]:hover) [class*="membersWrap"],
body:has(.vc-collapsible-ui-members-list-collapsed):has([class*="membersWrap"]:focus-within) [class*="membersWrap"] {
transition-delay: var(--vc-cui-hover-delay);
}
body:has(.vc-collapsible-ui-members-list-collapsed):has([class*="membersWrap"]:hover) [class*="membersWrap"] > :where(*),
body:has(.vc-collapsible-ui-members-list-collapsed):has([class*="membersWrap"]:focus-within) [class*="membersWrap"] > :where(*) {
opacity: 1;
pointer-events: auto;
transition: opacity var(--vc-cui-transition) var(--vc-cui-ease);
transition-delay: var(--vc-cui-hover-delay);
}
body:has(.vc-collapsible-ui-members-list-collapsed):not(:has([class*="membersWrap"]:hover), :has([class*="membersWrap"]:focus-within)) [class*="membersWrap"] {
width: var(--vc-cui-collapsed-size) !important;
min-width: var(--vc-cui-collapsed-size) !important;
max-width: var(--vc-cui-collapsed-size) !important;
}
body:has(.vc-collapsible-ui-members-list-collapsed):not(:has([class*="membersWrap"]:hover), :has([class*="membersWrap"]:focus-within)) [class*="membersWrap"] > :where(*) {
opacity: 0;
pointer-events: none;
transition: opacity var(--vc-cui-transition) var(--vc-cui-ease);
}
body:has(.vc-collapsible-ui-user-area-collapsed):has([class*="sidebar_"] > [class*="panels_"]:hover) [class*="sidebar_"] > [class*="panels_"] > :where(*),
body:has(.vc-collapsible-ui-user-area-collapsed):has([class*="sidebar_"] > [class*="panels_"]:focus-within) [class*="sidebar_"] > [class*="panels_"] > :where(*) {
opacity: 1;
pointer-events: auto;
transition: opacity var(--vc-cui-transition) var(--vc-cui-ease);
}
body:has(.vc-collapsible-ui-user-area-collapsed):not(:has([class*="sidebar_"] > [class*="panels_"]:hover), :has([class*="sidebar_"] > [class*="panels_"]:focus-within)) [class*="sidebar_"] > [class*="panels_"] {
height: var(--vc-cui-hover-peek) !important;
block-size: var(--vc-cui-hover-peek) !important;
min-height: var(--vc-cui-hover-peek) !important;
min-block-size: var(--vc-cui-hover-peek) !important;
max-height: var(--vc-cui-hover-peek) !important;
max-block-size: var(--vc-cui-hover-peek) !important;
overflow: hidden !important;
border-top-width: 0 !important;
padding-block: 0 !important;
transition:
height var(--vc-cui-transition) var(--vc-cui-ease),
block-size var(--vc-cui-transition) var(--vc-cui-ease),
min-height var(--vc-cui-transition) var(--vc-cui-ease),
min-block-size var(--vc-cui-transition) var(--vc-cui-ease),
max-height var(--vc-cui-transition) var(--vc-cui-ease),
max-block-size var(--vc-cui-transition) var(--vc-cui-ease),
padding var(--vc-cui-transition) var(--vc-cui-ease) !important;
}
body:has(.vc-collapsible-ui-user-area-collapsed):not(:has([class*="sidebar_"] > [class*="panels_"]:hover), :has([class*="sidebar_"] > [class*="panels_"]:focus-within)) [class*="sidebar_"] > [class*="panels_"] > :where(*) {
opacity: 0;
pointer-events: none;
transition: opacity var(--vc-cui-transition) var(--vc-cui-ease);
}
.vc-collapsible-ui-chat-buttons {
display: flex;
align-items: center;
min-inline-size: 0;
}
.vc-collapsible-ui-chat-buttons-collapsed {
min-inline-size: var(--vc-cui-hover-peek);
}
.vc-collapsible-ui-chat-buttons-items {
display: flex;
align-items: center;
min-inline-size: 0;
max-inline-size: var(--vc-cui-chat-buttons-width);
overflow: hidden;
transition:
max-inline-size var(--vc-cui-transition) var(--vc-cui-ease),
opacity var(--vc-cui-transition) var(--vc-cui-ease),
margin var(--vc-cui-transition) var(--vc-cui-ease);
}
.vc-collapsible-ui-chat-buttons-collapsed .vc-collapsible-ui-chat-buttons-items {
max-inline-size: 0;
opacity: 0;
pointer-events: none;
}
.vc-collapsible-ui-chat-buttons-collapsed:hover .vc-collapsible-ui-chat-buttons-items {
max-inline-size: var(--vc-cui-chat-buttons-width);
opacity: 1;
pointer-events: auto;
transition-delay: var(--vc-cui-hover-delay);
}
@media (prefers-reduced-motion: reduce) {
body:has(.vc-collapsible-ui-state) {
--vc-cui-transition: 0ms;
}
}

View file

@ -203,6 +203,7 @@ export default definePlugin({
name: "CommandPalette",
description: t("equicord.commandPalette.description"),
authors: [EquicordDevs.justjxke, EquicordDevs.Ethan],
tags: ["Appearance", "Customisation", "Commands", "Shortcuts"],
settings,
patches: [
{

View file

@ -6,7 +6,7 @@
import { DataStore } from "@api/index";
import { isPluginEnabled, plugins, startPlugin, stopPlugin } from "@api/PluginManager";
import { SettingsStore } from "@api/Settings";
import { Settings, SettingsStore } from "@api/Settings";
import { getUserSettingLazy } from "@api/UserSettings";
import { openPluginModal } from "@components/settings/tabs";
import { toggleEnabled } from "@equicordplugins/equicordHelper/utils";
@ -18,7 +18,6 @@ import { Guild } from "@vencord/discord-types";
import { findByPropsLazy, findExportedComponentLazy, findStoreLazy } from "@webpack";
import { Alerts, ChannelActionCreators, ChannelRouter, ChannelStore, ComponentDispatch, FluxDispatcher, GuildStore, IconUtils, MediaEngineStore, MessageStore, NavigationRouter, React, ReadStateStore, ReadStateUtils, SelectedChannelStore, SelectedGuildStore, SettingsRouter, StreamerModeStore, Toasts, useEffect, UserStore, VoiceActions } from "@webpack/common";
import type { FC, ReactElement, ReactNode } from "react";
import { Settings } from "Vencord";
import commandPalette from ".";
import {

View file

@ -19,6 +19,7 @@ import ComponentsTab from "./components/ComponentsTab";
export default definePlugin({
name: "Components",
description: t("equicord.components.description"),
tags: ["Appearance", "Customisation", "Console", "Developers", "Organisation"],
authors: [Devs.prism],
dependencies: ["Settings"],
startAt: StartAt.WebpackReady,

View file

@ -153,6 +153,7 @@ export default definePlugin({
name: "ContentWarning",
authors: [EquicordDevs.camila314],
description: t("equicord.contentWarning.description"),
tags: ["Appearance", "Utility"],
settings,
patches: [
{

View file

@ -91,6 +91,7 @@ const userContextMenuPatch: NavContextMenuPatchCallback = (children, { user }) =
export default definePlugin({
name: "CopyProfileColors",
description: t("equicord.copyProfileColors.description"),
tags: ["Appearance", "Customisation"],
authors: [EquicordDevs.Crxa, EquicordDevs.Cortex],
contextMenus: {
"user-context": userContextMenuPatch,

View file

@ -24,6 +24,7 @@ const getMetadataFromApi: (activity: any, userId: string) => Promise<any> = find
export default definePlugin({
name: "CopyStatusUrls",
description: t("equicord.copyStatusUrls.description"),
tags: ["Activity", "Utility"],
authors: [Devs.sadan],
patches: [

View file

@ -49,6 +49,7 @@ export default definePlugin({
name: "CopyUserMention",
authors: [EquicordDevs.Cortex, Devs.castdrian],
description: t("equicord.copyUserMention.description"),
tags: ["Chat", "Utility"],
contextMenus: {
"user-context": UserContextMenuPatch
}

View file

@ -236,8 +236,9 @@ migratePluginSettings("CursorBuddy", "Oneko", "oneko");
export default definePlugin({
name: "CursorBuddy",
description: t("equicord.cursorBuddy.description"),
tags: ["Appearance", "Customisation", "Fun"],
authors: [Devs.Ven, Devs.adryd, EquicordDevs.nexpid, EquicordDevs.ZcraftElite],
tags: ["Oneko", "FatassHorse", "Pet"],
searchTerms: ["Oneko", "FatassHorse", "Pet"],
settings,
isModified: true,

View file

@ -15,6 +15,7 @@ import { folderProp, int2rgba } from "./util";
export default definePlugin({
name: "CustomFolderIcons",
description: t("equicord.customFolderIcons.description"),
tags: ["Appearance", "Customisation", "Organisation"],
authors: [EquicordDevs.sadan],
settings,
patches: [

View file

@ -394,6 +394,8 @@ export function findOverride(id: string): SoundOverride | null {
export default definePlugin({
name: "CustomSounds",
description: t("equicord.customSounds.description"),
dependencies: ["AudioPlayerAPI"],
tags: ["Customisation", "Notifications", "Voice"],
authors: [Devs.ScattrdBlade, Devs.TheKodeToad],
settings,
startAt: StartAt.Init,

View file

@ -0,0 +1,116 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2026 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";
const Millis = {
HALF_SECOND: 500,
SECOND: 1e3,
MINUTE: 6e4,
HOUR: 36e5,
DAY: 864e5,
WEEK: 6048e5,
DAYS_30: 2592e6
};
const settings = definePluginSettings({
showForeverOnTop: {
type: OptionType.BOOLEAN,
description: "Show the Forever option at the top of the list instead of the bottom.",
restartNeeded: true,
default: true
},
extraSeconds: {
type: OptionType.STRING,
description: "Extra seconds to add, separated by a comma (e.g. 5, 10, 30)",
restartNeeded: true,
default: "15, 30, 45"
},
extraMinutes: {
type: OptionType.STRING,
description: "Extra minutes to add, separated by a comma (e.g. 5, 10, 30)",
restartNeeded: true,
default: "5, 10, 30"
},
extraHours: {
type: OptionType.STRING,
description: "Extra hours to add, separated by a comma (e.g. 2, 4, 6, 12)",
restartNeeded: true,
default: "2, 4, 6, 12"
},
extraDays: {
type: OptionType.STRING,
description: "Extra days to add, separated by a comma (e.g. 1, 2)",
restartNeeded: true,
default: "1, 2"
},
});
export default definePlugin({
name: "CustomStatusTimeouts",
description: "Adds configurable timeout presets to the status (presence) menu.",
tags: ["Activity", "Utility"],
authors: [EquicordDevs.Kiri, EquicordDevs.thororen],
settings,
patches: [
{
find: "#{intl::DURATION_FOREVER}",
replacement: {
match: /\[\{duration.*?#{intl::DURATION_FOREVER}\)\}\]/,
replace: "$self.buildTimeouts($&)"
}
}
],
buildTimeouts(existing) {
const parse = (str: string) => str.split(",").map(s => Number(s.trim())).filter(Boolean);
const seconds = parse(settings.store.extraSeconds);
const minutes = parse(settings.store.extraMinutes);
const hours = parse(settings.store.extraHours);
const days = parse(settings.store.extraDays);
// I was testing weeks and months and for some reason random ones didnt apply look below for the working ones
// tested weeks between 1-12 working weeks were 1, 2, 3, 8, 9, 10 the rest didnt work
// tested months between 1-12 working months were 2, 4, 10, 12 the rest didnt work
// for months I only tested evens except for 1 so I hardcoded them to avoid user complaints
const weeks = [1, 2, 3];
const months = [2, 4];
const extra = [
...seconds.map(s => ({
duration: s * Millis.SECOND,
label: () => `For ${s} ${s === 1 ? "Second" : "Seconds"}`
})),
...minutes.map(m => ({
duration: m * Millis.MINUTE,
label: () => `For ${m} ${m === 1 ? "Minute" : "Minutes"}`
})),
...hours.map(h => ({
duration: h * Millis.HOUR,
label: () => `For ${h} ${h === 1 ? "Hour" : "Hours"}`
})),
...days.map(d => ({
duration: d * Millis.DAY,
label: () => `For ${d} ${d === 1 ? "Day" : "Days"}`
})),
...weeks.map(w => ({
duration: w * Millis.WEEK,
label: () => `For ${w} ${w === 1 ? "Week" : "Weeks"}`
})),
...months.map(m => ({
duration: m * Millis.DAYS_30,
label: () => `For ${m} ${m === 1 ? "Month" : "Months"}`
})),
];
return [...existing, ...extra].sort((a, b) => {
if (a.duration === undefined) return settings.store.showForeverOnTop ? -1 : 1;
if (b.duration === undefined) return settings.store.showForeverOnTop ? 1 : -1;
return a.duration - b.duration;
});
}
});

View file

@ -126,6 +126,7 @@ const settings = definePluginSettings({
export default definePlugin({
name: "CustomTimestamps",
description: t("equicord.customTimestamps.description"),
tags: ["Appearance", "Customisation"],
authors: [Devs.Rini, EquicordDevs.nvhhr, EquicordDevs.Suffocate, Devs.Obsidian],
settings,
settingsAboutComponent: () => (

View file

@ -101,6 +101,7 @@ const settings = definePluginSettings({
export default definePlugin({
name: "CustomUserColors",
description: t("equicord.customUserColors.description"),
tags: ["Appearance", "Customisation", "Chat"],
authors: [EquicordDevs.mochienya],
contextMenus: {
"user-context": userContextMenuPatch,

View file

@ -144,15 +144,16 @@ function SectionSeparator(title: string) {
export default definePlugin({
name: "Declutter",
description: t("equicord.declutter.description"),
tags: ["Appearance", "Customisation"],
authors: [EquicordDevs.Leon135, Devs.prism, Devs.Kyuuhachi],
settings,
patches: [
{
// Nameplate
find: "#{intl::AVATAR_MALLOW}",
find: ".MINI_PREVIEW,[",
replacement: {
match: /function \i\(\i\)\{(?=.{0,25}skuId:)/,
replace: "$&return null;"
match: /function \i\((\i)\)\{(?=let\{nameplate:\i,hovered:\i,selected:\i,content:\i,placement:\i\}=)/,
replace: '$&if($1.placement!=="preview"&&$1.placement!=="mini_preview")return null;'
},
predicate: () => settings.store.removeNameplate,
},
@ -160,8 +161,8 @@ export default definePlugin({
// Profile banner animation effect
find: "bannerAdjustment,isHovering",
replacement: {
match: /\i=\i=>\{(?=.{0,50}\.useReducedMotion\))/,
replace: "$&return null;"
match: /\i=(\i)=>\{(?=.{0,50}\.useReducedMotion\))/,
replace: "$&if(!$1.shopPreview)return null;"
},
predicate: () => settings.store.removeProfileEffect,
},

View file

@ -11,6 +11,7 @@ import definePlugin from "@utils/types";
export default definePlugin({
name: "DisableCameras",
description: t("equicord.disableCameras.description"),
tags: ["Appearance", "Customisation", "Media", "Privacy"],
authors: [Devs.Joona],
patches: [
{

View file

@ -4,10 +4,10 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import SettingsPlugin from "@plugins/_core/settings";
import { detectClient } from "@plugins/_core/supportHelper";
import { gitHashShort } from "@shared/vencordUserAgent";
import { React } from "@webpack/common";
import SettingsPlugin from "plugins/_core/settings";
import { JSX } from "react";
import { ChromiumIcon, ClientIcon, DevBannerIcon, DiscordIcon, ElectronIcon, EquicordIcon, names, settings } from ".";

View file

@ -15,6 +15,7 @@ import { makeDevBanner, settings } from "./components";
export default definePlugin({
name: "DiscordDevBanner",
description: t("equicord.discordDevBanner.description"),
tags: ["Appearance", "Console", "Developers"],
authors: [EquicordDevs.KrystalSkull, Devs.thororen],
settings,
patches: [

View file

@ -34,6 +34,7 @@ export default definePlugin({
name: "DragFavoriteEmotes",
authors: [EquicordDevs.PWall],
description: t("equicord.dragFavoriteEmotes.description"),
tags: ["Customisation", "Chat", "Emotes"],
patches: [
{
find: "#{intl::EMOJI_FAVORITE_TOOLTIP}",

View file

@ -150,13 +150,14 @@ const settings = definePluginSettings({
export default definePlugin({
name: "Dragify",
description: t("equicord.dragify.description"),
tags: ["Chat", "Servers", "Utility", "Voice"],
authors: [EquicordDevs.justjxke],
settings,
patches: [
// Voice user rows (voice channel sidebar list)
{
find: '"system:click_outside","user:escape"',
find: ".VOICE_USER],shouldShow:",
replacement: {
match: /(?<=GuildChannelUserContextMenu.{0,150})"data-dnd-name":\i/,
replace: "\"data-dragify-user\":!0,\"data-user-id\":arguments[0].user?.id,draggable:!0,onDragStart:e=>$self.onUserDragStart(e,arguments[0].user),$&"

View file

@ -450,6 +450,7 @@ function onToggle(e: KeyboardEvent) {
export default definePlugin({
name: "ElementHighlighter",
description: t("equicord.elementHighlighter.description"),
tags: ["Developers"],
authors: [Devs.prism],
settings,

View file

@ -89,6 +89,7 @@ const settings = definePluginSettings({
export default definePlugin({
name: "EquibopStreamFixes",
description: "Tries to fix stream quality on Equibop by patching Discord's encoder and quality restrictions.",
tags: ["Voice"],
authors: [EquicordDevs.creations],
settings,

View file

@ -8,7 +8,7 @@ import { ApplicationCommandInputType, sendBotMessage } from "@api/Commands";
import { HeaderBarButton } from "@api/HeaderBar";
import { addMessagePreSendListener, removeMessagePreSendListener } from "@api/MessageEvents";
import { isPluginEnabled } from "@api/PluginManager";
import { definePluginSettings, migratePluginToSettings } from "@api/Settings";
import { definePluginSettings, migratePluginToSettings, Settings } from "@api/Settings";
import customRPC from "@plugins/customRPC";
import { Devs, EquicordDevs, GUILD_ID, SUPPORT_CHANNEL_ID, SUPPORT_CHANNEL_IDS, VC_SUPPORT_CHANNEL_IDS } from "@utils/constants";
import { isAnyPluginDev } from "@utils/misc";
@ -126,14 +126,22 @@ const settings = definePluginSettings({
disableAdoptTagPrompt: {
type: OptionType.BOOLEAN,
description: "Disable the prompt to adopt tags",
default: true,
restartNeeded: true
restartNeeded: true,
default: false,
},
jsonGateway: {
type: OptionType.BOOLEAN,
description: "Forces JSON on gateway reconnect",
restartNeeded: true,
default: false,
}
});
export default definePlugin({
name: "EquicordHelper",
description: t("equicord.equicordHelper.description"),
dependencies: ["CommandsAPI", "HeaderBarAPI", "MessageAccessoriesAPI"],
tags: ["Appearance", "Commands", "Utility"],
authors: [
Devs.thororen,
EquicordDevs.nyx,
@ -273,6 +281,36 @@ export default definePlugin({
replace: "return null;$&"
},
predicate: () => settings.store.disableAdoptTagPrompt
},
{
find: "JSONEncoding",
replacement: {
match: /void 0!==\i\?\i:/,
replace: ""
},
predicate: () => settings.store.jsonGateway
},
{
find: ".USE_OSX_NATIVE_TRAFFIC_LIGHTS",
predicate: () => Settings.winNativeTitleBar,
replacement: {
match: /case \i\.\i\.WINDOWS:/,
replace: 'case "WEB":'
},
},
{
find: '"refresh-title-bar-small"',
predicate: () => Settings.winNativeTitleBar,
replacement: [
{
match: /\i===\i\.PlatformTypes\.WINDOWS/g,
replace: "false"
},
{
match: /\i===\i\.PlatformTypes\.WEB/g,
replace: "true"
}
]
}
],
renderMessageAccessory(props) {

View file

@ -78,11 +78,10 @@ migratePluginSettings("EquicordToolbox", "VencordToolbox");
export default definePlugin({
name: "EquicordToolbox",
description: t("equicord.equicordToolbox.description"),
tags: ["Voice", "Accessibility"],
authors: [Devs.Ven, Devs.AutumnVN],
dependencies: ["HeaderBarAPI"],
settings,
headerBarButton: {
icon: Icon,
render: VencordPopoutButton,

View file

@ -34,6 +34,7 @@ const settings = definePluginSettings({
export default definePlugin({
name: "Equissant",
description: t("equicord.equissant.description"),
tags: ["Fun"],
authors: [EquicordDevs.SomeAspy, Devs.thororen],
dependencies: ["AudioPlayerAPI"],
settings,

View file

@ -105,6 +105,7 @@ let original: typeof ChannelActions.selectVoiceChannel;
export default definePlugin({
name: "ExitSounds",
description: t("equicord.exitSounds.description"),
tags: ["Fun", "Voice"],
authors: [Devs.prism],
dependencies: ["AudioPlayerAPI"],
settings,

View file

@ -130,6 +130,7 @@ function getUsernames(contacts: ContactsList[], type: number): string[] {
export default definePlugin({
name: "ExportMessages",
description: t("equicord.exportMessages.description"),
tags: ["Chat", "Utility"],
authors: [EquicordDevs.veygax, EquicordDevs.dat_insanity],
settings,
contextMenus: {

View file

@ -75,6 +75,7 @@ const settings = definePluginSettings({
export default definePlugin({
name: "FastDeleteChannels",
description: t("equicord.fastDeleteChannels.description"),
tags: ["Servers", "Utility"],
authors: [Devs.thororen],
settings,
patches: [

View file

@ -26,8 +26,9 @@ export const AttachmentContext = proxyLazyWebpack(() => React.createContext<null
export default definePlugin({
name: "FavouriteAnything",
description: t("equicord.favouriteAnything.description"),
tags: ["Chat", "Media"],
authors: [Devs.nin0dev, EquicordDevs.davri],
tags: ["favorite"],
searchTerms: ["favorite"],
managedStyle,
patches: [
// EMBEDS

View file

@ -44,7 +44,7 @@ interface Uint8ArrayConstructorExtended extends Uint8ArrayConstructor {
): Uint8ArrayExtended<ArrayBuffer>;
}
function supportsToBase64(array: Uint8Array<ArrayBufferLike>): array is Uint8ArrayExtended<ArrayBufferLike> {
function supportsToBase64(array: Uint8Array<ArrayBufferLike>): boolean {
return "toBase64" in array && typeof array.toBase64 === "function";
}

View file

@ -217,6 +217,7 @@ const channelAttachMenuPatch: NavContextMenuPatchCallback = (children, props) =>
export default definePlugin({
name: "FileUpload",
description: t("equicord.fileUpload.description"),
tags: ["Media"],
authors: [EquicordDevs.creations, EquicordDevs.keircn, Devs.ScattrdBlade],
settings,
patches: [

View file

@ -209,6 +209,12 @@ export const settings = definePluginSettings({
default: false,
hidden: true
},
preserveOriginalFilename: {
type: OptionType.BOOLEAN,
description: "Preserve the original filename when uploading.",
default: true,
hidden: true
},
autoCopy: {
type: OptionType.BOOLEAN,
description: "Auto copy upload URL",
@ -508,6 +514,13 @@ export function SettingsComponent() {
/>
</SettingsSection>
<SettingsSection tag="label" name="Preserve Original Filename" description="Use the original filename instead of naming uploads as upload.ext" inlineSetting>
<Switch
checked={Boolean((store as { preserveOriginalFilename?: boolean; }).preserveOriginalFilename)}
onChange={v => (store as { preserveOriginalFilename?: boolean; }).preserveOriginalFilename = v}
/>
</SettingsSection>
<SettingsSection tag="label" name="Auto Copy URL" description="Automatically copy the uploaded file URL to clipboard" inlineSetting>
<Switch
checked={store.autoCopy}

View file

@ -880,9 +880,24 @@ async function normalizeUploadBlob(blob: Blob, sourceUrl?: string): Promise<{ bl
}
const mimeType = getMimeFromExtension(ext);
const { preserveOriginalFilename } = settings.store;
let sourceFileName = "";
if (blob instanceof File && blob.name) {
sourceFileName = blob.name;
} else if (sourceUrl && URL.canParse(sourceUrl)) {
const segment = new URL(sourceUrl).pathname.split("/").pop();
if (segment) sourceFileName = decodeURIComponent(segment);
}
let filename = `upload.${ext}`;
if (preserveOriginalFilename && sourceFileName) {
const dotIndex = sourceFileName.lastIndexOf(".");
filename = `${sourceFileName.slice(0, dotIndex < 0 ? undefined : dotIndex)}.${ext}`;
}
return {
blob: new Blob([blob], { type: mimeType }),
filename: `upload.${ext}`
filename
};
}

View file

@ -90,6 +90,8 @@ const settings = definePluginSettings({
export default definePlugin({
name: "FindReply",
description: t("equicord.findReply.description"),
dependencies: ["MessagePopoverAPI"],
tags: ["Chat", "Shortcuts"],
authors: [Devs.newwares],
settings,
messagePopoverButton: {

View file

@ -30,6 +30,7 @@ export default definePlugin({
name: "FixFileExtensions",
authors: [Devs.thororen],
description: t("equicord.fixFileExtensions.description"),
tags: ["Media", "Utility"],
reporterTestable: ReporterTestable.None,
patches: [
// Taken from AnonymiseFileNames

View file

@ -75,6 +75,7 @@ const UserContextMenuPatch: NavContextMenuPatchCallback = (children, { channel,
export default definePlugin({
name: "FollowVoiceUser",
description: t("equicord.followVoiceUser.description"),
tags: ["Voice"],
authors: [EquicordDevs.TheArmagan],
settings,
settingsAboutComponent: () => (

View file

@ -201,6 +201,7 @@ const settings = definePluginSettings({
export default definePlugin({
name: "FontLoader",
description: t("equicord.fontLoader.description"),
tags: ["Appearance", "Customisation"],
authors: [EquicordDevs.vmohammad],
settings,

View file

@ -26,6 +26,7 @@ const settings = definePluginSettings({
export default definePlugin({
name: "ForwardAnywhere",
description: t("equicord.forwardAnywhere.description"),
tags: ["Chat", "Utility"],
authors: [Devs.thororen],
settings,
patches: [

View file

@ -38,6 +38,7 @@ function generateSearchResults(query) {
export default definePlugin({
name: "FrequentQuickSwitcher",
description: t("equicord.frequentQuickSwitcher.description"),
tags: ["Shortcuts", "Servers"],
authors: [Devs.Samwich],
generateSearchResults: generateSearchResults,
patches: [

View file

@ -12,6 +12,7 @@ import FriendCodesPanel from "./FriendCodesPanel";
export default definePlugin({
name: "FriendCodes",
description: "Generate FriendCodes to easily add friends",
tags: ["Friends", "Utility"],
authors: [Devs.HypedDomi],
patches: [
{

View file

@ -210,6 +210,7 @@ const userPatch: NavContextMenuPatchCallback = (children, { user }) => {
export default definePlugin({
name: "FriendTags",
description: t("equicord.friendTags.description"),
tags: ["Shortcuts"],
authors: [Devs.Samwich],
settings,
contextMenus: {

View file

@ -144,6 +144,7 @@ function getBadgesToApply() {
export default definePlugin({
name: "FriendshipRanks",
description: t("equicord.friendshipRanks.description"),
tags: ["Friends"],
authors: [Devs.Samwich],
start() {
getBadgesToApply().forEach(b => Badges.addProfileBadge(b));

View file

@ -15,6 +15,7 @@ import style from "./style.css?managed";
export default definePlugin({
name: "FullVCPFP",
description: t("equicord.fullVcPfp.description"),
tags: ["Appearance", "Voice"],
authors: [EquicordDevs.mochienya],
patches: [
{

View file

@ -148,6 +148,7 @@ function makeContextItem(props) {
export default definePlugin({
name: "Ghosted",
description: t("equicord.ghosted.description"),
tags: ["Chat", "Utility"],
authors: [EquicordDevs.vei, Devs.sadan, EquicordDevs.justjxke, EquicordDevs.iamme],
settings,
dependencies: ["AudioPlayerAPI", "ServerListAPI"],

View file

@ -7,6 +7,7 @@
import "./style.css";
import { Devs, EquicordDevs } from "@utils/constants";
import { t } from "@utils/translation";
import definePlugin from "@utils/types";
import { FluxDispatcher } from "@webpack/common";
@ -17,7 +18,6 @@ import { cache_collections, refreshCacheCollection, updateGif } from "./utils/co
import { getFormat } from "./utils/getFormat";
import { logger, stripPrefix } from "./utils/misc";
import { batchRefreshAttachmentUrls, isCdnUrlExpired } from "./utils/refreshUrl";
import { t } from "@utils/translation";
let GIF_COLLECTION_PREFIX: string;
let GIF_ITEM_PREFIX: string;
@ -28,6 +28,7 @@ export default definePlugin({
name: "GifCollections",
description: t("equicord.gifCollections.description"),
authors: [Devs.Aria, EquicordDevs.creations],
tags: ["Chat", "Emotes"],
settings,
contextMenus: {
"message": addCollectionContextMenuPatch,
@ -158,7 +159,7 @@ export default definePlugin({
oldTrendingCat = instance.props.trendingCategories;
}
if (settings.store.onlyShowCollections) {
instance.props.trendingCategories = [...this.sortedCollections(), ...instance.props.favorites];
instance.props.trendingCategories = this.sortedCollections();
} else if (oldTrendingCat != null) {
instance.props.trendingCategories = [...this.sortedCollections(), ...oldTrendingCat];
}

View file

@ -39,6 +39,8 @@ export const settings = definePluginSettings({
export default definePlugin({
name: "GitHubRepos",
description: t("equicord.githubRepos.description"),
dependencies: ["ProfileCollectionsAPI"],
tags: ["Appearance"],
authors: [EquicordDevs.talhakf, EquicordDevs.Panniku, EquicordDevs.benjii],
settings,

View file

@ -26,6 +26,7 @@ migratePluginSetting("GlobalBadges", "showRaincord", "showRa1ncord");
export default definePlugin({
name: "GlobalBadges",
description: t("equicord.globalBadges.description"),
tags: ["Appearance"],
authors: [Devs.HypedDomi, EquicordDevs.Wolfie, Devs.thororen],
settings,
settingsAboutComponent: () => (

Some files were not shown because too many files have changed in this diff Show more