Merge branch 'dev' into bun
This commit is contained in:
commit
86528e30f7
696 changed files with 17261 additions and 20613 deletions
5
.github/workflows/build.yml
vendored
5
.github/workflows/build.yml
vendored
|
|
@ -55,17 +55,20 @@ jobs:
|
|||
rm release/package.json
|
||||
rm release/*.map
|
||||
- name: Upload Equicord Stable
|
||||
if: ${{ !vars.ACT }}
|
||||
run: |
|
||||
gh release upload latest --clobber dist/release/*
|
||||
|
||||
- name: Upload Plugin JSONs to Equibored repo
|
||||
if: github.repository == 'Equicord/Equicord' && !vars.ACT
|
||||
run: |
|
||||
git config --global user.name "GitHub-Actions"
|
||||
git config --global user.email actions@github.com
|
||||
git clone https://$USERNAME:$GITHUB_TOKEN@github.com/$REPO.git plugins
|
||||
cd plugins
|
||||
|
||||
cp ../dist/release/*.json .
|
||||
cp ../dist/release/*plugins.json .
|
||||
cp ../dist/release/devs.json .
|
||||
git add -A
|
||||
git commit -m "Plugins for https://github.com/$GITHUB_REPOSITORY/commit/$GITHUB_SHA"
|
||||
git push --force https://$USERNAME:$GITHUB_TOKEN@github.com/$REPO.git
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
name: Build
|
||||
name: Build Branches
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
- "!pr-**"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
|
|
@ -28,6 +29,7 @@ jobs:
|
|||
Build:
|
||||
name: Build Equicord
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'Equicord/Equicord'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
|
@ -37,11 +39,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 +66,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
|
||||
|
|
@ -90,6 +91,7 @@ jobs:
|
|||
bun run generateDevsList dist/devs.json
|
||||
|
||||
- name: Upload to Equibuilds repo
|
||||
if: ${{ !vars.ACT }}
|
||||
run: |
|
||||
BRANCH="${{ github.event.inputs.branch || github.ref_name }}"
|
||||
COMMIT_SHA=$(git rev-parse --short HEAD)
|
||||
3
.github/workflows/nixosBuild.yml
vendored
3
.github/workflows/nixosBuild.yml
vendored
|
|
@ -41,7 +41,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
|
||||
cp desktop/* release
|
||||
|
|
@ -56,6 +56,7 @@ jobs:
|
|||
id: date
|
||||
run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||
- name: Upload Equicord Stable
|
||||
if: ${{ !vars.ACT }}
|
||||
run: |
|
||||
gh release create ${{ steps.date.outputs.date }} --latest=false
|
||||
gh release upload ${{ steps.date.outputs.date }} --clobber dist/release/*
|
||||
|
|
|
|||
12
.github/workflows/publish.yml
vendored
12
.github/workflows/publish.yml
vendored
|
|
@ -74,12 +74,16 @@ jobs:
|
|||
|
||||
- name: Create sources zip
|
||||
if: steps.check.outputs.release == 'true'
|
||||
run: zip -r dist/extension-sources.zip . -x "node_modules/*" -x "dist/*" -x ".git/*"
|
||||
run: zip -r dist/extension-sources.zip . -x "node_modules/*" -x "dist/*" -x
|
||||
".git/*"
|
||||
|
||||
- name: Publish extension
|
||||
if: steps.check.outputs.release == 'true'
|
||||
run: bunx @equicord/publish-browser-extension@latest --chrome-zip dist/extension-chrome.zip --firefox-zip dist/extension-firefox.zip --firefox-sources-zip dist/extension-sources.zip --edge-zip dist/extension-chrome.zip
|
||||
|
||||
if: steps.check.outputs.release == 'true' && !vars.ACT
|
||||
run: bunx @equicord/publish-browser-extension@latest --chrome-zip
|
||||
dist/extension-chrome.zip --firefox-zip
|
||||
dist/extension-firefox.zip --firefox-sources-zip
|
||||
dist/extension-sources.zip --edge-zip
|
||||
dist/extension-chrome.zip
|
||||
env:
|
||||
CHROME_EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }}
|
||||
CHROME_PUBLISHER_ID: ${{ secrets.CHROME_PUBLISHER_ID }}
|
||||
|
|
|
|||
4
.github/workflows/reportBrokenPlugins.yml
vendored
4
.github/workflows/reportBrokenPlugins.yml
vendored
|
|
@ -12,7 +12,9 @@ on:
|
|||
default: both
|
||||
webhook_url:
|
||||
type: string
|
||||
description: "Webhook URL that the report will be posted to. This will be visible for everyone, so DO NOT pass sensitive webhooks like discord webhook. This is meant to be used by Venbot."
|
||||
description: "Webhook URL that the report will be posted to. This will be
|
||||
visible for everyone, so DO NOT pass sensitive webhooks like
|
||||
discord webhook. This is meant to be used by Venbot."
|
||||
required: false
|
||||
schedule:
|
||||
# # Every day at midnight
|
||||
|
|
|
|||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -1,6 +1,9 @@
|
|||
dist
|
||||
node_modules
|
||||
.VSCodeCounter
|
||||
.vars
|
||||
.secrets
|
||||
.input
|
||||
|
||||
*.exe
|
||||
vencord_installer
|
||||
|
|
|
|||
|
|
@ -136,6 +136,8 @@ export default defineConfig(
|
|||
"use-isnan": "error",
|
||||
"prefer-const": ["error", { destructuring: "all" }],
|
||||
"prefer-spread": "error",
|
||||
// These are old deprecated browser globals which may be used by mistake, e.g. `addEventListener(e => console.log(event))`
|
||||
"no-restricted-globals": ["error", "event", "name"],
|
||||
|
||||
// Plugin Rules
|
||||
"simple-import-sort/imports": "error",
|
||||
|
|
|
|||
50
package.json
50
package.json
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "equicord",
|
||||
"private": "true",
|
||||
"version": "1.14.6.2",
|
||||
"version": "1.14.12.0",
|
||||
"description": "The other cutest Discord client mod",
|
||||
"homepage": "https://github.com/Equicord/Equicord#readme",
|
||||
"bugs": {
|
||||
|
|
@ -59,47 +59,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",
|
||||
"@electron/asar": "^4.2.0",
|
||||
"@stylistic/eslint-plugin": "^5.10.0",
|
||||
"@types/bun": "^1.3.9",
|
||||
"@types/chrome": "^0.1.30",
|
||||
"@types/diff": "^7.0.2",
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@types/node": "^24.10.1",
|
||||
"@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": "workspace:*",
|
||||
"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",
|
||||
"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",
|
||||
"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": "bun@1.3.9",
|
||||
|
|
|
|||
20
packages/discord-types/src/components.d.ts
vendored
20
packages/discord-types/src/components.d.ts
vendored
|
|
@ -331,6 +331,15 @@ declare enum PopoutAnimation {
|
|||
|
||||
type PopoutPosition = "top" | "bottom" | "left" | "right" | "center" | "window_center";
|
||||
|
||||
export interface PopoutProps {
|
||||
position: PopoutPosition;
|
||||
nudge: number;
|
||||
isPositioned: boolean;
|
||||
setPopoutRef(ref: any): void;
|
||||
closePopout(): void;
|
||||
updatePosition(): void;
|
||||
}
|
||||
|
||||
export type Popout = ComponentType<{
|
||||
children(
|
||||
thing: {
|
||||
|
|
@ -347,14 +356,8 @@ export type Popout = ComponentType<{
|
|||
): ReactNode;
|
||||
shouldShow?: boolean;
|
||||
targetElementRef: RefObject<any>;
|
||||
renderPopout(args: {
|
||||
closePopout(): void;
|
||||
isPositioned: boolean;
|
||||
nudge: number;
|
||||
position: PopoutPosition;
|
||||
setPopoutRef(ref: any): void;
|
||||
updatePosition(): void;
|
||||
}): ReactNode;
|
||||
renderPopout(props: PopoutProps): ReactNode;
|
||||
preload?(): Promise<any>;
|
||||
|
||||
onRequestOpen?(): void;
|
||||
onRequestClose?(): void;
|
||||
|
|
@ -536,4 +539,5 @@ export type ColorPicker = ComponentType<{
|
|||
suggestedColors?: string[];
|
||||
label?: ReactNode;
|
||||
onChange(value: number | null): void;
|
||||
disabled?: boolean;
|
||||
}>;
|
||||
|
|
|
|||
|
|
@ -78,7 +78,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;
|
||||
|
|
|
|||
|
|
@ -138,7 +138,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/_api", "equicordplugins/_core", "equicordplugins", "userplugins"];
|
||||
let code = "";
|
||||
let pluginsCode = "\n";
|
||||
let metaCode = "\n";
|
||||
|
|
|
|||
|
|
@ -31,11 +31,11 @@ import { getEntryPoint, isPluginFile, parseDevs, parseEquicordDevs, parseFile, P
|
|||
let dirs: string[];
|
||||
|
||||
if (equicordFlag) {
|
||||
dirs = ["src/equicordplugins"];
|
||||
dirs = ["src/equicordplugins/_core", "src/equicordplugins"];
|
||||
} else if (vencordFlag) {
|
||||
dirs = ["src/plugins", "src/plugins/_core"];
|
||||
} else {
|
||||
dirs = ["src/plugins", "src/plugins/_core", "src/equicordplugins"];
|
||||
dirs = ["src/plugins", "src/plugins/_core", "src/equicordplugins/_core", "src/equicordplugins"];
|
||||
}
|
||||
|
||||
const outputPath = args.find(a => !a.startsWith("--")) ?? null;
|
||||
|
|
|
|||
|
|
@ -80,7 +80,9 @@ const IGNORED_DISCORD_ERRORS = [
|
|||
"Downloading the full bad domains file",
|
||||
/\[GatewaySocket\].{0,110}Cannot access '/,
|
||||
"search for 'name' in undefined",
|
||||
"Attempting to set fast connect zstd when unsupported"
|
||||
"Attempting to set fast connect zstd when unsupported",
|
||||
/Cannot read properties of undefined \(reading 'call'\)/,
|
||||
"Could not complete Remote Auth login, trying to restart with a new Remote Auth session."
|
||||
] as Array<string | RegExp>;
|
||||
|
||||
function toCodeBlock(s: string, indentation = 0, isDiscord = false) {
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ export interface PluginData {
|
|||
name: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
searchTerms: string[];
|
||||
authors: Dev[];
|
||||
dependencies: string[];
|
||||
hasPatches: boolean;
|
||||
|
|
@ -151,7 +152,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) {
|
||||
|
|
@ -207,9 +209,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;
|
||||
|
|
|
|||
|
|
@ -41,8 +41,7 @@ import { popNotice, showNotice } from "./api/Notices";
|
|||
import { NotificationData, showNotification } from "./api/Notifications";
|
||||
import { initPluginManager, PMLogger, startAllPlugins } from "./api/PluginManager";
|
||||
import { PlainSettings, Settings, SettingsStore } from "./api/Settings";
|
||||
import { getCloudSettings, putCloudSettings, shouldCloudSync } from "./api/SettingsSync/cloudSync";
|
||||
import { localStorage } from "./utils/localStorage";
|
||||
import { areLocalSettingsDirty, getCloudSettings, getCloudSyncDirection, markLocalSettingsDirty, putCloudSettings, shouldCloudSync } from "./api/SettingsSync/cloudSync";
|
||||
import { relaunch } from "./utils/native";
|
||||
import { checkForUpdates, isOutdated as getIsOutdated, update, UpdateLogger } from "./utils/updater";
|
||||
import { onceReady } from "./webpack";
|
||||
|
|
@ -53,11 +52,6 @@ if (IS_REPORTER) {
|
|||
}
|
||||
|
||||
async function syncSettings() {
|
||||
// Check if cloud auth exists for current user before attempting sync
|
||||
if (localStorage.Vencord_cloudSyncDirection === undefined) {
|
||||
// by default, sync bi-directionally
|
||||
localStorage.Vencord_cloudSyncDirection = "both";
|
||||
}
|
||||
const hasCloudAuth = await dsGet("Vencord_cloudSecret");
|
||||
if (!hasCloudAuth) {
|
||||
if (Settings.cloud.authenticated) {
|
||||
|
|
@ -93,9 +87,9 @@ async function syncSettings() {
|
|||
if (
|
||||
Settings.cloud.settingsSync && // if it's enabled
|
||||
Settings.cloud.authenticated && // if cloud integrations are enabled
|
||||
localStorage.Vencord_cloudSyncDirection !== "manual" // if we're not in manual mode
|
||||
getCloudSyncDirection() !== "manual" // if we're not in manual mode
|
||||
) {
|
||||
if (localStorage.Vencord_settingsDirty && shouldCloudSync("push")) {
|
||||
if (areLocalSettingsDirty() && shouldCloudSync("push")) {
|
||||
await putCloudSettings();
|
||||
} else if (shouldCloudSync("pull") && await getCloudSettings(false)) { // if we synchronized something (false means no sync)
|
||||
// we show a notification here instead of allowing getCloudSettings() to show one to declutter the amount of
|
||||
|
|
@ -118,7 +112,7 @@ async function syncSettings() {
|
|||
}, 60_000);
|
||||
|
||||
SettingsStore.addGlobalChangeListener(() => {
|
||||
localStorage.Vencord_settingsDirty = true;
|
||||
markLocalSettingsDirty();
|
||||
saveSettingsOnFrequentAction();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -29,6 +29,10 @@ export const enum BadgePosition {
|
|||
}
|
||||
|
||||
export interface ProfileBadge {
|
||||
/**
|
||||
* Badge id, unused by vencord, required by discord
|
||||
*/
|
||||
id: string,
|
||||
/** The tooltip to show on hover. Required for image badges */
|
||||
description?: string;
|
||||
/** Custom component for the badge (tooltip not included) */
|
||||
|
|
|
|||
|
|
@ -190,3 +190,22 @@ addContextMenuPatch("textarea-context", (children, args) => {
|
|||
</Menu.MenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
// Wrapper stuff is down here to avoid conflicts with above
|
||||
export type ChatBarButtonWrapper = (buttons: ReactNode) => ReactNode;
|
||||
|
||||
/**
|
||||
* Registry for plugins that need to wrap the entire chat bar button container
|
||||
*/
|
||||
export const ChatBarButtonWrappers = new Map<string, ChatBarButtonWrapper>();
|
||||
|
||||
export const addChatBarButtonWrapper = (id: string, wrapper: ChatBarButtonWrapper) => ChatBarButtonWrappers.set(id, wrapper);
|
||||
export const removeChatBarButtonWrapper = (id: string) => ChatBarButtonWrappers.delete(id);
|
||||
|
||||
export function _wrapButtons(buttons: ReactNode) {
|
||||
let wrapped = buttons;
|
||||
for (const wrapper of ChatBarButtonWrappers.values()) {
|
||||
wrapped = wrapper(wrapped);
|
||||
}
|
||||
return wrapped;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,6 +131,8 @@ interface ContextMenuProps {
|
|||
}
|
||||
|
||||
export function _usePatchContextMenu(props: ContextMenuProps) {
|
||||
if (!Menu.MenuItem) return props; // Prevent crashes in case we fail to acquire menu items for some reason
|
||||
|
||||
props = {
|
||||
...props,
|
||||
children: cloneMenuChildren(props.children),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
*/
|
||||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { JSX, ReactNode } from "react";
|
||||
|
||||
export type MessageAccessoryFactory = (props: Record<string, any>) => ReactNode;
|
||||
|
|
@ -46,23 +47,28 @@ export function _modifyAccessories(
|
|||
elements: JSX.Element[],
|
||||
props: Record<string, any>
|
||||
) {
|
||||
for (const [key, accessory] of accessories.entries()) {
|
||||
const res = (
|
||||
<ErrorBoundary noop message={`Failed to render ${key} Message Accessory`} key={key}>
|
||||
<accessory.render {...props} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
try {
|
||||
for (const [key, accessory] of accessories.entries()) {
|
||||
const res = (
|
||||
<ErrorBoundary noop message={`Failed to render ${key} Message Accessory`} key={key}>
|
||||
<accessory.render {...props} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
elements.splice(
|
||||
accessory.position != null
|
||||
? accessory.position < 0
|
||||
? elements.length + accessory.position
|
||||
: accessory.position
|
||||
: elements.length,
|
||||
0,
|
||||
res
|
||||
);
|
||||
elements.splice(
|
||||
accessory.position != null
|
||||
? accessory.position < 0
|
||||
? elements.length + accessory.position
|
||||
: accessory.position
|
||||
: elements.length,
|
||||
0,
|
||||
res
|
||||
);
|
||||
}
|
||||
|
||||
return elements;
|
||||
} catch (e) {
|
||||
new Logger("MessageAccessories").error("Failed to modify message accessories", e);
|
||||
return elements;
|
||||
}
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
position: absolute;
|
||||
z-index: 2147483647;
|
||||
right: 1rem;
|
||||
width: 25vw;
|
||||
width: min(25vw, 400px);
|
||||
min-height: 10vh;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
*/
|
||||
|
||||
import { addProfileBadge, removeProfileBadge } from "@api/Badges";
|
||||
import { addChatBarButton, removeChatBarButton } from "@api/ChatButtons";
|
||||
import { addChatBarButton, addChatBarButtonWrapper, removeChatBarButton, removeChatBarButtonWrapper } from "@api/ChatButtons";
|
||||
import { registerCommand, unregisterCommand } from "@api/Commands";
|
||||
import { addContextMenuPatch, removeContextMenuPatch } from "@api/ContextMenu";
|
||||
import { addMemberListDecorator, removeMemberListDecorator } from "@api/MemberListDecorators";
|
||||
|
|
@ -28,10 +28,11 @@ 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";
|
||||
import { Patch, Plugin, PluginDef, ReporterTestable, StartAt } from "@utils/types";
|
||||
import { DefinedSettings, Patch, Plugin, PluginDef, PluginSettingDef, ReporterTestable, StartAt } from "@utils/types";
|
||||
import { FluxEvents } from "@vencord/discord-types";
|
||||
import { FluxDispatcher } from "@webpack/common";
|
||||
import { patches } from "@webpack/patcher";
|
||||
|
|
@ -39,10 +40,10 @@ 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";
|
||||
import { addProfileSection, removeProfileSection } from "./ProfileSections";
|
||||
import { addUserAreaButton, removeUserAreaButton } from "./UserArea";
|
||||
|
||||
const logger = new Logger("PluginManager", "#a6d189");
|
||||
|
|
@ -60,14 +61,37 @@ export function isPluginEnabled(p: string) {
|
|||
Settings.plugins[p]?.enabled
|
||||
) ?? false;
|
||||
}
|
||||
|
||||
export function isPluginRequired(p: string) {
|
||||
return (
|
||||
Plugins[p]?.required
|
||||
) ?? false;
|
||||
}
|
||||
|
||||
export function isSettingHidden(settings: DefinedSettings, setting: PluginSettingDef) {
|
||||
if (!("hidden" in setting)) return false;
|
||||
|
||||
return typeof setting.hidden === "function"
|
||||
? setting.hidden.call(settings)
|
||||
: Boolean(setting.hidden);
|
||||
}
|
||||
|
||||
export function isSettingDisabled(settings: DefinedSettings, setting: PluginSettingDef) {
|
||||
if (!("disabled" in setting)) return false;
|
||||
|
||||
return typeof setting.disabled === "function"
|
||||
? setting.disabled.call(settings)
|
||||
: Boolean(setting.disabled);
|
||||
}
|
||||
|
||||
export function hasAnyVisibleSettings({ settings }: Plugin) {
|
||||
return !!settings && Object.values(settings.def).some(s => !isSettingHidden(settings, s));
|
||||
}
|
||||
|
||||
export function addPatch(newPatch: Omit<Patch, "plugin">, pluginName: string, pluginPath = `Vencord.Plugins.plugins[${JSON.stringify(pluginName)}]`) {
|
||||
// TODO: this causes crashes
|
||||
if (pluginName === "Vesktop" && newPatch.find === ".STREAMING_AUTO_STREAMER_MODE,") return;
|
||||
if (pluginName === "Equibop" && newPatch.find === ".STREAMING_AUTO_STREAMER_MODE,") return;
|
||||
|
||||
const patch = newPatch as Patch;
|
||||
patch.plugin = pluginName;
|
||||
|
||||
|
|
@ -197,7 +221,8 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p:
|
|||
onBeforeMessageEdit, onBeforeMessageSend, onMessageClick,
|
||||
chatBarButton, renderMemberListDecorator, renderMessageAccessory, renderMessageDecoration, messagePopoverButton,
|
||||
// Custom
|
||||
renderNicknameIcon, headerBarButton, audioProcessor, userAreaButton, renderProfileCollection
|
||||
renderNicknameIcon, headerBarButton, audioProcessor, userAreaButton, renderProfileCollection, chatBarButtonWrapper,
|
||||
renderProfileSection
|
||||
} = p;
|
||||
|
||||
if (p.start) {
|
||||
|
|
@ -265,6 +290,8 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p:
|
|||
if (audioProcessor) addAudioProcessor(name, audioProcessor);
|
||||
if (userAreaButton) addUserAreaButton(name, userAreaButton.render, userAreaButton.priority);
|
||||
if (renderProfileCollection) addProfileCollection(name, renderProfileCollection);
|
||||
if (chatBarButtonWrapper) addChatBarButtonWrapper(name, chatBarButtonWrapper);
|
||||
if (renderProfileSection) addProfileSection(name, renderProfileSection);
|
||||
|
||||
return true;
|
||||
}, p => `startPlugin ${p.name}`);
|
||||
|
|
@ -275,7 +302,8 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu
|
|||
onBeforeMessageEdit, onBeforeMessageSend, onMessageClick,
|
||||
chatBarButton, renderMemberListDecorator, renderMessageAccessory, renderMessageDecoration, messagePopoverButton,
|
||||
// Custom
|
||||
renderNicknameIcon, headerBarButton, audioProcessor, userAreaButton, renderProfileCollection
|
||||
renderNicknameIcon, headerBarButton, audioProcessor, userAreaButton, renderProfileCollection, chatBarButtonWrapper,
|
||||
renderProfileSection
|
||||
} = p;
|
||||
|
||||
if (p.stop) {
|
||||
|
|
@ -341,6 +369,8 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu
|
|||
if (audioProcessor) removeAudioProcessor(name);
|
||||
if (userAreaButton) removeUserAreaButton(name);
|
||||
if (renderProfileCollection) removeProfileCollection(name);
|
||||
if (chatBarButtonWrapper) removeChatBarButtonWrapper(name);
|
||||
if (renderProfileSection) removeProfileSection(name);
|
||||
|
||||
return true;
|
||||
}, p => `stopPlugin ${p.name}`);
|
||||
|
|
@ -396,6 +426,8 @@ export const initPluginManager = onlyOnce(function init() {
|
|||
if (p.audioProcessor) neededApiPlugins.add("AudioPlayerAPI");
|
||||
if (p.userAreaButton) neededApiPlugins.add("UserAreaAPI");
|
||||
if (p.renderProfileCollection) neededApiPlugins.add("ProfileCollectionsAPI");
|
||||
if (p.chatBarButtonWrapper) neededApiPlugins.add("ChatInputButtonAPI");
|
||||
if (p.renderProfileSection) neededApiPlugins.add("ProfileSectionsAPI");
|
||||
|
||||
for (const key of pluginKeysToBind) {
|
||||
p[key] &&= p[key].bind(p) as any;
|
||||
|
|
@ -409,22 +441,11 @@ export const initPluginManager = onlyOnce(function init() {
|
|||
|
||||
for (const p of pluginsValues) {
|
||||
if (p.settings) {
|
||||
p.options ??= {};
|
||||
|
||||
p.settings.pluginName = p.name;
|
||||
for (const name in p.settings.def) {
|
||||
const def = p.settings.def[name];
|
||||
const checks = p.settings.checks?.[name];
|
||||
p.options[name] = { ...def, ...checks };
|
||||
}
|
||||
}
|
||||
|
||||
if (p.options) {
|
||||
for (const name in p.options) {
|
||||
const opt = p.options[name];
|
||||
if (opt.onChange != null) {
|
||||
SettingsStore.addChangeListener(`plugins.${p.name}.${name}`, opt.onChange);
|
||||
}
|
||||
for (const [key, def] of Object.entries(p.settings.def)) {
|
||||
if (def.onChange)
|
||||
SettingsStore.addChangeListener(`plugins.${p.name}.${key}`, def.onChange);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,12 +6,13 @@
|
|||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { User } from "@vencord/discord-types";
|
||||
import { useEffect, useState } from "@webpack/common";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
const logger = new Logger("ProfileCollectionAPI");
|
||||
|
||||
export type ProfileCollectionFactory = (props: any) => ReactNode | null;
|
||||
export type ProfileCollectionFactory = (props: { user: User; isSideBar: boolean; }) => ReactNode | Promise<ReactNode> | null;
|
||||
|
||||
interface CollectionEntry {
|
||||
render: ProfileCollectionFactory;
|
||||
|
|
|
|||
73
src/api/ProfileSections.tsx
Normal file
73
src/api/ProfileSections.tsx
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { useEffect, useState } from "@webpack/common";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
const logger = new Logger("ProfileSectionsAPI");
|
||||
|
||||
export type ProfileSectionFactory = (props: { userId: string; isSideBar: boolean; }) => ReactNode | Promise<ReactNode> | null;
|
||||
|
||||
interface SectionEntry {
|
||||
render: ProfileSectionFactory;
|
||||
priority: number;
|
||||
}
|
||||
|
||||
const profileSections = new Map<string, SectionEntry>();
|
||||
const profileSectionListeners = new Set<() => void>();
|
||||
|
||||
/**
|
||||
* Adds a section to the user profile panel near the 'Member Since' area.
|
||||
*
|
||||
* @param id - Unique identifier for the section (e.g., "my-plugin-profile-section")
|
||||
* @param render - Function that returns the section JSX.
|
||||
* Receives `{ userId, isSideBar }` so you can style accordingly.
|
||||
* @param priority - Higher values appear first. Default: 0
|
||||
*
|
||||
* @example
|
||||
* addProfileSection("my-section", ({ userId, isSideBar }) => (
|
||||
* <div>Custom content for {userId}</div>
|
||||
* ));
|
||||
*/
|
||||
export function addProfileSection(id: string, render: ProfileSectionFactory, priority = 0) {
|
||||
profileSections.set(id, { render, priority });
|
||||
profileSectionListeners.forEach(listener => listener());
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a section from the user profile panel.
|
||||
*
|
||||
* @param id - The identifier used when adding the section
|
||||
*/
|
||||
export function removeProfileSection(id: string) {
|
||||
profileSections.delete(id);
|
||||
profileSectionListeners.forEach(listener => listener());
|
||||
}
|
||||
|
||||
function ProfileSections({ userId, isSideBar }: { userId: string; isSideBar: boolean; }) {
|
||||
const [, forceUpdate] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = () => forceUpdate(n => n + 1);
|
||||
profileSectionListeners.add(listener);
|
||||
return () => { profileSectionListeners.delete(listener); };
|
||||
}, []);
|
||||
|
||||
return Array.from(profileSections)
|
||||
.sort(([, a], [, b]) => b.priority - a.priority)
|
||||
.map(([id, { render: Section }]) => (
|
||||
<ErrorBoundary noop key={id} onError={e => logger.error(`Failed to render profile section: ${id}`, e.error)}>
|
||||
<Section userId={userId} isSideBar={isSideBar} />
|
||||
</ErrorBoundary>
|
||||
));
|
||||
}
|
||||
|
||||
/** @internal Injected by ProfileSectionsAPI patch (do NOT call directly) */
|
||||
export function renderProfileSections(props: { userId: string; isSideBar: boolean; }) {
|
||||
return <ProfileSections {...props} />;
|
||||
}
|
||||
|
|
@ -25,11 +25,11 @@ export const enum ServerListRenderPosition {
|
|||
Below,
|
||||
}
|
||||
|
||||
const componentsIn = new Set<ComponentType>();
|
||||
const componentsAbove = new Set<ComponentType>();
|
||||
const componentsBelow = new Set<ComponentType>();
|
||||
const componentsIn = new Map<ComponentType, number>();
|
||||
const componentsAbove = new Map<ComponentType, number>();
|
||||
const componentsBelow = new Map<ComponentType, number>();
|
||||
|
||||
function getRenderFunctions(position: ServerListRenderPosition) {
|
||||
function getRenderMap(position: ServerListRenderPosition) {
|
||||
switch (position) {
|
||||
case ServerListRenderPosition.Above:
|
||||
return componentsAbove;
|
||||
|
|
@ -40,21 +40,20 @@ function getRenderFunctions(position: ServerListRenderPosition) {
|
|||
}
|
||||
}
|
||||
|
||||
export function addServerListElement(position: ServerListRenderPosition, renderFunction: ComponentType) {
|
||||
getRenderFunctions(position).add(renderFunction);
|
||||
export function addServerListElement(position: ServerListRenderPosition, renderFunction: ComponentType, priority = 0) {
|
||||
getRenderMap(position).set(renderFunction, priority);
|
||||
}
|
||||
|
||||
export function removeServerListElement(position: ServerListRenderPosition, renderFunction: ComponentType) {
|
||||
getRenderFunctions(position).delete(renderFunction);
|
||||
getRenderMap(position).delete(renderFunction);
|
||||
}
|
||||
|
||||
export const renderAll = (position: ServerListRenderPosition) => {
|
||||
return Array.from(
|
||||
getRenderFunctions(position),
|
||||
(Component, i) => (
|
||||
return Array.from(getRenderMap(position).entries())
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([Component], i) => (
|
||||
<ErrorBoundary noop key={i}>
|
||||
<Component />
|
||||
</ErrorBoundary>
|
||||
)
|
||||
);
|
||||
));
|
||||
};
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ const DefaultSettings: Settings = {
|
|||
autoUpdateNotification: true,
|
||||
useQuickCss: true,
|
||||
themeLinks: [],
|
||||
eagerPatches: IS_REPORTER,
|
||||
eagerPatches: false, // Eagerly patching no longer works due to module factories with the same id being able to have different sources now.
|
||||
enabledThemes: [],
|
||||
enabledThemeLinks: [],
|
||||
enableOnlineThemes: true,
|
||||
|
|
@ -178,7 +178,7 @@ export const SettingsStore = new SettingsStoreClass(settings, {
|
|||
if (path.startsWith("plugins.")) {
|
||||
const plugin = path.slice("plugins.".length);
|
||||
if (plugin in plugins) {
|
||||
const setting = plugins[plugin].options?.[key];
|
||||
const setting = plugins[plugin].settings?.def[key];
|
||||
if (!setting) return v;
|
||||
|
||||
if ("default" in setting)
|
||||
|
|
@ -208,7 +208,7 @@ if (!IS_REPORTER) {
|
|||
* Same as {@link Settings} but unproxied. You should treat this as readonly,
|
||||
* as modifying properties on this will not save to disk or call settings
|
||||
* listeners.
|
||||
* WARNING: default values specified in plugin.options will not be ensured here. In other words,
|
||||
* WARNING: default values specified in plugin.settings will not be ensured here. In other words,
|
||||
* settings for which you specified a default value may be uninitialised. If you need proper
|
||||
* handling for default values, use {@link Settings}
|
||||
*/
|
||||
|
|
@ -353,7 +353,13 @@ export function definePluginSettings<
|
|||
Checks extends SettingsChecks<Def>,
|
||||
PrivateSettings extends object = {}
|
||||
>(def: Def, checks?: Checks) {
|
||||
const definedSettings: DefinedSettings<Def, Checks, PrivateSettings> = {
|
||||
if (checks) {
|
||||
for (const [name, check] of Object.entries(checks)) {
|
||||
Object.assign(def[name], check);
|
||||
}
|
||||
}
|
||||
|
||||
const definedSettings: DefinedSettings<Def, PrivateSettings> = {
|
||||
get store() {
|
||||
if (!definedSettings.pluginName) throw new Error("Cannot access settings before plugin is initialized");
|
||||
return Settings.plugins[definedSettings.pluginName] as any;
|
||||
|
|
@ -368,11 +374,10 @@ export function definePluginSettings<
|
|||
: [`plugins.${definedSettings.pluginName}.*`]
|
||||
) as UseSettings<Settings>[]).plugins[definedSettings.pluginName] as any,
|
||||
def,
|
||||
checks: checks ?? {} as any,
|
||||
pluginName: "",
|
||||
|
||||
withPrivateSettings<T extends object>() {
|
||||
return this as DefinedSettings<Def, Checks, T>;
|
||||
return this as DefinedSettings<Def, T>;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -20,17 +20,33 @@ import { ManifestEntry, SyncRequest, SyncResponse } from "./types";
|
|||
const logger = new Logger("SettingsSync:Cloud", "#39b7e0");
|
||||
|
||||
const MANIFEST_STORE_KEY = "Vencord_cloudManifest";
|
||||
const API_VERSION_STORE_KEY = "Vencord_cloudApiVersions";
|
||||
|
||||
type ApiVersion = "v2" | "v1";
|
||||
|
||||
let apiVersion: ApiVersion = "v2";
|
||||
const SYNC_DIRECTION_KEY = "Vencord_cloudSyncDirection";
|
||||
const SETTINGS_DIRTY_KEY = "Vencord_settingsDirty";
|
||||
export const getCloudSyncDirection = () => localStorage.getItem(SYNC_DIRECTION_KEY) || "both";
|
||||
export const setCloudSyncDirection = (direction: "push" | "pull" | "both" | "manual") => localStorage.setItem(SYNC_DIRECTION_KEY, direction);
|
||||
export const areLocalSettingsDirty = () => localStorage.getItem(SETTINGS_DIRTY_KEY) === "true";
|
||||
export const markLocalSettingsDirty = () => localStorage.setItem(SETTINGS_DIRTY_KEY, "true");
|
||||
export const markLocalSettingsClean = () => localStorage.removeItem(SETTINGS_DIRTY_KEY);
|
||||
|
||||
async function loadApiVersionMap(): Promise<Record<string, ApiVersion>> {
|
||||
return await DataStore.get<Record<string, ApiVersion>>(API_VERSION_STORE_KEY) ?? {};
|
||||
}
|
||||
|
||||
async function getApiVersion(): Promise<ApiVersion> {
|
||||
return apiVersion;
|
||||
const map = await loadApiVersionMap();
|
||||
return map[getCloudUrl().origin] ?? "v2";
|
||||
}
|
||||
|
||||
async function setApiVersion(version: ApiVersion) {
|
||||
apiVersion = version;
|
||||
await DataStore.update<Record<string, ApiVersion>>(API_VERSION_STORE_KEY, map => {
|
||||
map ??= {};
|
||||
map[getCloudUrl().origin] = version;
|
||||
return map;
|
||||
});
|
||||
}
|
||||
|
||||
function toBase64(data: Uint8Array): string {
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import * as $Notifications from "./Notifications";
|
|||
import * as $UserArea from "./UserArea";
|
||||
export * as PluginManager from "./PluginManager";
|
||||
import * as $ProfileCollections from "./ProfileCollections";
|
||||
import * as $ProfileSections from "./ProfileSections";
|
||||
import * as $ServerList from "./ServerList";
|
||||
import * as $Settings from "./Settings";
|
||||
import * as $Styles from "./Styles";
|
||||
|
|
@ -176,3 +177,8 @@ export const isEquicord = true;
|
|||
* An API allowing you to add other collections where discords game collection is.
|
||||
*/
|
||||
export const ProfileCollections = $ProfileCollections;
|
||||
|
||||
/**
|
||||
* An API allowing you to add sections near the 'Member Since' area of user profile panels.
|
||||
*/
|
||||
export const ProfileSections = $ProfileSections;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -730,6 +730,34 @@ export function ClockIcon(props: IconProps) {
|
|||
);
|
||||
}
|
||||
|
||||
export function ChevronSmallDownIcon(props: IconProps) {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill={props.fill || "currentColor"}
|
||||
d="M5.3 9.3a1 1 0 0 1 1.4 0l5.3 5.29 5.3-5.3a1 1 0 1 1 1.4 1.42l-6 6a1 1 0 0 1-1.4 0l-6-6a1 1 0 0 1 0-1.42Z"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
export function ChevronSmallUpIcon(props: IconProps) {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill={props.fill || "currentColor"}
|
||||
d="M5.3 14.7a1 1 0 0 0 1.4 0l5.3-5.29 5.3 5.3a1 1 0 1 0 1.4-1.42l-6-6a1 1 0 0 0-1.4 0l-6 6a1 1 0 0 0 0 1.42Z"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
export function DownArrow(props: IconProps) {
|
||||
return (
|
||||
<Icon
|
||||
|
|
@ -797,3 +825,171 @@ export const ComponentsIcon = (props?: any) => {
|
|||
</Icon>
|
||||
);
|
||||
};
|
||||
|
||||
export const LogsIcon = (props?: any) => {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
fill={props.fill || "currentColor"}
|
||||
d="M21 21.93V2.07a1 1 0 0 0-1.27-.97l-2.5.7a3 3 0 0 1-1.46.04l-3.12-.7a3 3 0 0 0-1.3 0l-3.12.7a3 3 0 0 1-1.45-.04l-2.51-.7A1 1 0 0 0 3 2.07v19.86a1 1 0 0 0 1.27.97l2.5-.7a3 3 0 0 1 1.46-.04l3.12.7a3 3 0 0 0 1.3 0l3.12-.7a3 3 0 0 1 1.45.04l2.51.7a1 1 0 0 0 1.27-.97ZM7 8a1 1 0 0 1 1-1h8a1 1 0 1 1 0 2H8a1 1 0 0 1-1-1Zm1 3a1 1 0 0 0 0 2h8a1 1 0 1 0 0-2H8Zm-1 5a1 1 0 0 1 1-1h3a1 1 0 0 1 0 2H8a1 1 0 0 1-1-1Zm8-1a1 1 0 0 0 0 2h1a1 1 0 1 0 0-2h-1Z"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
};
|
||||
|
||||
export const BookmarkIcon = (props?: any) => {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill={props.fill || "currentColor"}
|
||||
d="M4 5a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v16a1 1 0 0 1-1.67.74l-5.66-5.13a1 1 0 0 0-1.34 0l-5.66 5.13A1 1 0 0 1 4 20.99V5Z"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
};
|
||||
|
||||
export function RobotIcon(props: IconProps) {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill={props.fill || "currentColor"}
|
||||
d="M7.89 13.46a1 1 0 0 1-1.78-.9L7 13l-.9-.45.01-.01.01-.02a2.24 2.24 0 0 1 .14-.23c.1-.14.23-.31.4-.5.37-.36.98-.79 1.84-.79.86 0 1.47.43 1.83.8a3.28 3.28 0 0 1 .55.72v.02h.01v.01L10 13l.9-.45a1 1 0 0 1-1.79.9 1.28 1.28 0 0 0-.19-.25c-.14-.13-.28-.2-.42-.2-.14 0-.28.07-.42.2a1.28 1.28 0 0 0-.19.25ZM13.55 13.9a1 1 0 0 0 1.34-.44c0-.02.02-.04.04-.06.03-.05.08-.13.15-.2.14-.13.28-.2.42-.2.14 0 .28.07.42.2a1.28 1.28 0 0 1 .19.25 1 1 0 0 0 1.78-.9L17 13l.9-.45-.01-.01-.01-.02a2.1 2.1 0 0 0-.14-.23 3.28 3.28 0 0 0-.4-.5c-.37-.36-.98-.79-1.84-.79-.86 0-1.47.43-1.83.8a3.28 3.28 0 0 0-.55.72v.02h-.01v.01L14 13l-.9-.45a1 1 0 0 0 .45 1.34Z"
|
||||
/>
|
||||
<path
|
||||
fill={props.fill || "currentColor"}
|
||||
fillRule="evenodd"
|
||||
d="M12 21c5.52 0 10-1.86 10-6 0-5.59-2.8-10.07-4.26-11.67a1 1 0 1 0-1.48 1.34 14.8 14.8 0 0 1 2.35 3.86A10.23 10.23 0 0 0 12 6C9.47 6 7.15 7.02 5.4 8.53a14.8 14.8 0 0 1 2.34-3.86 1 1 0 1 0-1.48-1.34A18.65 18.65 0 0 0 2 15c0 4.14 4.48 6 10 6Zm0-12c3.87 0 7 2 7 4.2S15.87 17 12 17s-7-1.6-7-3.8C5 11 8.13 9 12 9Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
export function HeadphonesIcon(props: IconProps) {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill={props.fill || "currentColor"}
|
||||
d="M12 3a9 9 0 0 0-8.95 10h1.87a5 5 0 0 1 4.1 2.13l1.37 1.97a3.1 3.1 0 0 1-.17 3.78 2.85 2.85 0 0 1-3.55.74 11 11 0 1 1 10.66 0c-1.27.71-2.73.23-3.55-.74a3.1 3.1 0 0 1-.17-3.78l1.38-1.97a5 5 0 0 1 4.1-2.13h1.86A9 9 0 0 0 12 3Z"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
export function ShieldIcon(props: IconProps) {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill={props.fill || "currentColor"}
|
||||
d="M17.05 8.76a4.7 4.7 0 0 1-1.29-.88c-.36-.33-.7-.73-.88-1.13-.33-.73-.16-.77-1.08-.77h-3.64c-.91 0-.74.04-1.08.77a4.94 4.94 0 0 1-2.17 2.01c-.74.33-.85.14-.86.82 0 .88-.22 3.34 1.78 5.19a13.68 13.68 0 0 0 3.98 2.47c.09.03.08.04.17.04.09 0 .08 0 .17-.04.76-.25 2.67-1.25 3.98-2.47 2-1.85 1.78-4.3 1.78-5.19-.01-.68-.12-.49-.86-.82Z"
|
||||
/>
|
||||
<path
|
||||
fill={props.fill || "currentColor"}
|
||||
d="M19.91 5.41c-.84 0-1.52-.65-1.52-1.46v-.3c0-.9-.77-1.65-1.71-1.65H7.31c-.94 0-1.71.74-1.71 1.65v.3c0 .81-.68 1.46-1.52 1.46H3.7c-.94 0-1.7.73-1.7 1.64v3.52l.01.49c.05 3.11.94 4.69 2.92 6.63C6.72 19.46 11.58 22 11.99 22c.41 0 5.27-2.54 7.06-4.31 1.98-1.95 2.92-3.53 2.92-6.63L22 7.05c0-.9-.76-1.64-1.7-1.64h-.39Zm-2.18 10.74a21.76 21.76 0 0 1-5.58 3.38c-.1.03-.2.03-.31 0a21.76 21.76 0 0 1-5.58-3.38c-1.15-1.08-2.12-2.37-2.12-5.29v-.33l.03-2.57c0-.31.26-.57.59-.57a2.88 2.88 0 0 0 2.89-2.8v-.03c0-.3.26-.55.59-.55h7.56c.32 0 .59.25.59.55v.03c0 1.53 1.3 2.77 2.89 2.8.32 0 .59.26.59.57l.03 2.57v.34c0 2.92-.98 4.2-2.14 5.28h-.03Z"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
export function CircleQuestionIcon(props: IconProps) {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
fill="transparent"
|
||||
/>
|
||||
<path
|
||||
fill={props.fill || "currentColor"}
|
||||
fillRule="evenodd"
|
||||
d="M12 23a11 11 0 1 0 0-22 11 11 0 0 0 0 22Zm-.28-16c-.98 0-1.81.47-2.27 1.14A1 1 0 1 1 7.8 7.01 4.73 4.73 0 0 1 11.72 5c2.5 0 4.65 1.88 4.65 4.38 0 2.1-1.54 3.77-3.52 4.24l.14 1a1 1 0 0 1-1.98.27l-.28-2a1 1 0 0 1 .99-1.14c1.54 0 2.65-1.14 2.65-2.38 0-1.23-1.1-2.37-2.65-2.37ZM13 17.88a1.13 1.13 0 1 1-2.25 0 1.13 1.13 0 0 1 2.25 0Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
export function AttachmentIcon(props: IconProps) {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill={props.fill || "currentColor"}
|
||||
d="M10.57 4.01a6.97 6.97 0 0 1 9.86 0l.54.55a6.99 6.99 0 0 1 0 9.88l-7.26 7.27a1 1 0 0 1-1.42-1.42l7.27-7.26a4.99 4.99 0 0 0 0-7.06L19 5.43a4.97 4.97 0 0 0-7.02 0l-8.02 8.02a3.24 3.24 0 1 0 4.58 4.58l6.24-6.24a1.12 1.12 0 0 0-1.58-1.58l-3.5 3.5a1 1 0 0 1-1.42-1.42l3.5-3.5a3.12 3.12 0 1 1 4.42 4.42l-6.24 6.24a5.24 5.24 0 0 1-7.42-7.42l8.02-8.02Z"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
export function StickerIcon(props: IconProps) {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill={props.fill || "currentColor"}
|
||||
fillRule="evenodd"
|
||||
d="M6 2h12a4 4 0 0 1 4 4v7.5a.5.5 0 0 1-.5.5H19a5 5 0 0 0-5 5v2.5a.5.5 0 0 1-.5.5H6a4 4 0 0 1-4-4V6a4 4 0 0 1 4-4Zm.5 8a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM19 8.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm-9.91 2.94a1 1 0 0 0-1.66 1.12 5.5 5.5 0 0 0 9.14 0 1 1 0 0 0-1.66-1.12 3.5 3.5 0 0 1-5.82 0Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
<path
|
||||
fill={props.fill || "currentColor"}
|
||||
d="M21.66 16c.03 0 .05.03.04.06a3 3 0 0 1-.58.82l-4.24 4.24a3 3 0 0 1-.82.58.04.04 0 0 1-.06-.04V19a3 3 0 0 1 3-3h2.66Z"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
export function GifIcon(props: IconProps) {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill={props.fill || "currentColor"}
|
||||
fillRule="evenodd"
|
||||
d="M5 2a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3V5a3 3 0 0 0-3-3H5Zm2.18 13.87a2.9 2.9 0 0 1-1.66-.49 3.18 3.18 0 0 1-1.12-1.35A4.88 4.88 0 0 1 4 12c0-.75.14-1.42.42-2 .29-.59.7-1.04 1.24-1.37a3.7 3.7 0 0 1 1.95-.5c.64 0 1.21.14 1.72.4.5.28.89.67 1.15 1.16l-1.22.88a1.76 1.76 0 0 0-1.64-1.02c-.66 0-1.17.22-1.52.65a2.74 2.74 0 0 0-.53 1.8c0 .78.18 1.39.53 1.81.35.42.86.64 1.52.64.3 0 .57-.06.81-.16.25-.12.45-.27.58-.47v-.93H7.3v-1.4h3.24v4.23H9.27l-.2-.71c-.43.57-1.06.86-1.89.86Zm6.34-.15h-1.57V8.28h1.57v7.44Zm1.51 0h1.57v-2.83h2.76v-1.42H16.6V9.7H20V8.28h-4.97v7.44Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
export function VideoIcon(props: IconProps) {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill={props.fill || "currentColor"}
|
||||
d="M4 4a3 3 0 0 0-3 3v10a3 3 0 0 0 3 3h11a3 3 0 0 0 3-3v-2.12a1 1 0 0 0 .55.9l3 1.5a1 1 0 0 0 1.45-.9V7.62a1 1 0 0 0-1.45-.9l-3 1.5a1 1 0 0 0-.55.9V7a3 3 0 0 0-3-3H4Z"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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=");
|
||||
|
|
|
|||
|
|
@ -16,9 +16,10 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { BaseText } from "@components/BaseText";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { handleComponentFailed } from "@components/handleComponentFailed";
|
||||
import { ModalCloseButton, ModalContent, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { onlyOnce } from "@utils/onlyOnce";
|
||||
import type { ComponentType, PropsWithChildren } from "react";
|
||||
|
||||
|
|
@ -43,13 +44,17 @@ export function wrapTab(component: ComponentType<any>, tab: string) {
|
|||
export function openSettingsTabModal(Tab: ComponentType<any>) {
|
||||
try {
|
||||
openModal(wrapTab((modalProps: ModalProps) => (
|
||||
<ModalRoot {...modalProps} size={ModalSize.MEDIUM}>
|
||||
<ModalRoot {...modalProps} size={ModalSize.MEDIUM} className="vc-settings-modal-root">
|
||||
<ModalHeader>
|
||||
<BaseText size="lg" weight="semibold" style={{ flexGrow: 1 }}>{Tab.displayName?.replace("SettingsTab", "") || "Settings"}</BaseText>
|
||||
<ModalCloseButton onClick={modalProps.onClose} />
|
||||
</ModalHeader>
|
||||
|
||||
<ModalContent className="vc-settings-modal">
|
||||
<ModalCloseButton onClick={modalProps.onClose} className="vc-settings-modal-close" />
|
||||
<Tab />
|
||||
</ModalContent>
|
||||
</ModalRoot>
|
||||
), Tab.displayName || "Settings Tab"));
|
||||
), Tab.displayName || "SettingsTab"));
|
||||
} catch {
|
||||
handleSettingsTabError();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,14 +5,14 @@
|
|||
*/
|
||||
|
||||
import { showNotice } from "@api/Notices";
|
||||
import { isPluginEnabled, pluginRequiresRestart, startDependenciesRecursive, startPlugin, stopPlugin } from "@api/PluginManager";
|
||||
import { hasAnyVisibleSettings, 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 { Plugin } from "@utils/types";
|
||||
import { React, showToast, Toasts } from "@webpack/common";
|
||||
import { Settings } from "Vencord";
|
||||
|
||||
import { PluginMeta } from "~plugins";
|
||||
|
||||
|
|
@ -146,7 +146,7 @@ export function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, on
|
|||
onClick={() => openPluginModal(plugin, onRestartNeeded)}
|
||||
className={cl("info-button")}
|
||||
>
|
||||
{plugin.settings?.def && Object.values(plugin.settings.def).some(s => s.type !== OptionType.CUSTOM && !s.hidden)
|
||||
{hasAnyVisibleSettings(plugin)
|
||||
? <CogWheel className={cl("info-icon")} />
|
||||
: <InfoIcon className={cl("info-icon")} />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
import "./PluginModal.css";
|
||||
|
||||
import { generateId } from "@api/Commands";
|
||||
import { hasAnyVisibleSettings, isSettingHidden } from "@api/PluginManager";
|
||||
import { useSettings } from "@api/Settings";
|
||||
import { BaseText } from "@components/BaseText";
|
||||
import { Button } from "@components/Button";
|
||||
|
|
@ -32,7 +33,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,9 +75,19 @@ 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));
|
||||
const hasSettings = hasAnyVisibleSettings(plugin);
|
||||
|
||||
// avoid layout shift by showing dummy users while loading users
|
||||
const fallbackAuthors = useMemo(() => [makeDummyUser({ username: "Loading...", id: "-1465912127305809920" })], []);
|
||||
|
|
@ -104,14 +115,17 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||
}
|
||||
|
||||
function renderSettings() {
|
||||
if (!hasSettings || !plugin.options)
|
||||
const { settings } = plugin;
|
||||
if (!hasSettings || !settings)
|
||||
return <Paragraph>There are no settings for this plugin.</Paragraph>;
|
||||
|
||||
const options = Object.entries(plugin.options).map(([key, setting]) => {
|
||||
if (setting.type === OptionType.CUSTOM || setting.hidden) return null;
|
||||
const options = Object.entries(settings.def).map(([key, setting]) => {
|
||||
if (setting.type === OptionType.CUSTOM) return null;
|
||||
|
||||
if (isSettingHidden(settings, setting)) return null;
|
||||
|
||||
function onChange(newValue: any) {
|
||||
const option = plugin.options?.[key];
|
||||
const option = plugin.settings!.def[key];
|
||||
if (!option || option.type === OptionType.CUSTOM) return;
|
||||
|
||||
pluginSettings[key] = newValue;
|
||||
|
|
@ -124,10 +138,10 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||
<ErrorBoundary noop key={key}>
|
||||
<Component
|
||||
id={key}
|
||||
option={setting}
|
||||
setting={setting}
|
||||
onChange={debounce(onChange)}
|
||||
pluginSettings={pluginSettings}
|
||||
definedSettings={plugin.settings}
|
||||
definedSettings={settings}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
|
@ -167,6 +181,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">
|
||||
|
|
|
|||
|
|
@ -16,20 +16,21 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { isSettingDisabled } from "@api/PluginManager";
|
||||
import { Switch } from "@components/Switch";
|
||||
import { PluginOptionBoolean } from "@utils/types";
|
||||
import { PluginSettingBooleanDef } from "@utils/types";
|
||||
import { React, useState } from "@webpack/common";
|
||||
|
||||
import { resolveError, SettingProps, SettingsSection } from "./Common";
|
||||
|
||||
export function BooleanSetting({ option, pluginSettings, definedSettings, id, onChange }: SettingProps<PluginOptionBoolean>) {
|
||||
const def = pluginSettings[id] ?? option.default;
|
||||
export function BooleanSetting({ setting, pluginSettings, definedSettings, id, onChange }: SettingProps<PluginSettingBooleanDef>) {
|
||||
const def = pluginSettings[id] ?? setting.default;
|
||||
|
||||
const [state, setState] = useState(def ?? false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
function handleChange(newValue: boolean): void {
|
||||
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
||||
const isValid = setting.isValid?.call(definedSettings, newValue) ?? true;
|
||||
|
||||
setState(newValue);
|
||||
setError(resolveError(isValid));
|
||||
|
|
@ -40,11 +41,11 @@ export function BooleanSetting({ option, pluginSettings, definedSettings, id, on
|
|||
}
|
||||
|
||||
return (
|
||||
<SettingsSection tag="label" name={id} description={option.description} error={error} inlineSetting>
|
||||
<SettingsSection tag="label" name={id} description={setting.description} error={error} inlineSetting>
|
||||
<Switch
|
||||
checked={state}
|
||||
onChange={handleChange}
|
||||
disabled={option.disabled?.call(definedSettings) ?? false}
|
||||
disabled={isSettingDisabled(definedSettings, setting)}
|
||||
/>
|
||||
</SettingsSection>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -8,24 +8,24 @@ import { BaseText } from "@components/BaseText";
|
|||
import { classNameFactory } from "@utils/css";
|
||||
import { classes } from "@utils/misc";
|
||||
import { wordsFromCamel, wordsToTitle } from "@utils/text";
|
||||
import { DefinedSettings, PluginOptionBase } from "@utils/types";
|
||||
import { DefinedSettings, PluginSettingDefCommon } from "@utils/types";
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
export const cl = classNameFactory("vc-plugins-setting-");
|
||||
|
||||
interface SettingBaseProps<T> {
|
||||
option: T;
|
||||
setting: T;
|
||||
onChange(newValue: any): void;
|
||||
pluginSettings: {
|
||||
[setting: string]: any;
|
||||
enabled: boolean;
|
||||
};
|
||||
id: string;
|
||||
definedSettings?: DefinedSettings;
|
||||
definedSettings: DefinedSettings;
|
||||
}
|
||||
|
||||
export type SettingProps<T extends PluginOptionBase> = SettingBaseProps<T>;
|
||||
export type ComponentSettingProps<T extends Omit<PluginOptionBase, "description" | "placeholder">> = SettingBaseProps<T>;
|
||||
export type SettingProps<T extends PluginSettingDefCommon> = SettingBaseProps<T>;
|
||||
export type ComponentSettingProps<T extends Omit<PluginSettingDefCommon, "description" | "placeholder">> = SettingBaseProps<T>;
|
||||
|
||||
export function resolveError(isValidResult: boolean | string) {
|
||||
if (typeof isValidResult === "string") return isValidResult;
|
||||
|
|
|
|||
|
|
@ -16,10 +16,10 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { PluginOptionComponent } from "@utils/types";
|
||||
import { PluginSettingComponentDef } from "@utils/types";
|
||||
|
||||
import { ComponentSettingProps } from "./Common";
|
||||
|
||||
export function ComponentSetting({ option, onChange }: ComponentSettingProps<PluginOptionComponent>) {
|
||||
return option.component({ setValue: onChange, option });
|
||||
export function ComponentSetting({ setting, onChange }: ComponentSettingProps<PluginSettingComponentDef>) {
|
||||
return setting.component({ setValue: onChange, option: setting });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,24 +16,25 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { OptionType, PluginOptionNumber } from "@utils/types";
|
||||
import { isSettingDisabled } from "@api/PluginManager";
|
||||
import { OptionType, PluginSettingBigIntDef, PluginSettingNumberDef } from "@utils/types";
|
||||
import { React, TextInput, useState } from "@webpack/common";
|
||||
|
||||
import { resolveError, SettingProps, SettingsSection } from "./Common";
|
||||
|
||||
const MAX_SAFE_NUMBER = BigInt(Number.MAX_SAFE_INTEGER);
|
||||
|
||||
export function NumberSetting({ option, pluginSettings, definedSettings, id, onChange }: SettingProps<PluginOptionNumber>) {
|
||||
export function NumberSetting({ setting, pluginSettings, definedSettings, id, onChange }: SettingProps<PluginSettingNumberDef | PluginSettingBigIntDef>) {
|
||||
function serialize(value: any) {
|
||||
if (option.type === OptionType.BIGINT) return BigInt(value);
|
||||
if (setting.type === OptionType.BIGINT) return BigInt(value);
|
||||
return Number(value);
|
||||
}
|
||||
|
||||
const [state, setState] = useState<any>(`${pluginSettings[id] ?? option.default ?? 0}`);
|
||||
const [state, setState] = useState<any>(`${pluginSettings[id] ?? setting.default ?? 0}`);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
function handleChange(newValue: any) {
|
||||
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
||||
const isValid = setting.isValid?.call(definedSettings, newValue) ?? true;
|
||||
|
||||
setError(resolveError(isValid));
|
||||
|
||||
|
|
@ -41,7 +42,7 @@ export function NumberSetting({ option, pluginSettings, definedSettings, id, onC
|
|||
onChange(serialize(newValue));
|
||||
}
|
||||
|
||||
if (option.type === OptionType.NUMBER && BigInt(newValue) >= MAX_SAFE_NUMBER) {
|
||||
if (setting.type === OptionType.NUMBER && BigInt(newValue) >= MAX_SAFE_NUMBER) {
|
||||
setState(`${Number.MAX_SAFE_INTEGER}`);
|
||||
} else {
|
||||
setState(newValue);
|
||||
|
|
@ -49,15 +50,15 @@ export function NumberSetting({ option, pluginSettings, definedSettings, id, onC
|
|||
}
|
||||
|
||||
return (
|
||||
<SettingsSection name={id} description={option.description} error={error}>
|
||||
<SettingsSection name={id} description={setting.description} error={error}>
|
||||
<TextInput
|
||||
type="number"
|
||||
pattern="-?[0-9]+"
|
||||
placeholder={option.placeholder ?? "Enter a number"}
|
||||
placeholder={setting.placeholder ?? "Enter a number"}
|
||||
value={state}
|
||||
onChange={handleChange}
|
||||
disabled={option.disabled?.call(definedSettings) ?? false}
|
||||
{...option.componentProps}
|
||||
disabled={isSettingDisabled(definedSettings, setting)}
|
||||
{...setting.componentProps}
|
||||
/>
|
||||
</SettingsSection>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -16,19 +16,20 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { PluginOptionSelect } from "@utils/types";
|
||||
import { isSettingDisabled } from "@api/PluginManager";
|
||||
import { PluginSettingSelectDef } from "@utils/types";
|
||||
import { React, Select, useState } from "@webpack/common";
|
||||
|
||||
import { resolveError, SettingProps, SettingsSection } from "./Common";
|
||||
|
||||
export function SelectSetting({ option, pluginSettings, definedSettings, onChange, id }: SettingProps<PluginOptionSelect>) {
|
||||
const def = pluginSettings[id] ?? option.options?.find(o => o.default)?.value;
|
||||
export function SelectSetting({ setting, pluginSettings, definedSettings, onChange, id }: SettingProps<PluginSettingSelectDef>) {
|
||||
const def = pluginSettings[id] ?? setting.options?.find(o => o.default)?.value;
|
||||
|
||||
const [state, setState] = useState<any>(def ?? null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
function handleChange(newValue: any) {
|
||||
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
||||
const isValid = setting.isValid?.call(definedSettings, newValue) ?? true;
|
||||
|
||||
setState(newValue);
|
||||
setError(resolveError(isValid));
|
||||
|
|
@ -39,17 +40,17 @@ export function SelectSetting({ option, pluginSettings, definedSettings, onChang
|
|||
}
|
||||
|
||||
return (
|
||||
<SettingsSection name={id} description={option.description} error={error}>
|
||||
<SettingsSection name={id} description={setting.description} error={error}>
|
||||
<Select
|
||||
placeholder={option.placeholder ?? "Select an option"}
|
||||
options={option.options}
|
||||
placeholder={setting.placeholder ?? "Select an option"}
|
||||
options={setting.options}
|
||||
maxVisibleItems={5}
|
||||
closeOnSelect={true}
|
||||
select={handleChange}
|
||||
isSelected={v => v === state}
|
||||
serialize={v => String(v)}
|
||||
isDisabled={option.disabled?.call(definedSettings) ?? false}
|
||||
{...option.componentProps}
|
||||
isDisabled={isSettingDisabled(definedSettings, setting)}
|
||||
{...setting.componentProps}
|
||||
/>
|
||||
</SettingsSection>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -16,18 +16,19 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { PluginOptionSlider } from "@utils/types";
|
||||
import { isSettingDisabled } from "@api/PluginManager";
|
||||
import { PluginSettingSliderDef } from "@utils/types";
|
||||
import { React, Slider, useState } from "@webpack/common";
|
||||
|
||||
import { resolveError, SettingProps, SettingsSection } from "./Common";
|
||||
|
||||
export function SliderSetting({ option, pluginSettings, definedSettings, id, onChange }: SettingProps<PluginOptionSlider>) {
|
||||
const def = pluginSettings[id] ?? option.default;
|
||||
export function SliderSetting({ setting, pluginSettings, definedSettings, id, onChange }: SettingProps<PluginSettingSliderDef>) {
|
||||
const def = pluginSettings[id] ?? setting.default;
|
||||
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
function handleChange(newValue: number): void {
|
||||
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
||||
const isValid = setting.isValid?.call(definedSettings, newValue) ?? true;
|
||||
|
||||
setError(resolveError(isValid));
|
||||
|
||||
|
|
@ -37,17 +38,17 @@ export function SliderSetting({ option, pluginSettings, definedSettings, id, onC
|
|||
}
|
||||
|
||||
return (
|
||||
<SettingsSection name={id} description={option.description} error={error}>
|
||||
<SettingsSection name={id} description={setting.description} error={error}>
|
||||
<Slider
|
||||
markers={option.markers}
|
||||
minValue={option.markers[0]}
|
||||
maxValue={option.markers[option.markers.length - 1]}
|
||||
markers={setting.markers}
|
||||
minValue={setting.markers[0]}
|
||||
maxValue={setting.markers[setting.markers.length - 1]}
|
||||
initialValue={def}
|
||||
onValueChange={handleChange}
|
||||
onValueRender={(v: number) => String(v.toFixed(2))}
|
||||
stickToMarkers={option.stickToMarkers ?? true}
|
||||
disabled={option.disabled?.call(definedSettings) ?? false}
|
||||
{...option.componentProps}
|
||||
stickToMarkers={setting.stickToMarkers ?? true}
|
||||
disabled={isSettingDisabled(definedSettings, setting)}
|
||||
{...setting.componentProps}
|
||||
/>
|
||||
</SettingsSection>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -16,17 +16,18 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { PluginOptionString } from "@utils/types";
|
||||
import { isSettingDisabled } from "@api/PluginManager";
|
||||
import { PluginSettingStringDef } from "@utils/types";
|
||||
import { React, TextArea, TextInput, useState } from "@webpack/common";
|
||||
|
||||
import { resolveError, SettingProps, SettingsSection } from "./Common";
|
||||
|
||||
export function TextSetting({ option, pluginSettings, definedSettings, id, onChange }: SettingProps<PluginOptionString>) {
|
||||
const [state, setState] = useState(pluginSettings[id] ?? option.default ?? null);
|
||||
export function TextSetting({ setting, pluginSettings, definedSettings, id, onChange }: SettingProps<PluginSettingStringDef>) {
|
||||
const [state, setState] = useState(pluginSettings[id] ?? setting.default ?? null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
function handleChange(newValue: string) {
|
||||
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
||||
const isValid = setting.isValid?.call(definedSettings, newValue) ?? true;
|
||||
|
||||
setState(newValue);
|
||||
setError(resolveError(isValid));
|
||||
|
|
@ -37,22 +38,22 @@ export function TextSetting({ option, pluginSettings, definedSettings, id, onCha
|
|||
}
|
||||
|
||||
return (
|
||||
<SettingsSection name={id} description={option.description} error={error}>
|
||||
{option.multiline
|
||||
<SettingsSection name={id} description={setting.description} error={error}>
|
||||
{setting.multiline
|
||||
? <TextArea
|
||||
placeholder={option.placeholder ?? "Enter a value"}
|
||||
placeholder={setting.placeholder ?? "Enter a value"}
|
||||
value={state}
|
||||
onChange={handleChange}
|
||||
disabled={option.disabled?.call(definedSettings) ?? false}
|
||||
{...option.componentProps} />
|
||||
disabled={isSettingDisabled(definedSettings, setting)}
|
||||
{...setting.componentProps} />
|
||||
: <TextInput
|
||||
type="text"
|
||||
placeholder={option.placeholder ?? "Enter a value"}
|
||||
placeholder={setting.placeholder ?? "Enter a value"}
|
||||
value={state}
|
||||
onChange={handleChange}
|
||||
maxLength={null}
|
||||
disabled={option.disabled?.call(definedSettings) ?? false}
|
||||
{...option.componentProps}
|
||||
disabled={isSettingDisabled(definedSettings, setting)}
|
||||
{...setting.componentProps}
|
||||
/>
|
||||
}
|
||||
</SettingsSection>
|
||||
|
|
|
|||
|
|
@ -36,7 +36,8 @@ import { Logger } from "@utils/Logger";
|
|||
import { Margins } from "@utils/margins";
|
||||
import { classes } from "@utils/misc";
|
||||
import { useAwaiter, useIntersection } from "@utils/react";
|
||||
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";
|
||||
|
|
@ -143,7 +144,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 () => {
|
||||
|
|
@ -194,33 +196,20 @@ 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 enabled = isPluginEnabled(plugin.name);
|
||||
const { status, tags } = searchValue;
|
||||
|
||||
switch (status) {
|
||||
case SearchStatus.DISABLED:
|
||||
if (enabled) return false;
|
||||
if (isPluginEnabled(plugin.name)) return false;
|
||||
break;
|
||||
case SearchStatus.ENABLED:
|
||||
if (!enabled) return false;
|
||||
if (!isPluginEnabled(plugin.name)) return false;
|
||||
break;
|
||||
case SearchStatus.EQUICORD:
|
||||
if (!PluginMeta[plugin.name].folderName.startsWith("src/equicordplugins/")) return false;
|
||||
|
|
@ -239,12 +228,15 @@ 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.name.match(/[A-Z]/g)?.join("").toLowerCase().includes(search) || // acronyms like BF for BetterFolders
|
||||
plugin.description.toLowerCase().includes(search) ||
|
||||
plugin.tags?.some(t => t.toLowerCase().includes(search))
|
||||
plugin.searchTerms?.some(t => t.toLowerCase().includes(search))
|
||||
);
|
||||
}, [searchValue, search]);
|
||||
|
||||
|
|
@ -406,31 +398,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>
|
||||
|
||||
|
|
@ -456,6 +462,7 @@ export default function PluginSettings() {
|
|||
<HeadingTertiary className={classes(Margins.top20, Margins.bottom8)}>
|
||||
Required Plugins
|
||||
</HeadingTertiary>
|
||||
|
||||
<div className={cl("grid")}>
|
||||
{requiredPlugins.length
|
||||
? requiredPlugins
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -90,10 +90,10 @@
|
|||
height: 1.25em;
|
||||
}
|
||||
|
||||
.vc-settings-modal {
|
||||
padding: 1.5em !important;
|
||||
.vc-settings-modal-root {
|
||||
background: var(--background-base-low);
|
||||
}
|
||||
|
||||
.vc-settings-modal-close {
|
||||
float: right;
|
||||
.vc-settings-modal {
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ export function Updatable(props: CommonProps) {
|
|||
) : isOutdated ? (
|
||||
<>
|
||||
<Paragraph>
|
||||
There {updates.length === 1 ? "is 1 update" : `are ${updates.length} updates`} available. Click the button below to download and install.
|
||||
There {updates.length === 1 ? "is 1 update" : `are ${updates.length} updates`} available. Click the button above to download and install.
|
||||
</Paragraph>
|
||||
<Changes updates={updates} {...props} />
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -20,14 +20,14 @@ 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";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { identity, isAnyPluginDev } from "@utils/misc";
|
||||
import { relaunch } from "@utils/native";
|
||||
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";
|
||||
|
|
@ -64,45 +64,38 @@ function EquicordSettings() {
|
|||
title: string;
|
||||
description?: string;
|
||||
restartRequired?: boolean;
|
||||
warning: { enabled: boolean; message?: string; };
|
||||
}
|
||||
> = [
|
||||
warning?: string;
|
||||
}>
|
||||
= [
|
||||
{
|
||||
key: "useQuickCss",
|
||||
title: "Enable Custom CSS",
|
||||
description: "Load custom CSS from the QuickCSS editor. This allows you to customize Discord's appearance with your own styles.",
|
||||
restartRequired: true,
|
||||
warning: { enabled: false },
|
||||
},
|
||||
!IS_WEB && {
|
||||
key: "enableReactDevtools",
|
||||
title: "Enable React Developer Tools",
|
||||
description: "Enable the React Developer Tools extension for debugging Discord's React components. Useful for plugin development.",
|
||||
restartRequired: true,
|
||||
warning: { enabled: false },
|
||||
},
|
||||
(!IS_WEB && !IS_DISCORD_DESKTOP || !IS_WINDOWS) && {
|
||||
key: "mainWindowFrameless",
|
||||
title: "Disable the Main Window Frame",
|
||||
description: "Remove the native window frame for a cleaner look. You can still move the window by dragging the title bar area.",
|
||||
restartRequired: true,
|
||||
warning: { enabled: false },
|
||||
},
|
||||
!IS_WEB &&
|
||||
(!IS_DISCORD_DESKTOP || !IS_WINDOWS
|
||||
!IS_WEB && (!IS_DISCORD_DESKTOP || !IS_WINDOWS
|
||||
? {
|
||||
key: "frameless",
|
||||
title: "Disable All Window Frames",
|
||||
description: "Remove the native window frame for a cleaner look. You can still move the window by dragging the title bar area.",
|
||||
restartRequired: true,
|
||||
warning: { enabled: false },
|
||||
}
|
||||
: {
|
||||
key: "winNativeTitleBar",
|
||||
title: "Use Windows' native title bar instead of Discord's custom one",
|
||||
description: "Replace Discord's custom title bar with the standard Windows title bar. This may improve compatibility with some window management tools.",
|
||||
restartRequired: true,
|
||||
warning: { enabled: false },
|
||||
}
|
||||
),
|
||||
!IS_WEB && {
|
||||
|
|
@ -110,27 +103,21 @@ function EquicordSettings() {
|
|||
title: "Enable Window Transparency",
|
||||
description: "Make the Discord window transparent. A theme that supports transparency is required or this will do nothing.",
|
||||
restartRequired: true,
|
||||
warning: {
|
||||
enabled: true,
|
||||
message: IS_WINDOWS
|
||||
? "This will stop the window from being resizable and prevents you from snapping the window to screen edges."
|
||||
: "This will stop the window from being resizable.",
|
||||
},
|
||||
warning: IS_WINDOWS
|
||||
? "This will stop the window from being resizable and prevents you from snapping the window to screen edges."
|
||||
: "This will stop the window from being resizable.",
|
||||
},
|
||||
IS_DISCORD_DESKTOP && {
|
||||
key: "disableMinSize",
|
||||
title: "Disable Minimum Window Size",
|
||||
description: "Allow the Discord window to be resized smaller than its default minimum size. Useful for tiling window managers or small screens.",
|
||||
restartRequired: true,
|
||||
warning: { enabled: false },
|
||||
},
|
||||
!IS_WEB &&
|
||||
IS_WINDOWS && {
|
||||
!IS_WEB && IS_WINDOWS && {
|
||||
key: "winCtrlQ",
|
||||
title: "Register Ctrl+Q as shortcut to close Discord",
|
||||
description: "Add Ctrl+Q as a keyboard shortcut to close Discord. This provides an alternative to Alt+F4 for quickly closing the application.",
|
||||
restartRequired: true,
|
||||
warning: { enabled: false },
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -249,14 +236,26 @@ 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 ? (
|
||||
s.warning ? (
|
||||
<>
|
||||
{s.description}
|
||||
<Notice.Warning className={Margins.top8} style={{ width: "100%" }}>
|
||||
{s.warning.message}
|
||||
{s.warning}
|
||||
</Notice.Warning>
|
||||
</>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -9,16 +9,16 @@ 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();
|
||||
let v: Record<PropertyKey, string> | null = null;
|
||||
let chunksMap: unknown;
|
||||
|
||||
Object.defineProperty(Object.prototype, sym, {
|
||||
get() {
|
||||
v = this;
|
||||
chunksMap = this;
|
||||
return "";
|
||||
},
|
||||
configurable: true
|
||||
|
|
@ -27,7 +27,7 @@ function getWebpackChunkMap() {
|
|||
wreq.u(sym);
|
||||
delete Object.prototype[sym];
|
||||
|
||||
return v;
|
||||
return chunksMap as Record<PropertyKey, string> | null;
|
||||
}
|
||||
|
||||
export async function loadLazyChunks() {
|
||||
|
|
@ -54,7 +54,10 @@ export async function loadLazyChunks() {
|
|||
|
||||
async function searchAndLoadLazyChunks(factoryCode: string) {
|
||||
// Workaround to avoid loading the CSS debugging chunk which turns the app pink
|
||||
const hasCssDebuggingLoad = foundCssDebuggingLoad ? false : (foundCssDebuggingLoad = factoryCode.includes(".cssDebuggingEnabled&&"));
|
||||
// const hasCssDebuggingLoad = foundCssDebuggingLoad ? false : (foundCssDebuggingLoad = factoryCode.includes(".cssDebuggingEnabled&&"));
|
||||
|
||||
// Disabled for now since this causes lots of chunks concatenated into the same module get marked as invalid, and thus not loaded.
|
||||
const hasCssDebuggingLoad = foundCssDebuggingLoad = false;
|
||||
|
||||
const lazyChunks = factoryCode.matchAll(hasCssDebuggingLoad ? CompleteLazyChunkRegex : PartialLazyChunkRegex);
|
||||
const validChunkGroups = new Set<[chunkIds: PropertyKey[], entryPoint: PropertyKey]>();
|
||||
|
|
@ -66,7 +69,7 @@ export async function loadLazyChunks() {
|
|||
?.matchAll(Webpack.ChunkIdsRegex)
|
||||
.map(m => {
|
||||
const numChunkId = Number(m[1]);
|
||||
return Number.isNaN(numChunkId) ? m[1] : numChunkId;
|
||||
return Number.isNaN(numChunkId) ? m[1] : String(numChunkId);
|
||||
})
|
||||
.toArray()
|
||||
?? [];
|
||||
|
|
@ -107,7 +110,7 @@ export async function loadLazyChunks() {
|
|||
|
||||
if (!invalidChunkGroup) {
|
||||
const numEntryPoint = Number(entryPoint);
|
||||
validChunkGroups.add([chunkIds, Number.isNaN(numEntryPoint) ? entryPoint : numEntryPoint]);
|
||||
validChunkGroups.add([chunkIds, Number.isNaN(numEntryPoint) ? entryPoint : String(numEntryPoint)]);
|
||||
}
|
||||
}));
|
||||
|
||||
|
|
@ -178,10 +181,10 @@ export async function loadLazyChunks() {
|
|||
}
|
||||
|
||||
// All chunks Discord has mapped to asset files, even if they are not used anymore
|
||||
const chunkMap = getWebpackChunkMap();
|
||||
if (!chunkMap) throw new Error("Failed to get chunk map");
|
||||
const chunksMap = getWebpackChunkMap();
|
||||
if (!chunksMap) throw new Error("Failed to get chunk map");
|
||||
|
||||
const allChunks = Object.keys(chunkMap).map(id => Number.isNaN(Number(id)) ? id : Number(id));
|
||||
const allChunks = Object.keys(chunksMap);
|
||||
if (allChunks.length === 0) throw new Error("Failed to get all chunks");
|
||||
|
||||
// Chunks which our regex could not catch to load
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export interface ReporterData {
|
|||
};
|
||||
failedWebpack: Record<TypeWebpackSearchHistory, string[][]>;
|
||||
}
|
||||
|
||||
export const reporterData: ReporterData = {
|
||||
failedPatches: {
|
||||
foundNoModule: [],
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ async function runReporter() {
|
|||
addPatch({
|
||||
find: '"Could not find app-mount"',
|
||||
replacement: {
|
||||
match: /(?<="use strict";)/,
|
||||
replace: "Vencord.Webpack._initReporter();"
|
||||
match: /"Could not find app-mount"/,
|
||||
replace: "(Vencord.Webpack._initReporter(),$&)"
|
||||
}
|
||||
}, "Equicord Reporter");
|
||||
|
||||
|
|
@ -39,6 +39,13 @@ async function runReporter() {
|
|||
|
||||
await loadLazyChunksDone;
|
||||
|
||||
// Manually require all modules to make sure all lazily required modules are patched
|
||||
for (const moduleId of Object.keys(Webpack.wreq.m)) {
|
||||
try {
|
||||
Webpack.wreq(moduleId);
|
||||
} catch { }
|
||||
}
|
||||
|
||||
if (IS_REPORTER && IS_WEB && !IS_VESKTOP && !IS_EQUIBOP) {
|
||||
console.log("[REPORTER_META]", {
|
||||
buildNumber: getBuildNumber(),
|
||||
|
|
@ -55,7 +62,7 @@ async function runReporter() {
|
|||
}
|
||||
|
||||
for (const [plugin, moduleId, match, totalTime] of patchTimings) {
|
||||
if (totalTime > 5) {
|
||||
if (totalTime > 10) {
|
||||
new Logger("WebpackPatcher").warn(`Patch by ${plugin} took ${Math.round(totalTime * 100) / 100}ms (Module id is ${String(moduleId)}): ${match}`);
|
||||
}
|
||||
}
|
||||
|
|
@ -70,7 +77,6 @@ async function runReporter() {
|
|||
else method = "find";
|
||||
}
|
||||
if (searchType === "waitForStore") method = "findStore";
|
||||
if (searchType === "waitForStore" && args[0] === "PermissionStore") continue;
|
||||
|
||||
let result: any;
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export default definePlugin({
|
|||
authors: [Devs.thororen],
|
||||
patches: [
|
||||
{
|
||||
find: /\.POPOUT,onClose:\i}\),nicknameIcons:.+?\.isProvisional/,
|
||||
find: "#{intl::USER_PROFILE_FRIEND_REQUEST_TOAST}",
|
||||
replacement: {
|
||||
match: /user:\i,widgets:.{0,100}?\}\),/,
|
||||
replace: "$&Vencord.Api.ProfileCollections.renderProfileCollections(arguments[0]),",
|
||||
|
|
|
|||
40
src/equicordplugins/_api/profileSections.ts
Normal file
40
src/equicordplugins/_api/profileSections.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2026 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
|
||||
export default definePlugin({
|
||||
name: "ProfileSectionsAPI",
|
||||
description: "API to add sections near the 'Member Since' area of user profile panels.",
|
||||
authors: [Devs.thororen],
|
||||
patches: [
|
||||
// DM User Sidebar
|
||||
{
|
||||
find: "#{intl::PREMIUM_GIFTING_BUTTON}),action:",
|
||||
replacement: {
|
||||
match: /(#{intl::USER_PROFILE_MEMBER_SINCE}\),.{0,100}userId:(\i\.id)}\)}\))/,
|
||||
replace: "$1,Vencord.Api.ProfileSections.renderProfileSections({userId:$2,isSideBar:true})",
|
||||
}
|
||||
},
|
||||
// User Profile Modal
|
||||
{
|
||||
find: ",applicationRoleConnection:",
|
||||
replacement: {
|
||||
match: /(#{intl::USER_PROFILE_MEMBER_SINCE}\),.{0,100}userId:(\i\.id),.{0,100}}\)}\)),/,
|
||||
replace: "$1,Vencord.Api.ProfileSections.renderProfileSections({userId:$2,isSideBar:false}),",
|
||||
}
|
||||
},
|
||||
// User Profile Modal v2
|
||||
{
|
||||
find: ".MODAL_V2,onClose:",
|
||||
replacement: {
|
||||
match: /(#{intl::USER_PROFILE_MEMBER_SINCE}\),.{0,100}userId:(\i\.id),.{0,100}}\)}\)),/,
|
||||
replace: "$1,Vencord.Api.ProfileSections.renderProfileSections({userId:$2,isSideBar:false}),",
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
|
@ -22,8 +22,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
|
||||
{
|
||||
|
|
|
|||
32
src/equicordplugins/_core/concatenatedModules.tsx
Normal file
32
src/equicordplugins/_core/concatenatedModules.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2026 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { IconsDef } from "@equicordplugins/iconViewer/types";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
|
||||
export let iconsModule: IconsDef;
|
||||
|
||||
export default definePlugin({
|
||||
name: "ConcatenatedModules",
|
||||
description: "Extract modules that have been concatenated by the bundler",
|
||||
authors: [Devs.thororen],
|
||||
|
||||
required: true,
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: "AngleBracketsIcon",
|
||||
replacement: {
|
||||
match: /\i\.\i\((\i)\),\i\.\i\(\i,\{AIcon/,
|
||||
replace: "$self.iconsModule($1),$&"
|
||||
}
|
||||
}
|
||||
],
|
||||
iconsModule(value: IconsDef) {
|
||||
iconsModule = value;
|
||||
},
|
||||
});
|
||||
|
|
@ -10,6 +10,7 @@ import definePlugin from "@utils/types";
|
|||
export default definePlugin({
|
||||
name: "AltKrispSwitch",
|
||||
description: "Makes the Noise Suppression Popout switch between None and Krisp instead of Krisp and Standard",
|
||||
tags: ["Customisation", "Voice"],
|
||||
authors: [Devs.newwares],
|
||||
patches: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,24 +5,21 @@
|
|||
*/
|
||||
|
||||
import { Devs } from "@utils/constants";
|
||||
import { openUserProfile } from "@utils/discord";
|
||||
import definePlugin from "@utils/types";
|
||||
|
||||
export default definePlugin({
|
||||
name: "AlwaysExpandProfiles",
|
||||
description: "Always expands profile popouts to the full modal",
|
||||
tags: ["Appearance", "Utility"],
|
||||
authors: [Devs.thororen],
|
||||
patches: [
|
||||
{
|
||||
find: '"view-profile"',
|
||||
replacement: {
|
||||
match: /("PRESS_VIEW_PROFILE".{0,200})return(?<=userId:(\i\.id).*?)/,
|
||||
replace: "$1return $self.openUserModal($2);"
|
||||
match: /(\i)=.{0,45}\(0,\i\.openUserProfileModal.{0,300}(?=return)/,
|
||||
replace: "$&return $1();"
|
||||
},
|
||||
all: true
|
||||
},
|
||||
],
|
||||
openUserModal(userId: string) {
|
||||
openUserProfile(userId);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -193,6 +193,7 @@ async function playSound(buffer: AudioBuffer, volume: number) {
|
|||
export default definePlugin({
|
||||
name: "Animalese",
|
||||
description: "Plays animal crossing animalese for every message sent (they yap a lot)",
|
||||
tags: ["Customisation", "Fun"],
|
||||
authors: [EquicordDevs.ryanamay, EquicordDevs.Mocha],
|
||||
settings,
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export default definePlugin({
|
|||
name: "AtSomeone",
|
||||
authors: [Devs.Joona],
|
||||
description: "Mention someone randomly",
|
||||
tags: ["Chat", "Fun"],
|
||||
patches: [
|
||||
{
|
||||
find: ".LAUNCHABLE_APPLICATIONS;",
|
||||
|
|
|
|||
|
|
@ -209,6 +209,7 @@ function handlePaste(event: ClipboardEvent) {
|
|||
export default definePlugin({
|
||||
name: "AutoZipper",
|
||||
description: "Automatically zips specified file types and folders before uploading to Discord",
|
||||
tags: ["Chat", "Organisation"],
|
||||
authors: [EquicordDevs.SSnowly],
|
||||
settings,
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ const DATASTORE_KEY = "bannersEverywhere";
|
|||
export default definePlugin({
|
||||
name: "BannersEverywhere",
|
||||
description: "Displays banners in the member list ",
|
||||
tags: ["Appearance", "Customisation"],
|
||||
authors: [Devs.ImLvna, Devs.AutumnVN],
|
||||
settings,
|
||||
patches: [
|
||||
|
|
|
|||
|
|
@ -107,6 +107,8 @@ const settings = definePluginSettings({
|
|||
export default definePlugin({
|
||||
name: "DecodeBase64",
|
||||
description: "Decode base64 content of any message and copy the decoded content.",
|
||||
dependencies: ["MessagePopoverAPI"],
|
||||
tags: ["Appearance", "Customisation", "Chat"],
|
||||
authors: [EquicordDevs.ThePirateStoner],
|
||||
settings,
|
||||
messagePopoverButton: {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { cl } from "../utils";
|
|||
import { Caret } from "./Caret";
|
||||
|
||||
export function CarouselControls({ activities, currentActivity, onActivityChange }: CarouselControlsProps) {
|
||||
const currentIndex = activities.indexOf(currentActivity);
|
||||
const currentIndex = activities.findIndex(a => a.id === currentActivity.id);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -48,7 +48,7 @@ export function CarouselControls({ activities, currentActivity, onActivityChange
|
|||
<div
|
||||
key={"dot--" + index}
|
||||
onClick={() => onActivityChange(activity)}
|
||||
className={cl("controls-dot", currentActivity === activity && "controls-selected")} />
|
||||
className={cl("controls-dot", currentActivity.id === activity.id && "controls-selected")} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export default definePlugin({
|
|||
name: "BetterActivities",
|
||||
description: "Shows activity icons in the member list and allows showing all activities",
|
||||
authors: [Devs.D3SOX, Devs.Arjix, Devs.AutumnVN, Devs.thororen],
|
||||
tags: ["activity"],
|
||||
tags: ["Activity"],
|
||||
settings,
|
||||
patchActivityList,
|
||||
showAllActivitiesComponent,
|
||||
|
|
@ -30,7 +30,12 @@ export default definePlugin({
|
|||
find: '"ActivityStatus"),',
|
||||
replacement: [
|
||||
{
|
||||
match: /(?<=className:\i,children:\[).*?(?=\i\(\),\i&&)/g,
|
||||
match: /(?<=className:\i,children:\[).*?(?=\i\(\),\i&&)/,
|
||||
replace: "",
|
||||
predicate: () => settings.store.removeGameActivityStatus,
|
||||
},
|
||||
{
|
||||
match: /(?<=className:\i,children:\[\i\(\),)null.*?tooltipClassName:\i\}\),/,
|
||||
replace: "",
|
||||
predicate: () => settings.store.removeGameActivityStatus,
|
||||
},
|
||||
|
|
@ -45,7 +50,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
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Activity } from "@vencord/discord-types";
|
||||
import { findComponentByCodeLazy } from "@webpack";
|
||||
import { React, Tooltip } from "@webpack/common";
|
||||
import { JSX } from "react";
|
||||
|
|
@ -18,11 +19,12 @@ import { cl, getApplicationIcons } from "../utils";
|
|||
// Discord no longer shows an icon here by default but we use the one from the popout now here
|
||||
const DefaultActivityIcon = findComponentByCodeLazy("M5 2a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3V5a3 3 0 0 0-3-3H5Zm6.81 7c-.54 0-1 .26-1.23.61A1 1 0 0 1 8.92 8.5 3.49 3.49 0 0 1 11.82 7c1.81 0 3.43 1.38 3.43 3.25 0 1.45-.98 2.61-2.27 3.06a1 1 0 0 1-1.96.37l-.19-1a1 1 0 0 1 .98-1.18c.87 0 1.44-.63 1.44-1.25S12.68 9 11.81 9ZM13 16a1 1 0 1 1-2 0 1 1 0 0 1 2 0Zm7-10.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0ZM18.5 20a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM7 18.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0ZM5.5 7a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z");
|
||||
|
||||
export function patchActivityList({ activities, user, hideTooltip }: ActivityListProps): JSX.Element | null {
|
||||
export function patchActivityList({ activities: rawActivities, user, hideTooltip }: ActivityListProps): JSX.Element | null {
|
||||
const icons: ActivityListIcon[] = [];
|
||||
|
||||
if (user.bot || settings.store.hideTooltip && hideTooltip) return null;
|
||||
|
||||
const activities = rawActivities.filter((a): a is Activity => a != null);
|
||||
const applicationIcons = getApplicationIcons(activities);
|
||||
if (applicationIcons.length) {
|
||||
const compareImageSource = (a: ApplicationIcon, b: ApplicationIcon) => {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export function showAllActivitiesComponent({ activity, user, ...props }: Readonl
|
|||
|
||||
const activities = useStateFromStores(
|
||||
[PresenceStore],
|
||||
() => PresenceStore.getActivities(user.id).filter((activity: Activity) => activity.type !== 4)
|
||||
() => PresenceStore.getActivities(user.id).filter((activity: Activity) => activity != null && activity.type !== 4)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -33,8 +33,12 @@ export function showAllActivitiesComponent({ activity, user, ...props }: Readonl
|
|||
return;
|
||||
}
|
||||
|
||||
if (!currentActivity || !activities.includes(currentActivity))
|
||||
const existing = currentActivity && activities.find(a => a.id === currentActivity.id);
|
||||
if (!existing) {
|
||||
setCurrentActivity(activities[0]);
|
||||
} else if (existing !== currentActivity) {
|
||||
setCurrentActivity(existing);
|
||||
}
|
||||
}, [activities]);
|
||||
|
||||
// we use these for other activities, it would be better to somehow get the corresponding activity props
|
||||
|
|
@ -47,6 +51,7 @@ export function showAllActivitiesComponent({ activity, user, ...props }: Readonl
|
|||
if (!activities.length) return null;
|
||||
|
||||
if (settings.store.allActivitiesStyle === "carousel") {
|
||||
if (!currentActivity) return null;
|
||||
return (
|
||||
<ErrorBoundary noop>
|
||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export function getActivityApplication(activity: Activity | null) {
|
|||
|
||||
export function getApplicationIcons(activities: Activity[], preferSmall = false): ApplicationIcon[] {
|
||||
const applicationIcons: ApplicationIcon[] = [];
|
||||
const applications = activities.filter(activity => activity.application_id || activity.platform || activity?.id?.startsWith("spotify:"));
|
||||
const applications = activities.filter(activity => activity != null && (activity.application_id || activity.platform || activity.id?.startsWith("spotify:")));
|
||||
|
||||
for (const activity of applications) {
|
||||
const { assets, application_id, platform, id } = activity;
|
||||
|
|
|
|||
|
|
@ -274,6 +274,7 @@ const settings = definePluginSettings({
|
|||
export default definePlugin({
|
||||
name: "BetterAudioPlayer",
|
||||
description: "Adds a spectrograph and oscilloscope visualizer to audio attachment players.",
|
||||
tags: ["Appearance", "Media", "Voice"],
|
||||
authors: [EquicordDevs.creations],
|
||||
settings,
|
||||
|
||||
|
|
|
|||
|
|
@ -9,48 +9,82 @@ import "./styles.css";
|
|||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Heading } from "@components/Heading";
|
||||
import { DeleteIcon, PlusIcon } from "@components/Icons";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { Devs, EquicordDevs } from "@utils/constants";
|
||||
import { classNameFactory } from "@utils/css";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { Button, TextInput } from "@webpack/common";
|
||||
import { Button, Select, TextInput, useState } from "@webpack/common";
|
||||
|
||||
const cl = classNameFactory("vc-bbr-");
|
||||
|
||||
interface BanReason {
|
||||
text: string;
|
||||
deleteSeconds?: number;
|
||||
}
|
||||
|
||||
const DELETE_DURATION_OPTIONS = [
|
||||
{ label: "Discord default", value: null },
|
||||
{ label: "Don't delete any", value: 0 },
|
||||
{ label: "Previous hour", value: 3600 },
|
||||
{ label: "Previous 6 hours", value: 21600 },
|
||||
{ label: "Previous 12 hours", value: 43200 },
|
||||
{ label: "Previous 24 hours", value: 86400 },
|
||||
{ label: "Previous 3 days", value: 259200 },
|
||||
{ label: "Previous 7 days", value: 604800 },
|
||||
];
|
||||
|
||||
function normalizeReason(r: BanReason | string): BanReason {
|
||||
return typeof r === "string" ? { text: r } : r;
|
||||
}
|
||||
|
||||
function getStoredReasons(): BanReason[] {
|
||||
return (settings.store.reasons as (BanReason | string)[]).map(normalizeReason);
|
||||
}
|
||||
|
||||
function ReasonsComponent() {
|
||||
const { reasons } = settings.store;
|
||||
const [reasons, setReasons] = useState<BanReason[]>(getStoredReasons);
|
||||
|
||||
const update = (next: BanReason[]) => {
|
||||
setReasons(next);
|
||||
settings.store.reasons = next;
|
||||
};
|
||||
|
||||
return (
|
||||
<section>
|
||||
<Heading>Reasons</Heading>
|
||||
{reasons.map((r, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={cl("reason-wrapper")}
|
||||
>
|
||||
<div key={i} className={cl("reason-wrapper")}>
|
||||
<TextInput
|
||||
value={r}
|
||||
onChange={v => {
|
||||
reasons[i] = v;
|
||||
settings.store.reasons = reasons;
|
||||
}}
|
||||
value={r.text}
|
||||
onChange={v => update(reasons.map((x, j) => j === i ? { ...x, text: v } : x))}
|
||||
placeholder="Reason"
|
||||
/>
|
||||
<Button
|
||||
className={cl("remove-button")}
|
||||
color={Button.Colors.TRANSPARENT}
|
||||
onClick={() => {
|
||||
reasons.splice(i, 1);
|
||||
settings.store.reasons = reasons;
|
||||
}}
|
||||
onClick={() => update(reasons.filter((_, j) => j !== i))}
|
||||
look={Button.Looks.FILLED}
|
||||
size={Button.Sizes.MIN}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</Button>
|
||||
<div className={cl("duration-row")}>
|
||||
<span className="vc-text-base vc-text-sm vc-text-normal vc-text-defaultColor vc-plugins-setting-description">Message Auto-delete duration</span>
|
||||
<Select
|
||||
options={DELETE_DURATION_OPTIONS}
|
||||
select={v => update(reasons.map((x, j) => j === i ? { ...x, deleteSeconds: v === null ? undefined : v } : x))}
|
||||
isSelected={v => (v ?? null) === (r.deleteSeconds ?? null)}
|
||||
serialize={v => v == null ? "default" : String(v)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className={cl("reason-wrapper")}>
|
||||
<Button onClick={() => settings.store.reasons.push("")} className={cl("add-button")} size={Button.Sizes.LARGE} color={Button.Colors.TRANSPARENT}>
|
||||
<Button
|
||||
onClick={() => update([...reasons, { text: "" }])}
|
||||
className={cl("add-button")}
|
||||
size={Button.Sizes.LARGE}
|
||||
color={Button.Colors.TRANSPARENT}
|
||||
>
|
||||
<PlusIcon /> Add another reason
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -62,7 +96,7 @@ const settings = definePluginSettings({
|
|||
reasons: {
|
||||
description: "Your custom reasons",
|
||||
type: OptionType.COMPONENT,
|
||||
default: [] as string[],
|
||||
default: [] as BanReason[],
|
||||
component: ReasonsComponent,
|
||||
},
|
||||
isTextInputDefault: {
|
||||
|
|
@ -74,30 +108,57 @@ const settings = definePluginSettings({
|
|||
export default definePlugin({
|
||||
name: "BetterBanReasons",
|
||||
description: "Create custom reasons to use in the Discord ban modal, and/or show a text input by default instead of the options.",
|
||||
authors: [Devs.Inbestigator],
|
||||
tags: ["Appearance", "Customisation"],
|
||||
authors: [Devs.Inbestigator, EquicordDevs.yonn2222],
|
||||
|
||||
durationSetter: null as ((v: number) => void) | null,
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: "#{intl::BAN_REASON_OPTION_SPAM_ACCOUNT}",
|
||||
replacement: [{
|
||||
match: /(\[\{name:\i\.\i\.\i\(\i\.\i\.\i\),.+?"other"\}\])/,
|
||||
replace: "$self.getReasons($1)"
|
||||
},
|
||||
{
|
||||
match: /useState\(null\)(?=.{0,300}targetUserId:)/,
|
||||
replace: "useState($self.getDefaultState())"
|
||||
}]
|
||||
replacement: [
|
||||
{
|
||||
match: /(\[\{name:\i\.\i\.\i\(\i\.\i\.\i\),.+?"other"\}\])/,
|
||||
replace: "$self.getReasons($1)"
|
||||
},
|
||||
{
|
||||
match: /useState\(null\)(?=.{0,300}targetUserId:)/,
|
||||
replace: "useState($self.getDefaultState())"
|
||||
},
|
||||
{
|
||||
match: /(\[\i,\i\])=(\i)\.useState\((null!=\i\?\i:\i)\)/,
|
||||
replace: "$1=$self.captureDeleteState($2.useState,$3)"
|
||||
},
|
||||
{
|
||||
match: /\i=\i\.useCallback\((\i)=>\{.{0,10},\i\(null\)(?=\},\[\]\))/,
|
||||
replace: "$&,$self.onReasonSelect($1)"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
getReasons(defaults) {
|
||||
const storedReasons = settings.store.reasons.filter((r: string) => r.trim());
|
||||
const reasons: string[] = storedReasons.length
|
||||
? storedReasons
|
||||
: [];
|
||||
|
||||
getReasons(defaults: { name: string; value: string; }[]) {
|
||||
const stored = getStoredReasons().filter(r => r.text.trim());
|
||||
return [
|
||||
...reasons.map(s => ({ name: s, value: s })),
|
||||
...defaults
|
||||
...stored.map(r => ({ name: r.text, value: r.text })),
|
||||
...defaults,
|
||||
];
|
||||
},
|
||||
|
||||
getDefaultState: () => settings.store.isTextInputDefault ? 1 : 0,
|
||||
|
||||
captureDeleteState(hook: (initial: number) => [number, (v: number) => void], initial: number) {
|
||||
const result = hook(initial);
|
||||
this.durationSetter = result[1];
|
||||
return result;
|
||||
},
|
||||
|
||||
onReasonSelect(value: string) {
|
||||
const reason = getStoredReasons().find(r => r.text === value);
|
||||
if (reason?.deleteSeconds !== undefined) {
|
||||
this.durationSetter?.(reason.deleteSeconds);
|
||||
}
|
||||
},
|
||||
|
||||
settings,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,6 +8,13 @@
|
|||
|
||||
.vc-bbr-remove-button {
|
||||
color: var(--text-muted);
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
.vc-bbr-duration-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.vc-bbr-remove-button:hover {
|
||||
|
|
|
|||
|
|
@ -17,21 +17,23 @@ let updateFunc = (v: any) => { };
|
|||
export default definePlugin({
|
||||
name: "BetterBlockedUsers",
|
||||
description: "Allows you to search in blocked users list and makes names selectable in settings.",
|
||||
tags: ["Appearance", "Shortcuts"],
|
||||
authors: [EquicordDevs.TheArmagan],
|
||||
patches: [
|
||||
{
|
||||
find: '"],{numberOfBlockedUsers:',
|
||||
group: true,
|
||||
replacement: [
|
||||
{
|
||||
match: /(?<=\}=(\i).*?\]\}\))/,
|
||||
replace: ",$1.listType==='blocked'?$self.renderSearchInput():null"
|
||||
match: /(?<=\(0,\i\.jsx\)\(\i,\{listType:(\i),numberOfUsers:\i\.length\}\),)/,
|
||||
replace: "$1==='blocked'?$self.renderSearchInput():null,"
|
||||
},
|
||||
{
|
||||
match: /(?<=userId:(\i).*?\}\)\]\}\),)(\(.*?loading:\i\}\))/,
|
||||
match: /(?<=\{userId:(\i).*?\.globalName.{0,25}\}\)\]\}\),)(\(.*?loading:\i\}\))/,
|
||||
replace: "$self.renderUser($1,$2)",
|
||||
},
|
||||
{
|
||||
match: /(?<=\}=(\i).{0,10}(\i).useState.{0,5};)/,
|
||||
match: /(?<=userIds:\i,listType:\i\}=(\i).{0,30}(\i)\.useState\(\d+\);)/,
|
||||
replace: "let [searchResults,setSearchResults]=$2.useState([]);$self.setUpdateFunc($1,setSearchResults);"
|
||||
},
|
||||
{
|
||||
|
|
@ -49,17 +51,19 @@ export default definePlugin({
|
|||
updateFunc(searchResults);
|
||||
}, []);
|
||||
|
||||
return <TextInput
|
||||
placeholder="Search users..."
|
||||
style={{ width: "200px" }}
|
||||
onInput={e => {
|
||||
const search = (e.target as HTMLInputElement).value.toLowerCase().trim();
|
||||
setValue(search);
|
||||
lastSearch = search;
|
||||
const searchResults = this.getFilteredUsers(search);
|
||||
updateFunc(searchResults);
|
||||
}} value={value}
|
||||
></TextInput>;
|
||||
return <div className="vc-bbu-search">
|
||||
<TextInput
|
||||
placeholder="Search users..."
|
||||
style={{ width: "200px" }}
|
||||
onInput={e => {
|
||||
const search = (e.target as HTMLInputElement).value.toLowerCase().trim();
|
||||
setValue(search);
|
||||
lastSearch = search;
|
||||
const searchResults = this.getFilteredUsers(search);
|
||||
updateFunc(searchResults);
|
||||
}} value={value}
|
||||
/>
|
||||
</div>;
|
||||
},
|
||||
renderUser(userId: string, rest: any) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
[class*="usersList"] [class*="text"] {
|
||||
user-select: text !important;
|
||||
}
|
||||
|
||||
.vc-bbu-search,
|
||||
.vc-bbu-search * {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
|
@ -33,6 +33,8 @@ function fetchIndex(target: object) {
|
|||
export default definePlugin({
|
||||
name: "BetterCommands",
|
||||
description: "Enhances the command system with miscellaneous improvements.",
|
||||
dependencies: ["CommandsAPI"],
|
||||
tags: ["Appearance", "Commands", "Shortcuts"],
|
||||
authors: [Devs.thororen],
|
||||
settings,
|
||||
patches: [
|
||||
|
|
|
|||
|
|
@ -34,13 +34,14 @@ function lurk(id: string) {
|
|||
export default definePlugin({
|
||||
name: "BetterInvites",
|
||||
description: "See invites expiration date, view inviter profile and preview servers before joining by clicking the name",
|
||||
tags: ["Appearance", "Customisation", "Chat", "Servers"],
|
||||
authors: [EquicordDevs.iamme, Devs.thororen],
|
||||
patches: [
|
||||
{
|
||||
find: "#{intl::xdCLeM::raw}",
|
||||
replacement: [
|
||||
{
|
||||
match: /profile:\i\}\),.{0,15}profile:\i/,
|
||||
match: /profile:\i\}\),.{0,20}profile:\i(?=.{0,20}profile:\i)/,
|
||||
replace: "$&,invite:arguments[0].invite"
|
||||
}
|
||||
]
|
||||
|
|
@ -85,8 +86,11 @@ export default definePlugin({
|
|||
);
|
||||
},
|
||||
Header(inviter: User | undefined, guildName: string) {
|
||||
if (!inviter) return null;
|
||||
|
||||
const userId = UserStore.getCurrentUser().id;
|
||||
if (!inviter || userId === inviter.id) return null;
|
||||
const isSelf = userId === inviter.id;
|
||||
|
||||
return (
|
||||
<div className="vc-bi-header-inner">
|
||||
<img
|
||||
|
|
@ -98,7 +102,9 @@ export default definePlugin({
|
|||
: "/assets/1f0bfc0865d324c2587920a7d80c609b.png?size=128"}
|
||||
/>
|
||||
<div className="vc-bi-header-text">
|
||||
{inviter.global_name || inviter.username} invited you to {guildName}
|
||||
{isSelf
|
||||
? `You sent an invite to join ${guildName}`
|
||||
: `${inviter.global_name || inviter.username} invited you to ${guildName}`}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
.vc-bi-inviter-avatar {
|
||||
margin: 0;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
position: static;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
|
@ -10,10 +10,10 @@
|
|||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 4px;
|
||||
margin: 0;
|
||||
margin-bottom: 5px;
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.vc-bi-header-text {
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
|
||||
.vc-bi-tooltip-icon {
|
||||
color: var(--text-muted);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-top: 1px;
|
||||
}
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-top: 1.5px;
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ export default definePlugin({
|
|||
name: "BetterPlusReacts",
|
||||
authors: [Devs.Joona],
|
||||
description: "The amount of plus before :emoji: is the message to add it to",
|
||||
tags: ["Chat", "Emotes"],
|
||||
patches: [
|
||||
{
|
||||
find: ".SLASH_COMMAND_USED,",
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ export function containsBlockedKeywords(message: Message) {
|
|||
export default definePlugin({
|
||||
name: "BlockKeywords",
|
||||
description: "Blocks messages containing specific user-defined keywords, as if the user sending them was blocked.",
|
||||
tags: ["Appearance", "Customisation", "Privacy"],
|
||||
authors: [EquicordDevs.catcraft],
|
||||
patches: [
|
||||
{
|
||||
|
|
@ -80,19 +81,26 @@ export default definePlugin({
|
|||
replace: "$&$1=$self.blockMessagesWithKeywords($1);"
|
||||
}
|
||||
},
|
||||
...[
|
||||
'"MessageStore"',
|
||||
'"ReadStateStore"'
|
||||
].map(find => ({
|
||||
find,
|
||||
{
|
||||
find: '"MessageStore"',
|
||||
predicate: () => settings.store.ignoreBlockedMessages && settings.store.blockedWords !== "",
|
||||
replacement: [
|
||||
{
|
||||
match: /(?<=function (\i)\((\i)\){)(?=.*MESSAGE_CREATE:\1)/,
|
||||
replace: (_, _funcName, props) => `if($self.containsBlockedKeywords(${props}.message))return;`
|
||||
match: /(?<=MESSAGE_CREATE:function\((\i)\){)/,
|
||||
replace: (_, props) => `if($self.containsBlockedKeywords(${props}.message))return;`
|
||||
}
|
||||
]
|
||||
})),
|
||||
},
|
||||
{
|
||||
find: '"ReadStateStore"',
|
||||
predicate: () => settings.store.ignoreBlockedMessages && settings.store.blockedWords !== "",
|
||||
replacement: [
|
||||
{
|
||||
match: /(?<=MESSAGE_CREATE:function\((\i)\){)/,
|
||||
replace: (_, props) => `if($self.containsBlockedKeywords(${props}.message))return;`
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
|
||||
settings,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import definePlugin from "@utils/types";
|
|||
export default definePlugin({
|
||||
name: "BlockKrisp",
|
||||
description: "Prevent Krisp from loading",
|
||||
tags: ["Privacy", "Utility", "Voice"],
|
||||
authors: [Devs.D3SOX],
|
||||
patches: [
|
||||
// Block loading modules on Desktop
|
||||
|
|
@ -24,8 +25,8 @@ export default definePlugin({
|
|||
{
|
||||
find: "krisp_browser_models",
|
||||
replacement: {
|
||||
match: /(\i,\{\i:\(\)=>)\i/,
|
||||
replace: "$1null"
|
||||
match: /if\(this._noiseCancellation\)/,
|
||||
replace: "if(false)"
|
||||
}
|
||||
},
|
||||
// Set Krisp to not supported
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import definePlugin from "@utils/types";
|
|||
export default definePlugin({
|
||||
name: "BypassPinPrompt",
|
||||
description: "Bypass the pin prompt when using the pin functions",
|
||||
tags: ["Shortcuts"],
|
||||
authors: [Devs.thororen],
|
||||
patches: [
|
||||
...[
|
||||
|
|
|
|||
|
|
@ -157,6 +157,7 @@ const settings = definePluginSettings({
|
|||
export default definePlugin({
|
||||
name: "BypassStatus",
|
||||
description: "Still get notifications from specific sources when in do not disturb mode. Right-click on users/channels/guilds to set them to bypass do not disturb mode.",
|
||||
tags: ["Activity", "Customisation", "Notifications", "Servers"],
|
||||
authors: [Devs.Inbestigator],
|
||||
dependencies: ["AudioPlayerAPI"],
|
||||
flux: {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ function renderBadge(id: number, title: string) {
|
|||
export default definePlugin({
|
||||
name: "ChannelBadges",
|
||||
description: "Adds badges to channels based on their type",
|
||||
tags: ["Appearance", "Customisation", "Servers"],
|
||||
authors: [EquicordDevs.creations, Devs.thororen],
|
||||
settings,
|
||||
patches: [
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
import { BaseText } from "@components/BaseText";
|
||||
import { ChannelTabsProps, closeTab, ensureUnreadFallbackCountsLoaded, getNotificationDotState, getUnreadFallbackCounts, isTabSelected, moveDraggedTabs, moveToTab, openedTabs, settings, updateUnreadFallbackCounts } from "@equicordplugins/channelTabs/util";
|
||||
import { ActivityIcon, CircleQuestionIcon, DiscoveryIcon, EnvelopeIcon, FriendsIcon, ICYMIIcon, NitroIcon, QuestIcon, ShopIcon } from "@equicordplugins/channelTabs/util/icons";
|
||||
import { activeQuestIntervals } from "@equicordplugins/questify"; // sorry murphy!
|
||||
import { getActiveAutoCompletes } from "@equicordplugins/questify/utils/completion";
|
||||
import { classNameFactory } from "@utils/css";
|
||||
import { getGuildAcronym, getIntlMessage, getUniqueUsername } from "@utils/discord";
|
||||
import { classes } from "@utils/misc";
|
||||
|
|
@ -467,20 +467,19 @@ export default function ChannelTab(props: ChannelTabsProps & { index: number; })
|
|||
}), []);
|
||||
drag(drop(ref));
|
||||
|
||||
// check if quests running (questify momentLet)
|
||||
const hasActiveQuests = activeQuestIntervals.size > 0;
|
||||
const hasActiveQuests = getActiveAutoCompletes().length > 0;
|
||||
return <div
|
||||
className={cl("tab", {
|
||||
"tab-compact": compact,
|
||||
"tab-selected": isTabSelected(id),
|
||||
"tab-entering": isEntering,
|
||||
"tab-closing": isClosing,
|
||||
"tab-dragging": isDragging,
|
||||
"tab-drop-target": isDropTarget,
|
||||
"tab-nitro": channelId === "__nitro__",
|
||||
"tab-quests-active": channelId === "__quests__" && hasActiveQuests,
|
||||
wider: settings.store.widerTabsAndBookmarks
|
||||
})}
|
||||
"tab-compact": compact,
|
||||
"tab-selected": isTabSelected(id),
|
||||
"tab-entering": isEntering,
|
||||
"tab-closing": isClosing,
|
||||
"tab-dragging": isDragging,
|
||||
"tab-drop-target": isDropTarget,
|
||||
"tab-nitro": channelId === "__nitro__",
|
||||
"tab-quests-active": channelId === "__quests__" && hasActiveQuests,
|
||||
wider: settings.store.widerTabsAndBookmarks
|
||||
})}
|
||||
key={index}
|
||||
ref={ref}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ export default function ChannelsTabsContainer(props: BasicChannelTabsProps) {
|
|||
showBookmarkBar,
|
||||
widerTabsAndBookmarks,
|
||||
tabWidthScale,
|
||||
tabHeightScale,
|
||||
enableNumberKeySwitching,
|
||||
numberKeySwitchCount,
|
||||
enableCloseTabShortcut,
|
||||
|
|
@ -65,6 +66,7 @@ export default function ChannelsTabsContainer(props: BasicChannelTabsProps) {
|
|||
"showBookmarkBar",
|
||||
"widerTabsAndBookmarks",
|
||||
"tabWidthScale",
|
||||
"tabHeightScale",
|
||||
"enableNumberKeySwitching",
|
||||
"numberKeySwitchCount",
|
||||
"enableCloseTabShortcut",
|
||||
|
|
@ -288,6 +290,16 @@ export default function ChannelsTabsContainer(props: BasicChannelTabsProps) {
|
|||
|
||||
if (isFullscreen) return null;
|
||||
|
||||
const shouldFollowNewTabButton = newTabButtonBehavior && !tabsOverflow;
|
||||
const newTabButton = (
|
||||
<button
|
||||
onClick={() => createTab(props, true)}
|
||||
className={cl("button", "new-button", "hoverable")}
|
||||
>
|
||||
<PlusSmallIcon />
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classes(
|
||||
|
|
@ -312,7 +324,7 @@ export default function ChannelsTabsContainer(props: BasicChannelTabsProps) {
|
|||
!compactAutoExpandOnHover && cl("no-compact-hover-expand")
|
||||
)}
|
||||
ref={ref}
|
||||
style={{ "--tab-width-scale": tabWidthScale / 100 } as React.CSSProperties}
|
||||
style={{ "--tab-width-scale": tabWidthScale / 100, "--tab-height-scale": tabHeightScale / 100 } as React.CSSProperties}
|
||||
onContextMenu={e => ContextMenuApi.openContextMenu(e, () => <BasicContextMenu />)}
|
||||
>
|
||||
{showBookmarkBar && <>
|
||||
|
|
@ -322,20 +334,16 @@ export default function ChannelsTabsContainer(props: BasicChannelTabsProps) {
|
|||
<div className={cl("tab-container")}>
|
||||
<HorizontalScroller
|
||||
customRef={node => { scrollerRef.current = node; }}
|
||||
className={cl("tab-scroller", newTabButtonBehavior && !tabsOverflow && "tab-scroller-following")}
|
||||
className={cl("tab-scroller", shouldFollowNewTabButton && "tab-scroller-following")}
|
||||
>
|
||||
{openedTabs.filter(tab => tab != null).map((tab, i) =>
|
||||
<ChannelTab {...tab} index={i} key={tab.id} />
|
||||
)}
|
||||
{GhostTabs}
|
||||
{shouldFollowNewTabButton && newTabButton}
|
||||
</HorizontalScroller>
|
||||
|
||||
<button
|
||||
onClick={() => createTab(props, true)}
|
||||
className={cl("button", "new-button", "hoverable")}
|
||||
>
|
||||
<PlusSmallIcon />
|
||||
</button>
|
||||
{!shouldFollowNewTabButton && newTabButton}
|
||||
</div >
|
||||
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,8 +15,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;
|
||||
|
|
@ -47,7 +47,8 @@ const contextMenuPatch: NavContextMenuPatchCallback = (children, props: { channe
|
|||
export default definePlugin({
|
||||
name: "ChannelTabs",
|
||||
description: "Group your commonly visited channels in tabs, like a browser",
|
||||
authors: [Devs.TheSun, Devs.TheKodeToad, EquicordDevs.keifufu, Devs.Nickyux, EquicordDevs.DiabeloDEV, EquicordDevs.justjxke],
|
||||
tags: ["Appearance", "Customisation", "Organisation", "Servers"],
|
||||
authors: [Devs.TheSun, Devs.TheKodeToad, EquicordDevs.keifufu, Devs.Nickyux, EquicordDevs.DiabeloDEV, EquicordDevs.justjxke, EquicordDevs.keircn],
|
||||
dependencies: ["ContextMenuAPI"],
|
||||
contextMenus: {
|
||||
"channel-mention-context": contextMenuPatch,
|
||||
|
|
|
|||
|
|
@ -212,7 +212,8 @@
|
|||
border-color 0.15s ease,
|
||||
box-shadow 0.15s ease,
|
||||
opacity 0.15s ease;
|
||||
padding: 6px 8px;
|
||||
min-height: calc(38px * var(--tab-height-scale, 1));
|
||||
padding: calc(6px * var(--tab-height-scale, 1)) 8px;
|
||||
position: relative;
|
||||
cursor: grab;
|
||||
flex-shrink: 0;
|
||||
|
|
@ -1338,4 +1339,4 @@
|
|||
/* disable transitions when compact expand animation is disabled */
|
||||
.vc-channeltabs-no-compact-animation .vc-channeltabs-tab-number-badge {
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,11 +39,9 @@ function AnimationSettings(): JSX.Element {
|
|||
{ label: "Active Quests Gradient", value: "quests-active", selected: settings.store.animationQuestsActive }
|
||||
];
|
||||
|
||||
const [currentValue, setCurrentValue] = useState(animationOptions.filter(option => option.selected));
|
||||
|
||||
function updateSettingsTruthy(enabled: DynamicDropdownSettingOption[]) {
|
||||
const enabledValues = enabled.map(option => option.value);
|
||||
const [currentValue, setCurrentValue] = useState(animationOptions.filter(option => option.selected).map(option => option.value));
|
||||
|
||||
function updateSettingsTruthy(enabledValues: string[]) {
|
||||
animationOptions.forEach(option => {
|
||||
option.selected = enabledValues.includes(option.value);
|
||||
});
|
||||
|
|
@ -64,23 +62,24 @@ function AnimationSettings(): JSX.Element {
|
|||
settings.store.animationResizeHandle = enabledValues.includes("resize-handle");
|
||||
settings.store.animationQuestsActive = enabledValues.includes("quests-active");
|
||||
|
||||
setCurrentValue(enabled);
|
||||
setCurrentValue(enabledValues);
|
||||
}
|
||||
|
||||
function handleChange(values: Array<DynamicDropdownSettingOption | string>) {
|
||||
if (values.length === 0) {
|
||||
updateSettingsTruthy([]);
|
||||
const valueStrings = values.map(v => typeof v === "string" ? v : v.value);
|
||||
const toggled = valueStrings.length > currentValue.length
|
||||
? valueStrings.find(v => !currentValue.includes(v))
|
||||
: currentValue.find(v => !valueStrings.includes(v));
|
||||
|
||||
if (toggled == null) {
|
||||
updateSettingsTruthy(valueStrings);
|
||||
return;
|
||||
}
|
||||
|
||||
const stringlessValues = values.filter(v => typeof v !== "string") as DynamicDropdownSettingOption[];
|
||||
const selectedOption = values.find(v => typeof v === "string") as string;
|
||||
const option = animationOptions.find(option => option.value === selectedOption) as DynamicDropdownSettingOption;
|
||||
|
||||
if (option.selected) {
|
||||
updateSettingsTruthy(stringlessValues.filter(v => v.value !== selectedOption));
|
||||
if (currentValue.includes(toggled)) {
|
||||
updateSettingsTruthy(currentValue.filter(v => v !== toggled));
|
||||
} else {
|
||||
updateSettingsTruthy([...stringlessValues, option]);
|
||||
updateSettingsTruthy([...currentValue, toggled]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -179,6 +178,12 @@ export const settings = definePluginSettings({
|
|||
hidden: true,
|
||||
restartNeeded: false
|
||||
},
|
||||
tabHeightScale: {
|
||||
type: OptionType.NUMBER,
|
||||
description: "Tab height scale (percentage).",
|
||||
default: 100,
|
||||
restartNeeded: false
|
||||
},
|
||||
renderAllTabs: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Keep all tabs cached in memory for faster switching (caches scroll position and state)",
|
||||
|
|
|
|||
|
|
@ -4,10 +4,8 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { iconsModule } from "@equicordplugins/_core/concatenatedModules";
|
||||
import { Icon } from "@vencord/discord-types";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
|
||||
const iconsModule = findByPropsLazy("AngleBracketsIcon", "StaffBadgeIcon") as Record<string, Icon>;
|
||||
|
||||
let iconNames: string[] | undefined;
|
||||
|
||||
|
|
|
|||
|
|
@ -7,38 +7,75 @@
|
|||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import { Channel } from "@vencord/discord-types";
|
||||
import { ChannelStore } from "@webpack/common";
|
||||
|
||||
const SMALL_CAPS: Record<string, string> = {
|
||||
"ᴀ": "a", "ʙ": "b", "ᴄ": "c", "ᴅ": "d", "ᴇ": "e", "ꜰ": "f", "ɢ": "g", "ʜ": "h", "ɪ": "i", "ᴊ": "j",
|
||||
"ᴋ": "k", "ʟ": "l", "ᴍ": "m", "ɴ": "n", "ᴏ": "o", "ᴘ": "p", "ǫ": "q", "ʀ": "r", "ꜱ": "s", "ᴛ": "t",
|
||||
"ᴜ": "u", "ᴠ": "v", "ᴡ": "w", "x": "x", "ʏ": "y", "ᴢ": "z",
|
||||
};
|
||||
|
||||
const ORIGINAL_NAME = Symbol("cleanChannelName.original");
|
||||
|
||||
let editingChannelId: string | null = null;
|
||||
|
||||
function computeClean(name: string, type: number): string {
|
||||
const cleaned = name
|
||||
.normalize("NFKC")
|
||||
.replace(/[ᴀʙᴄᴅᴇꜰɢʜɪᴊᴋʟᴍɴᴏᴘǫʀꜱᴛᴜᴠᴡxʏᴢ]/g, m => SMALL_CAPS[m])
|
||||
.replace(/[^ -~]?\p{Extended_Pictographic}[^ -~]?/ug, "")
|
||||
.replace(/-?[^\p{Letter} -~]-?/ug, [2, 4].includes(type) ? " " : "-")
|
||||
.replace(/(^-|-$)/g, "")
|
||||
.replace(/-+/g, "-");
|
||||
return cleaned || name;
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "CleanChannelName",
|
||||
authors: [Devs.AutumnVN],
|
||||
description: "remove all emoji and decor shit from channel names",
|
||||
description: "Remove emoji and decoration from channel names. Reverts to the original while you're editing the channel.",
|
||||
tags: ["Appearance", "Customisation", "Chat", "Emotes", "Servers"],
|
||||
patches: [
|
||||
{
|
||||
find: "loadAllGuildAndPrivateChannelsFromDisk(){",
|
||||
replacement: {
|
||||
match: /(?<=getChannel\(\i\)\{if\(null!=\i\)return )\i\(\i\)/,
|
||||
replace: "$self.cleanChannelName($&)",
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
flux: {
|
||||
CHANNEL_SETTINGS_INIT({ channelId }: { channelId: string; }) {
|
||||
editingChannelId = channelId;
|
||||
(ChannelStore as any).emitChange?.();
|
||||
},
|
||||
CHANNEL_SETTINGS_CLOSE() {
|
||||
editingChannelId = null;
|
||||
(ChannelStore as any).emitChange?.();
|
||||
},
|
||||
},
|
||||
|
||||
cleanChannelName(channel?: Channel) {
|
||||
if (channel) {
|
||||
channel.name = channel.name
|
||||
.normalize("NFKC")
|
||||
.replace(/[ᴀʙᴄᴅᴇꜰɢʜɪᴊᴋʟᴍɴᴏᴘǫʀꜱᴛᴜᴠᴡxʏᴢ]/g, match => {
|
||||
const smallCapsToNormal = {
|
||||
"ᴀ": "a", "ʙ": "b", "ᴄ": "c", "ᴅ": "d", "ᴇ": "e", "ꜰ": "f", "ɢ": "g", "ʜ": "h", "ɪ": "i", "ᴊ": "j",
|
||||
"ᴋ": "k", "ʟ": "l", "ᴍ": "m", "ɴ": "n", "ᴏ": "o", "ᴘ": "p", "ǫ": "q", "ʀ": "r", "ꜱ": "s", "ᴛ": "t",
|
||||
"ᴜ": "u", "ᴠ": "v", "ᴡ": "w", "x": "x", "ʏ": "y", "ᴢ": "z"
|
||||
};
|
||||
return smallCapsToNormal[match];
|
||||
})
|
||||
.replace(/[^\u0020-\u007E]?\p{Extended_Pictographic}[^\u0020-\u007E]?/ug, "")
|
||||
.replace(/-?[^\p{Letter}\u0020-\u007E]-?/ug, [2, 4].includes(channel.type) ? " " : "-")
|
||||
.replace(/(^-|-$)/g, "")
|
||||
.replace(/-+/g, "-");
|
||||
}
|
||||
if (channel == null) return channel;
|
||||
const c = channel as any;
|
||||
|
||||
if (c[ORIGINAL_NAME] !== undefined) return channel;
|
||||
|
||||
c[ORIGINAL_NAME] = channel.name;
|
||||
|
||||
Object.defineProperty(channel, "name", {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get() {
|
||||
if (editingChannelId === channel.id) return c[ORIGINAL_NAME];
|
||||
return computeClean(c[ORIGINAL_NAME], channel.type);
|
||||
},
|
||||
set(value: string) {
|
||||
c[ORIGINAL_NAME] = value;
|
||||
},
|
||||
});
|
||||
|
||||
return channel;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import definePlugin from "@utils/types";
|
|||
export default definePlugin({
|
||||
name: "CleanerChannelGroups",
|
||||
description: "Hides all channels in collapsed categories, even if they have unread messages.",
|
||||
tags: ["Appearance", "Customisation", "Chat", "Organisation", "Servers"],
|
||||
authors: [EquicordDevs.justjxke],
|
||||
patches: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -146,13 +146,14 @@ const WrappedClickableRole = ErrorBoundary.wrap(ClickableRole, { noop: true });
|
|||
export default definePlugin({
|
||||
name: "ClickableRoles",
|
||||
description: "Click on roles in user profiles and the member list to see which members have them.",
|
||||
tags: ["Appearance", "Roles"],
|
||||
authors: [Devs.prism],
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: "#{intl::T+3Adg::raw}",
|
||||
find: "#{intl::zr0Y5R::raw}",
|
||||
replacement: {
|
||||
match: /(colors:\i\}\);)return(.*?enableTooltip:!1\}\):null,\i\]\}\))/,
|
||||
match: /(\.colorString\?\?\i;)return(.*?enableTooltip:!1\}\):null,\i\]\}\))/,
|
||||
replace: "$1return $self.wrapRolePill(arguments[0],()=>$2)",
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -142,7 +142,8 @@ function activeNowView(cards) {
|
|||
export default definePlugin({
|
||||
name: "ClientSideBlock",
|
||||
description: "Allows you to locally hide almost all content from any user",
|
||||
tags: ["blocked", "block", "hide", "hidden", "noblockedmessages"],
|
||||
tags: ["Utility"],
|
||||
searchTerms: ["blocked", "block", "hide", "hidden", "noblockedmessages"],
|
||||
authors: [Devs.Samwich, EquicordDevs.KamiRu],
|
||||
settings,
|
||||
activeNowView,
|
||||
|
|
@ -224,8 +225,8 @@ export default definePlugin({
|
|||
{
|
||||
find: "ACTIVE_NOW_COLUMN)",
|
||||
replacement: {
|
||||
match: /(\i\.\i),\{(?=\}\)\])/,
|
||||
replace: '"div",{children:$self.activeNowView($1())'
|
||||
match: /(__invalid_consentCard.{0,40}\()(\i),\{/,
|
||||
replace: '$1"div",{children:$self.activeNowView($2())'
|
||||
}
|
||||
},
|
||||
// mutual friends list in user profile
|
||||
|
|
|
|||
|
|
@ -37,6 +37,12 @@ const settings = definePluginSettings({
|
|||
default: true,
|
||||
restartNeeded: true
|
||||
},
|
||||
enableSpeakingIndicators: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Enable speaking indicators",
|
||||
default: true,
|
||||
restartNeeded: true
|
||||
},
|
||||
enableAdvancedSignals: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Enable advanced clip signals (auto-clip triggers)",
|
||||
|
|
@ -72,6 +78,7 @@ migratePluginSettings("ClipsEnhancements", "TimelessClips");
|
|||
export default definePlugin({
|
||||
name: "ClipsEnhancements",
|
||||
description: "Add more Clip FPS and duration options, custom clip length, RPC tagging and more",
|
||||
tags: ["Activity", "Media", "Utility"],
|
||||
authors: [Devs.niko, Devs.Joona, EquicordDevs.keircn],
|
||||
settings,
|
||||
patches: [
|
||||
|
|
@ -100,8 +107,8 @@ export default definePlugin({
|
|||
{
|
||||
find: "2026-03-clips-experiment",
|
||||
replacement: {
|
||||
match: /defaultConfig:\{enableClips:!\d,ignorePlatformRestriction:!\d,showClipsHeaderEntrypoint:!\d,enableScreenshotKeybind:!\d,enableVoiceOnlyClips:!\d,enableAdvancedSignals:!\d\}/,
|
||||
replace: "defaultConfig:{enableClips:!0,ignorePlatformRestriction:$self.settings.store.ignorePlatformRestriction,showClipsHeaderEntrypoint:!0,enableScreenshotKeybind:$self.settings.store.enableScreenshotKeybind,enableVoiceOnlyClips:$self.settings.store.enableVoiceOnlyClips,enableAdvancedSignals:$self.settings.store.enableAdvancedSignals}"
|
||||
match: /defaultConfig:\{enableClips:!\d,ignorePlatformRestriction:!\d,enableScreenshotKeybind:!\d,enableVoiceOnlyClips:!\d,enableSpeakingIndicators:!\d,enableAdvancedSignals:!\d\}/,
|
||||
replace: "defaultConfig:{enableClips:!0,ignorePlatformRestriction:$self.settings.store.ignorePlatformRestriction,enableScreenshotKeybind:$self.settings.store.enableScreenshotKeybind,enableVoiceOnlyClips:$self.settings.store.enableVoiceOnlyClips,enableSpeakingIndicators:$self.settings.store.enableSpeakingIndicators,enableAdvancedSignals:$self.settings.store.enableAdvancedSignals}"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
|
|||
142
src/equicordplugins/collapsibleUi/index.tsx
Normal file
142
src/equicordplugins/collapsibleUi/index.tsx
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
* 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(`collapsed-size-${store.collapsedSize}`), 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", "ChatInputButtonAPI"],
|
||||
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),"
|
||||
}
|
||||
},
|
||||
],
|
||||
chatBarButtonWrapper: (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"));
|
||||
},
|
||||
});
|
||||
110
src/equicordplugins/collapsibleUi/settings.ts
Normal file
110
src/equicordplugins/collapsibleUi/settings.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* 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 collapsedSizeMarkers = [8, 12, 16, 20, 24, 28, 32, 40, 48];
|
||||
|
||||
export const panelRegistry = {
|
||||
guildBar: {
|
||||
id: "guildBar",
|
||||
classId: "guild-bar",
|
||||
label: "Guild Bar",
|
||||
collapsedKey: "guildBarCollapsed",
|
||||
},
|
||||
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",
|
||||
},
|
||||
titleBar: {
|
||||
id: "titleBar",
|
||||
classId: "title-bar",
|
||||
label: "Title Bar",
|
||||
collapsedKey: "titleBarCollapsed",
|
||||
},
|
||||
headerBar: {
|
||||
id: "headerBar",
|
||||
classId: "header-bar",
|
||||
label: "Header Bar",
|
||||
collapsedKey: "headerBarCollapsed",
|
||||
},
|
||||
userArea: {
|
||||
id: "userArea",
|
||||
classId: "user-area",
|
||||
label: "User Area",
|
||||
collapsedKey: "userAreaCollapsed",
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type PanelId = keyof typeof panelRegistry;
|
||||
|
||||
export const toolbarPanelOrder = ["guildBar", "channelList", "membersList", "chatButtons", "titleBar", "headerBar", "userArea"] as const satisfies readonly PanelId[];
|
||||
|
||||
export const settings = definePluginSettings({
|
||||
collapsedSize: {
|
||||
type: OptionType.SLIDER,
|
||||
description: "Collapsed hover area size in pixels.",
|
||||
default: 12,
|
||||
markers: collapsedSizeMarkers,
|
||||
},
|
||||
transitionMs: {
|
||||
type: OptionType.SLIDER,
|
||||
description: "Panel transition speed in milliseconds.",
|
||||
default: 160,
|
||||
markers: transitionMarkers,
|
||||
},
|
||||
guildBarCollapsed: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Persist the guild bar as collapsed.",
|
||||
default: false,
|
||||
},
|
||||
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,
|
||||
},
|
||||
titleBarCollapsed: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Persist the title bar as collapsed.",
|
||||
default: false,
|
||||
},
|
||||
headerBarCollapsed: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Persist the header bar as collapsed.",
|
||||
default: false,
|
||||
},
|
||||
userAreaCollapsed: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Persist the user area as collapsed.",
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
416
src/equicordplugins/collapsibleUi/style.css
Normal file
416
src/equicordplugins/collapsibleUi/style.css
Normal file
|
|
@ -0,0 +1,416 @@
|
|||
@property --vc-cui-collapsed-size {
|
||||
syntax: "<length>";
|
||||
inherits: true;
|
||||
initial-value: 12px;
|
||||
}
|
||||
|
||||
@property --vc-cui-transition {
|
||||
syntax: "<time>";
|
||||
inherits: true;
|
||||
initial-value: 160ms;
|
||||
}
|
||||
|
||||
:root {
|
||||
--vc-cui-chat-buttons-width: min(320px, 36vw);
|
||||
--vc-cui-hover-peek: var(--vc-cui-collapsed-size);
|
||||
--vc-cui-hover-delay: 120ms;
|
||||
--vc-cui-header-bar-height: 32px;
|
||||
--vc-cui-user-area-collapsed-width: 52px;
|
||||
--vc-cui-ease: cubic-bezier(.2, 0, 0, 1);
|
||||
--vc-cui-fade-transition: opacity var(--vc-cui-transition) var(--vc-cui-ease);
|
||||
--vc-cui-inline-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);
|
||||
--vc-cui-block-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)
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-collapsed-size-8) {
|
||||
--vc-cui-collapsed-size: 8px
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-collapsed-size-12) {
|
||||
--vc-cui-collapsed-size: 12px
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-collapsed-size-16) {
|
||||
--vc-cui-collapsed-size: 16px
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-collapsed-size-20) {
|
||||
--vc-cui-collapsed-size: 20px
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-collapsed-size-24) {
|
||||
--vc-cui-collapsed-size: 24px
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-collapsed-size-28) {
|
||||
--vc-cui-collapsed-size: 28px
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-collapsed-size-32) {
|
||||
--vc-cui-collapsed-size: 32px
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-collapsed-size-40) {
|
||||
--vc-cui-collapsed-size: 40px
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-collapsed-size-48) {
|
||||
--vc-cui-collapsed-size: 48px
|
||||
}
|
||||
|
||||
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-guild-bar-collapsed) nav[class*="guilds_"]:not(:is(.vc-betterFolders-sidebar nav[class*="guilds_"])) {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
overflow: hidden !important;
|
||||
transition: var(--vc-cui-inline-transition) !important
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-guild-bar-collapsed) nav[class*="guilds_"]:not(:is(.vc-betterFolders-sidebar nav[class*="guilds_"])):is(:hover, :focus-within) {
|
||||
z-index: 3
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-guild-bar-collapsed) nav[class*="guilds_"]:not(:is(.vc-betterFolders-sidebar nav[class*="guilds_"]), :is(:hover, :focus-within), :is(.vc-betterFolders-sidebar-grid:has(.vc-betterFolders-sidebar:is(:hover, :focus-within)) nav[class*="guilds_"])) {
|
||||
background: var(--background-gradient-low, var(--background-base-lowest)) !important;
|
||||
border-start-end-radius: var(--radius-sm, 8px);
|
||||
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
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-guild-bar-collapsed) .vc-betterFolders-sidebar {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
overflow: hidden !important;
|
||||
transition: var(--vc-cui-inline-transition) !important
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-guild-bar-collapsed) .vc-betterFolders-sidebar-grid:has(nav[class*="guilds_"]:is(:hover, :focus-within), .vc-betterFolders-sidebar:is(:hover, :focus-within)) .vc-betterFolders-sidebar {
|
||||
z-index: 3;
|
||||
transition-delay: var(--vc-cui-hover-delay)
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-guild-bar-collapsed) .vc-betterFolders-sidebar-grid:not(:has(nav[class*="guilds_"]:is(:hover, :focus-within), .vc-betterFolders-sidebar:is(:hover, :focus-within))) .vc-betterFolders-sidebar {
|
||||
width: 0 !important;
|
||||
inline-size: 0 !important;
|
||||
min-width: 0 !important;
|
||||
min-inline-size: 0 !important;
|
||||
max-width: 0 !important;
|
||||
max-inline-size: 0 !important;
|
||||
flex: 0 0 0 !important
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-channel-list-collapsed) [class*="sidebarList_"] {
|
||||
overflow: hidden !important;
|
||||
transition: var(--vc-cui-inline-transition) !important
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-channel-list-collapsed) [class*="sidebarList_"]:is(:hover, :focus-within) {
|
||||
transition-delay: var(--vc-cui-hover-delay)
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-channel-list-collapsed) [class*="sidebarList_"]:is(:hover, :focus-within) > * {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transition: var(--vc-cui-fade-transition);
|
||||
transition-delay: var(--vc-cui-hover-delay)
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-channel-list-collapsed):not(:has([class*="sidebar_"] > [class*="panels_"]:is(:hover, :focus-within))) [class*="sidebarList_"]:not(:is(:hover, :focus-within)) {
|
||||
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
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-channel-list-collapsed) [class*="sidebar_"] > [class*="panels_"] {
|
||||
overflow: hidden !important;
|
||||
transition: var(--vc-cui-inline-transition) !important
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-channel-list-collapsed) [class*="sidebar_"] > [class*="panels_"]:not(:is(:hover, :focus-within)) {
|
||||
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
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-channel-list-collapsed) [class*="sidebar_"] > [class*="panels_"]:not(:is(:hover, :focus-within)) .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) [class*="sidebar_"] > [class*="panels_"]:is(:hover, :focus-within) .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: var(--vc-cui-inline-transition) !important
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-members-list-collapsed) [class*="membersWrap"]:is(:hover, :focus-within) {
|
||||
transition-delay: var(--vc-cui-hover-delay)
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-members-list-collapsed) [class*="membersWrap"]:is(:hover, :focus-within) > * {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transition: var(--vc-cui-fade-transition);
|
||||
transition-delay: var(--vc-cui-hover-delay)
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-members-list-collapsed) [class*="membersWrap"]:not(:is(:hover, :focus-within)) {
|
||||
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) [class*="membersWrap"]:not(:is(:hover, :focus-within)) > * {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: var(--vc-cui-fade-transition)
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-user-area-collapsed) [class*="sidebar_"] > [class*="panels_"]:is(:hover, :focus-within) > * {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transition: var(--vc-cui-fade-transition)
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-user-area-collapsed) [class*="sidebar_"] > [class*="panels_"]:not(:is(:hover, :focus-within)) {
|
||||
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: var(--vc-cui-block-transition) !important
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-user-area-collapsed) [class*="sidebar_"] > [class*="panels_"]:not(:is(:hover, :focus-within)) > * {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: var(--vc-cui-fade-transition)
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-guild-bar-collapsed):has(.vc-collapsible-ui-channel-list-collapsed) [class*="sidebar_"] > [class*="panels_"]:not(:is(:hover, :focus-within)) {
|
||||
width: calc(var(--vc-cui-hover-peek) + 4px) !important;
|
||||
inline-size: calc(var(--vc-cui-hover-peek) + 4px) !important;
|
||||
min-width: calc(var(--vc-cui-hover-peek) + 4px) !important;
|
||||
min-inline-size: calc(var(--vc-cui-hover-peek) + 4px) !important;
|
||||
max-width: calc(var(--vc-cui-hover-peek) + 4px) !important;
|
||||
max-inline-size: calc(var(--vc-cui-hover-peek) + 4px) !important
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-title-bar-collapsed) section[class*="title_"][class*="container__"] {
|
||||
overflow: hidden !important;
|
||||
transition: var(--vc-cui-block-transition) !important
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-title-bar-collapsed) section[class*="title_"][class*="container__"]:is(:hover, :focus-within) {
|
||||
transition-delay: var(--vc-cui-hover-delay)
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-title-bar-collapsed) section[class*="title_"][class*="container__"]:is(:hover, :focus-within) > * {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transition: var(--vc-cui-fade-transition);
|
||||
transition-delay: var(--vc-cui-hover-delay)
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-title-bar-collapsed) section[class*="title_"][class*="container__"]:not(:is(:hover, :focus-within)) {
|
||||
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;
|
||||
padding-block: 0 !important
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-title-bar-collapsed) section[class*="title_"][class*="container__"]:not(:is(:hover, :focus-within)) > * {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: var(--vc-cui-fade-transition)
|
||||
}
|
||||
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
body:has(.vc-collapsible-ui-header-bar-collapsed) [class*="bar_"]:has(> [class*="title_"]) {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
overflow: hidden !important;
|
||||
transition: var(--vc-cui-block-transition) !important
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-header-bar-collapsed) [class*="base_"]:has(> [class*="bar_"] > [class*="title_"]) {
|
||||
/* stylelint-disable-next-line value-keyword-case */
|
||||
grid-template-rows: [top] var(--vc-cui-hover-peek) [titleBarEnd] auto [noticeEnd] minmax(0, 1fr) [end] !important
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-header-bar-collapsed) [class*="base_"]:has(> [class*="bar_"]:is(:hover, :focus-within) > [class*="title_"]) {
|
||||
/* stylelint-disable-next-line value-keyword-case */
|
||||
grid-template-rows: [top] var(--vc-cui-header-bar-height) [titleBarEnd] auto [noticeEnd] minmax(0, 1fr) [end] !important
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-header-bar-collapsed) [class*="bar_"]:has(> [class*="title_"]):is(:hover, :focus-within) {
|
||||
height: var(--vc-cui-header-bar-height) !important;
|
||||
block-size: var(--vc-cui-header-bar-height) !important;
|
||||
min-height: var(--vc-cui-header-bar-height) !important;
|
||||
min-block-size: var(--vc-cui-header-bar-height) !important;
|
||||
max-height: var(--vc-cui-header-bar-height) !important;
|
||||
max-block-size: var(--vc-cui-header-bar-height) !important;
|
||||
transition-delay: var(--vc-cui-hover-delay)
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-header-bar-collapsed) [class*="bar_"]:has(> [class*="title_"]):is(:hover, :focus-within) > * {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transition: var(--vc-cui-fade-transition);
|
||||
transition-delay: var(--vc-cui-hover-delay)
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-header-bar-collapsed) [class*="bar_"]:has(> [class*="title_"]):not(:is(:hover, :focus-within)) {
|
||||
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;
|
||||
padding-block: 0 !important
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-header-bar-collapsed) [class*="bar_"]:has(> [class*="title_"]):not(:is(:hover, :focus-within)) > * {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: var(--vc-cui-fade-transition)
|
||||
}
|
||||
/* stylelint-enable no-descending-specificity */
|
||||
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
body:has(.vc-collapsible-ui-guild-bar-collapsed) .vc-betterFolders-sidebar-grid:has(nav[class*="guilds_"]:is(:hover, :focus-within), .vc-betterFolders-sidebar:is(:hover, :focus-within)) .vc-betterFolders-sidebar > *,
|
||||
body:has(.vc-collapsible-ui-guild-bar-collapsed) nav[class*="guilds_"]:not(:is(.vc-betterFolders-sidebar nav[class*="guilds_"])):is(:hover, :focus-within) > * {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transition: var(--vc-cui-fade-transition);
|
||||
transition-delay: calc(var(--vc-cui-hover-delay) + var(--vc-cui-transition))
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-guild-bar-collapsed) nav[class*="guilds_"]:not(:is(.vc-betterFolders-sidebar nav[class*="guilds_"])):is(:hover, :focus-within) > [class*="unreadMentionsIndicator"] {
|
||||
pointer-events: none
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-guild-bar-collapsed) .vc-betterFolders-sidebar-grid:not(:has(nav[class*="guilds_"]:is(:hover, :focus-within), .vc-betterFolders-sidebar:is(:hover, :focus-within))) .vc-betterFolders-sidebar > *,
|
||||
body:has(.vc-collapsible-ui-guild-bar-collapsed) nav[class*="guilds_"]:not(:is(.vc-betterFolders-sidebar nav[class*="guilds_"]), :is(:hover, :focus-within), :is(.vc-betterFolders-sidebar-grid:has(.vc-betterFolders-sidebar:is(:hover, :focus-within)) nav[class*="guilds_"])) > * {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: var(--vc-cui-fade-transition)
|
||||
}
|
||||
|
||||
body:has(.vc-collapsible-ui-channel-list-collapsed):not(:has([class*="sidebar_"] > [class*="panels_"]:is(:hover, :focus-within))) [class*="sidebarList_"]:not(:is(:hover, :focus-within)) > * {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: var(--vc-cui-fade-transition)
|
||||
}
|
||||
/* stylelint-enable no-descending-specificity */
|
||||
|
||||
.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:is(:hover, :focus-within) .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
|
||||
}
|
||||
}
|
||||
|
|
@ -7,8 +7,6 @@
|
|||
import { isPluginEnabled, plugins } from "@api/PluginManager";
|
||||
import { NotesIcon } from "@components/Icons";
|
||||
import { toggleEnabled } from "@equicordplugins/equicordHelper/utils";
|
||||
import { noteHandler } from "@equicordplugins/holyNotes/NoteHandler";
|
||||
import type { Note } from "@equicordplugins/holyNotes/types";
|
||||
import type { ScheduledMessage } from "@equicordplugins/scheduledMessages/types";
|
||||
import { addScheduledMessage, getChannelDisplayInfo, getScheduledMessages, removeScheduledMessage, sendScheduledMessageNow, updateScheduledMessageTime } from "@equicordplugins/scheduledMessages/utils";
|
||||
import { sleep } from "@utils/misc";
|
||||
|
|
@ -21,17 +19,11 @@ import { DISCORD_INTERNAL_HOSTS, executeCommand, getCommandById } from "../regis
|
|||
import { makeIconFromUrl } from "../ui/iconFromUrl";
|
||||
import { sendMessageToChannel, sendMessageToUser } from "./sendMessageAction";
|
||||
|
||||
interface ResolvedHolyNote {
|
||||
notebook: string;
|
||||
note: Note;
|
||||
}
|
||||
|
||||
interface StagedRescheduledMessage {
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
let stagedMoveNote: ResolvedHolyNote | null = null;
|
||||
let stagedRescheduleMessage: StagedRescheduledMessage | null = null;
|
||||
let pendingCancelScheduledMessageId: string | null = null;
|
||||
|
||||
|
|
@ -559,198 +551,6 @@ function filterScheduledMessages(target: string): ScheduledMessage[] {
|
|||
});
|
||||
}
|
||||
|
||||
function getNotebookNames(): string[] {
|
||||
return Object.keys(noteHandler.getAllNotes());
|
||||
}
|
||||
|
||||
function filterNotebookNames(target: string, includeMain: boolean): string[] {
|
||||
const normalizedTarget = normalizeText(target);
|
||||
return getNotebookNames()
|
||||
.filter(notebook => includeMain || notebook !== "Main")
|
||||
.filter(notebook => !normalizedTarget || notebook.toLowerCase().includes(normalizedTarget))
|
||||
.sort((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
function getResolvedNotes(target: string): ResolvedHolyNote[] {
|
||||
const normalizedTarget = normalizeText(target);
|
||||
const notebooks = noteHandler.getAllNotes();
|
||||
const resolved: ResolvedHolyNote[] = [];
|
||||
|
||||
for (const [notebook, notes] of Object.entries(notebooks)) {
|
||||
for (const note of Object.values(notes)) {
|
||||
const content = note.content?.trim() ?? "";
|
||||
if (normalizedTarget && !content.toLowerCase().includes(normalizedTarget)) continue;
|
||||
resolved.push({ notebook, note });
|
||||
}
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function toNoteSnippet(note: Note): string {
|
||||
const content = note.content?.replace(/\s+/g, " ").trim();
|
||||
if (!content) return "(No text)";
|
||||
if (content.length <= 56) return content;
|
||||
return `${content.slice(0, 53)}...`;
|
||||
}
|
||||
|
||||
function buildCreateNotebookCandidates(target: string): QueryActionCandidate[] {
|
||||
const notebookName = target.trim();
|
||||
if (!notebookName) {
|
||||
return [{
|
||||
id: "query-create-notebook-invalid",
|
||||
label: "Create notebook",
|
||||
description: "Notebook name is required.",
|
||||
badge: "Query",
|
||||
icon: NotesIcon,
|
||||
run: () => {
|
||||
showToast("Notebook name is required.", Toasts.Type.FAILURE);
|
||||
return false;
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
return [{
|
||||
id: `query-create-notebook-${notebookName.toLowerCase()}`,
|
||||
label: `Create notebook ${notebookName}`,
|
||||
badge: "Query",
|
||||
icon: NotesIcon,
|
||||
run: async () => {
|
||||
if (!await ensureHolyNotesEnabled()) return false;
|
||||
if (noteHandler.getNotes(notebookName)) {
|
||||
showToast(`Notebook ${notebookName} already exists`, Toasts.Type.FAILURE);
|
||||
return false;
|
||||
}
|
||||
noteHandler.newNoteBook(notebookName);
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
function buildDeleteNotebookCandidates(target: string): QueryActionCandidate[] {
|
||||
const notebooks = filterNotebookNames(target, false);
|
||||
if (notebooks.length === 0) {
|
||||
return [{
|
||||
id: "query-delete-notebook-none",
|
||||
label: "Delete notebook",
|
||||
description: "No deletable notebook found.",
|
||||
badge: "Query",
|
||||
icon: NotesIcon,
|
||||
run: () => {
|
||||
showToast("No deletable notebook found.", Toasts.Type.MESSAGE);
|
||||
return false;
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
return notebooks.slice(0, 8).map(notebook => ({
|
||||
id: `query-delete-notebook-${notebook.toLowerCase()}`,
|
||||
label: notebook,
|
||||
description: "Notebook",
|
||||
badge: "Query",
|
||||
icon: NotesIcon,
|
||||
run: async () => {
|
||||
if (!await ensureHolyNotesEnabled()) return false;
|
||||
noteHandler.deleteNotebook(notebook);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
function buildJumpNoteCandidates(target: string): QueryActionCandidate[] {
|
||||
const notes = getResolvedNotes(target).slice(0, 10);
|
||||
if (notes.length === 0) {
|
||||
return [{
|
||||
id: "query-jump-note-none",
|
||||
label: "Jump to note",
|
||||
description: "No matching note found.",
|
||||
badge: "Query",
|
||||
icon: NotesIcon,
|
||||
run: () => {
|
||||
showToast("No matching note found.", Toasts.Type.FAILURE);
|
||||
return false;
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
return notes.map(({ notebook, note }) => ({
|
||||
id: `query-jump-note-${notebook}-${note.id}`,
|
||||
label: `[${notebook}] ${toNoteSnippet(note)}`,
|
||||
description: "Open original message.",
|
||||
badge: "Query",
|
||||
icon: NotesIcon,
|
||||
run: async () => {
|
||||
if (!await ensureHolyNotesEnabled()) return false;
|
||||
NavigationRouter.transitionTo(`/channels/${note.guild_id || "@me"}/${note.channel_id}/${note.id}`);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
function buildMoveNoteCandidates(target: string): QueryActionCandidate[] {
|
||||
if (stagedMoveNote) {
|
||||
const destinationNotebooks = filterNotebookNames(target, true)
|
||||
.filter(notebook => notebook !== stagedMoveNote?.notebook);
|
||||
|
||||
if (destinationNotebooks.length === 0) {
|
||||
return [{
|
||||
id: "query-move-note-no-destination",
|
||||
label: "Move note",
|
||||
description: "No destination notebook found.",
|
||||
badge: "Query",
|
||||
icon: NotesIcon,
|
||||
run: () => {
|
||||
showToast("No destination notebook found.", Toasts.Type.FAILURE);
|
||||
return false;
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
return destinationNotebooks.slice(0, 8).map(destinationNotebook => ({
|
||||
id: `query-move-note-destination-${destinationNotebook.toLowerCase()}`,
|
||||
label: `[${stagedMoveNote!.notebook}] ${toNoteSnippet(stagedMoveNote!.note)} -> Move to ${destinationNotebook}`,
|
||||
description: "Move note",
|
||||
badge: "Query",
|
||||
icon: NotesIcon,
|
||||
run: async () => {
|
||||
if (!stagedMoveNote) return;
|
||||
if (!await ensureHolyNotesEnabled()) return false;
|
||||
if (!stagedMoveNote) return;
|
||||
noteHandler.moveNote(stagedMoveNote.note, stagedMoveNote.notebook, destinationNotebook);
|
||||
stagedMoveNote = null;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
const notes = getResolvedNotes(target).slice(0, 10);
|
||||
if (notes.length === 0) {
|
||||
return [{
|
||||
id: "query-move-note-none",
|
||||
label: "Move note",
|
||||
description: "No matching note found.",
|
||||
badge: "Query",
|
||||
icon: NotesIcon,
|
||||
run: () => {
|
||||
showToast("No matching note found.", Toasts.Type.FAILURE);
|
||||
return false;
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
return notes.map(({ notebook, note }) => ({
|
||||
id: `query-move-note-select-${notebook}-${note.id}`,
|
||||
label: `[${notebook}] ${toNoteSnippet(note)}`,
|
||||
description: "Select destination notebook.",
|
||||
badge: "Query",
|
||||
icon: NotesIcon,
|
||||
run: () => {
|
||||
return ensureHolyNotesEnabled().then(enabled => {
|
||||
if (!enabled) return false;
|
||||
stagedMoveNote = { notebook, note };
|
||||
showToast("Note selected. Choose destination notebook.", Toasts.Type.MESSAGE);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
function buildQuickScheduleMessageCandidates(target: string): QueryActionCandidate[] {
|
||||
const parsed = parseQuickScheduleTarget(target);
|
||||
if (!parsed) return [];
|
||||
|
|
@ -926,15 +726,11 @@ function buildCancelScheduledMessageCandidates(target: string): QueryActionCandi
|
|||
export function buildQueryResolution(query: string): QueryResolution {
|
||||
const parsed = parseQuery(query);
|
||||
if (!parsed) {
|
||||
stagedMoveNote = null;
|
||||
stagedRescheduleMessage = null;
|
||||
pendingCancelScheduledMessageId = null;
|
||||
return { type: "none" };
|
||||
}
|
||||
|
||||
if (parsed.intent !== "move_note") {
|
||||
stagedMoveNote = null;
|
||||
}
|
||||
if (parsed.intent !== "reschedule_message") {
|
||||
stagedRescheduleMessage = null;
|
||||
}
|
||||
|
|
@ -971,14 +767,6 @@ export function buildQueryResolution(query: string): QueryResolution {
|
|||
return { type: "candidates", candidates: buildTogglePluginCandidates(parsed.target) };
|
||||
case "open_url":
|
||||
return { type: "candidates", candidates: buildOpenUrlCandidates(parsed.target) };
|
||||
case "create_notebook":
|
||||
return { type: "candidates", candidates: buildCreateNotebookCandidates(parsed.target) };
|
||||
case "delete_notebook":
|
||||
return { type: "candidates", candidates: buildDeleteNotebookCandidates(parsed.target) };
|
||||
case "move_note":
|
||||
return { type: "candidates", candidates: buildMoveNoteCandidates(parsed.target) };
|
||||
case "jump_note":
|
||||
return { type: "candidates", candidates: buildJumpNoteCandidates(parsed.target) };
|
||||
case "schedule_message":
|
||||
return { type: "candidates", candidates: buildQuickScheduleMessageCandidates(parsed.target) };
|
||||
case "reschedule_message":
|
||||
|
|
|
|||
|
|
@ -1,161 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2026 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { isPluginEnabled, plugins } from "@api/PluginManager";
|
||||
import { toggleEnabled } from "@equicordplugins/equicordHelper/utils";
|
||||
import { noteHandler } from "@equicordplugins/holyNotes/NoteHandler";
|
||||
import { Message } from "@vencord/discord-types";
|
||||
import { MessageStore, SelectedChannelStore, Toasts } from "@webpack/common";
|
||||
|
||||
import { DEFAULT_CATEGORY_ID } from "../../metadata/categories";
|
||||
import { TAG_NAVIGATION, TAG_PLUGINS, TAG_UTILITY } from "../../metadata/tags";
|
||||
import type { CommandEntry } from "../../registry";
|
||||
import type { HolyNotesPlugin } from "../types";
|
||||
|
||||
function showToast(message: string, type: (typeof Toasts.Type)[keyof typeof Toasts.Type]) {
|
||||
Toasts.show({ message, type, id: Toasts.genId(), options: { position: Toasts.Position.BOTTOM } });
|
||||
}
|
||||
|
||||
function getHolyNotesPlugin(): HolyNotesPlugin | null {
|
||||
const plugin = plugins.HolyNotes as HolyNotesPlugin | undefined;
|
||||
return plugin ?? null;
|
||||
}
|
||||
|
||||
async function ensureHolyNotesPluginEnabled() {
|
||||
const plugin = getHolyNotesPlugin();
|
||||
if (!plugin) {
|
||||
showToast("HolyNotes plugin is unavailable.", Toasts.Type.FAILURE);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isPluginEnabled(plugin.name)) return true;
|
||||
|
||||
const success = await toggleEnabled(plugin.name);
|
||||
if (!success || !isPluginEnabled(plugin.name)) {
|
||||
showToast("Failed to enable HolyNotes.", Toasts.Type.FAILURE);
|
||||
return false;
|
||||
}
|
||||
|
||||
showToast("Enabled HolyNotes.", Toasts.Type.SUCCESS);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function runHolyNotesOpen() {
|
||||
if (!await ensureHolyNotesPluginEnabled()) return;
|
||||
|
||||
const plugin = getHolyNotesPlugin();
|
||||
const openNotes = plugin?.toolboxActions?.["Open Notes"];
|
||||
if (typeof openNotes !== "function") {
|
||||
showToast("HolyNotes action is unavailable.", Toasts.Type.FAILURE);
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.resolve(openNotes());
|
||||
}
|
||||
|
||||
function getLastNonSystemMessageInChannel(channelId: string): Message | null {
|
||||
const messageCollection = MessageStore.getMessages?.(channelId) as { _array?: Message[]; } | undefined;
|
||||
const messages = messageCollection?._array;
|
||||
if (!messages?.length) return null;
|
||||
|
||||
for (let index = messages.length - 1; index >= 0; index--) {
|
||||
const message = messages[index] as Message & { deleted?: boolean; type?: number; };
|
||||
if (!message || message.deleted) continue;
|
||||
if (typeof message.type === "number" && message.type !== 0) continue;
|
||||
return message;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function runHolyNotesQuickSaveLastMessage() {
|
||||
if (!await ensureHolyNotesPluginEnabled()) return;
|
||||
|
||||
const channelId = SelectedChannelStore.getChannelId();
|
||||
if (!channelId) {
|
||||
showToast("No active channel available.", Toasts.Type.FAILURE);
|
||||
return;
|
||||
}
|
||||
|
||||
const message = getLastNonSystemMessageInChannel(channelId);
|
||||
if (!message) {
|
||||
showToast("No message available to save.", Toasts.Type.MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
noteHandler.addNote(message, "Main");
|
||||
}
|
||||
|
||||
export function createHolyNotesExtensionCommands(): CommandEntry[] {
|
||||
return [
|
||||
{
|
||||
id: "extension-holy-notes-open",
|
||||
label: "Open Notes",
|
||||
description: "Open the HolyNotes notebook modal.",
|
||||
keywords: ["holy", "notes", "notebook", "bookmark", "open", "plugin", "extension"],
|
||||
categoryId: DEFAULT_CATEGORY_ID,
|
||||
tags: [TAG_PLUGINS, TAG_UTILITY, TAG_NAVIGATION],
|
||||
handler: runHolyNotesOpen
|
||||
},
|
||||
{
|
||||
id: "extension-holy-notes-create-notebook-query",
|
||||
label: "Create Notebook",
|
||||
description: "Create a HolyNotes notebook.",
|
||||
keywords: ["holy", "notes", "notebook", "create", "new", "plugin", "extension"],
|
||||
categoryId: DEFAULT_CATEGORY_ID,
|
||||
tags: [TAG_PLUGINS, TAG_UTILITY],
|
||||
closeAfterExecute: true,
|
||||
queryTemplate: "create notebook ",
|
||||
queryPlaceholder: "Notebook name",
|
||||
handler: () => undefined
|
||||
},
|
||||
{
|
||||
id: "extension-holy-notes-delete-notebook-query",
|
||||
label: "Delete Notebook",
|
||||
description: "Delete a HolyNotes notebook.",
|
||||
keywords: ["holy", "notes", "notebook", "delete", "remove", "plugin", "extension"],
|
||||
categoryId: DEFAULT_CATEGORY_ID,
|
||||
tags: [TAG_PLUGINS, TAG_UTILITY],
|
||||
closeAfterExecute: true,
|
||||
queryTemplate: "delete notebook ",
|
||||
queryPlaceholder: "Notebook name",
|
||||
handler: () => undefined
|
||||
},
|
||||
{
|
||||
id: "extension-holy-notes-move-note-query",
|
||||
label: "Move Note",
|
||||
description: "Move a HolyNotes note to another notebook.",
|
||||
keywords: ["holy", "notes", "note", "move", "notebook", "plugin", "extension"],
|
||||
categoryId: DEFAULT_CATEGORY_ID,
|
||||
tags: [TAG_PLUGINS, TAG_UTILITY],
|
||||
closeAfterExecute: true,
|
||||
queryTemplate: "move note ",
|
||||
queryPlaceholder: "Find note by content, then destination",
|
||||
handler: () => undefined
|
||||
},
|
||||
{
|
||||
id: "extension-holy-notes-jump-note-query",
|
||||
label: "Jump To Note",
|
||||
description: "Jump to the original message for a HolyNotes note.",
|
||||
keywords: ["holy", "notes", "jump", "note", "message", "plugin", "extension"],
|
||||
categoryId: DEFAULT_CATEGORY_ID,
|
||||
tags: [TAG_PLUGINS, TAG_NAVIGATION],
|
||||
closeAfterExecute: true,
|
||||
queryTemplate: "jump to note ",
|
||||
queryPlaceholder: "Search note content",
|
||||
handler: () => undefined
|
||||
},
|
||||
{
|
||||
id: "extension-holy-notes-quick-save-last-message",
|
||||
label: "Quick Save Last Message",
|
||||
description: "Save the latest message in this channel to Main.",
|
||||
keywords: ["holy", "notes", "save", "last", "message", "bookmark", "main", "plugin", "extension"],
|
||||
categoryId: DEFAULT_CATEGORY_ID,
|
||||
tags: [TAG_PLUGINS, TAG_UTILITY],
|
||||
handler: runHolyNotesQuickSaveLastMessage
|
||||
}
|
||||
];
|
||||
}
|
||||
|
|
@ -69,18 +69,6 @@ export const EXTENSIONS_CATALOG: ExtensionDefinition[] = [
|
|||
tags: [TAG_PLUGINS, TAG_UTILITY],
|
||||
keywords: ["extension", "plugin", "random", "voice", "join", "channel", "vc"]
|
||||
},
|
||||
{
|
||||
id: HOLY_NOTES_EXTENSION_ID,
|
||||
label: "HolyNotes",
|
||||
description: "Control HolyNotes from the command palette.",
|
||||
detailCategoryId: HOLY_NOTES_EXTENSION_DETAIL_CATEGORY_ID,
|
||||
commandId: "extension-holy-notes-open",
|
||||
commandLabel: "Open Notes",
|
||||
commandDescription: "Open the HolyNotes notebook modal.",
|
||||
sourcePath: "src/equicordplugins/holyNotes",
|
||||
tags: [TAG_PLUGINS, TAG_UTILITY],
|
||||
keywords: ["extension", "plugin", "holy", "notes", "notebook", "bookmark"]
|
||||
},
|
||||
{
|
||||
id: SILENT_MESSAGE_TOGGLE_EXTENSION_ID,
|
||||
label: "SilentMessageToggle",
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import { Toasts } from "@webpack/common";
|
|||
|
||||
import { TAG_DEVELOPER, TAG_NAVIGATION, TAG_PLUGINS, TAG_UTILITY } from "../metadata/tags";
|
||||
import type { CommandEntry, ExtensionDefinition } from "../registry";
|
||||
import { createHolyNotesExtensionCommands } from "./actions/holyNotes";
|
||||
import { createRandomVoiceExtensionCommand } from "./actions/randomVoice";
|
||||
import { createScheduledMessagesExtensionCommands } from "./actions/scheduledMessages";
|
||||
import { createSilentMessageToggleExtensionCommand } from "./actions/silentMessageToggle";
|
||||
|
|
@ -19,7 +18,6 @@ import {
|
|||
EXTENSIONS_CATALOG_CATEGORY_ID,
|
||||
EXTENSIONS_DETAIL_PROVIDER_ID,
|
||||
EXTENSIONS_PACK_PROVIDER_ID,
|
||||
HOLY_NOTES_EXTENSION_ID,
|
||||
RANDOM_VOICE_EXTENSION_ID,
|
||||
SCHEDULED_MESSAGES_EXTENSION_ID,
|
||||
SILENT_MESSAGE_TOGGLE_EXTENSION_ID,
|
||||
|
|
@ -55,10 +53,6 @@ function createInstalledExtensionCommands(extensionsState: ExtensionsState): Com
|
|||
entries.push(createRandomVoiceExtensionCommand(extensionsState.extensionKeybinds));
|
||||
}
|
||||
|
||||
if (extensionsState.installedExtensionIds.has(HOLY_NOTES_EXTENSION_ID)) {
|
||||
entries.push(...createHolyNotesExtensionCommands());
|
||||
}
|
||||
|
||||
if (extensionsState.installedExtensionIds.has(SILENT_MESSAGE_TOGGLE_EXTENSION_ID)) {
|
||||
entries.push(createSilentMessageToggleExtensionCommand(extensionsState.extensionKeybinds));
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue