Merge branch 'dev' into tray-stuff
This commit is contained in:
commit
03006efd08
699 changed files with 18386 additions and 21366 deletions
9
.github/workflows/build.yml
vendored
9
.github/workflows/build.yml
vendored
|
|
@ -23,10 +23,10 @@ jobs:
|
|||
|
||||
- uses: pnpm/action-setup@v3
|
||||
|
||||
- name: Use Node.js 20
|
||||
- name: Use Node.js 24
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 24
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
|
|
@ -68,17 +68,20 @@ jobs:
|
|||
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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
7
.github/workflows/nixosBuild.yml
vendored
7
.github/workflows/nixosBuild.yml
vendored
|
|
@ -20,10 +20,10 @@ jobs:
|
|||
|
||||
- uses: pnpm/action-setup@v3
|
||||
|
||||
- name: Use Node.js 20
|
||||
- name: Use Node.js 24
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 24
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
|
|
@ -50,7 +50,7 @@ jobs:
|
|||
cp browser/browser.* release
|
||||
cp Equicord.user.{js,js.LEGAL.txt} release
|
||||
|
||||
# copy the plugin data jsons, the extension zips and the desktop/vesktop asars
|
||||
# copy the plugin data jsons, the extension zips and the desktop/equibop asars
|
||||
cp *.{json,zip,asar} release
|
||||
|
||||
# legacy un-asared files
|
||||
|
|
@ -69,6 +69,7 @@ jobs:
|
|||
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
|
||||
|
||||
- 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/*
|
||||
|
|
|
|||
16
.github/workflows/publish.yml
vendored
16
.github/workflows/publish.yml
vendored
|
|
@ -62,11 +62,11 @@ jobs:
|
|||
if: steps.check.outputs.release == 'true'
|
||||
uses: pnpm/action-setup@v3
|
||||
|
||||
- name: Use Node.js 20
|
||||
- name: Use Node.js 24
|
||||
if: steps.check.outputs.release == 'true'
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 24
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
|
|
@ -79,12 +79,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: pnpx @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: pnpx @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
|
|
@ -33,10 +33,10 @@ jobs:
|
|||
|
||||
- uses: pnpm/action-setup@v3 # Install pnpm using packageManager key in package.json
|
||||
|
||||
- name: Use Node.js 20
|
||||
- name: Use Node.js 24
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 24
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
|
|
|
|||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
|
|
@ -18,10 +18,10 @@ jobs:
|
|||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3 # Install pnpm using packageManager key in package.json
|
||||
|
||||
- name: Use Node.js 20
|
||||
- name: Use Node.js 24
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 24
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
|
|
|
|||
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",
|
||||
|
|
|
|||
52
package.json
52
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": {
|
||||
|
|
@ -54,47 +54,47 @@
|
|||
"gifenc": "github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3",
|
||||
"idb": "8.0.3",
|
||||
"jsqr": "1.4.0",
|
||||
"monaco-editor": "^0.54.0",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"nanoid": "^5.1.6",
|
||||
"native-file-system-adapter": "^3.0.1",
|
||||
"usercss-meta": "^0.12.0",
|
||||
"virtual-merge": "^1.0.1"
|
||||
"virtual-merge": "^1.0.1",
|
||||
"yaml-js": "^0.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron/asar": "^4.0.1",
|
||||
"@stylistic/eslint-plugin": "^5.6.0",
|
||||
"@types/adm-zip": "^0.5.7",
|
||||
"@types/chrome": "^0.1.30",
|
||||
"@types/diff": "^7.0.2",
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@types/node": "^24.10.1",
|
||||
"@electron/asar": "^4.2.0",
|
||||
"@stylistic/eslint-plugin": "^5.10.0",
|
||||
"@types/chrome": "^0.1.40",
|
||||
"@types/lodash": "^4.17.24",
|
||||
"@types/node": "^25.6.0",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@types/yazl": "^3.3.0",
|
||||
"@types/yazl": "^3.3.1",
|
||||
"@vencord/discord-types": "link:packages/discord-types",
|
||||
"diff": "^8.0.2",
|
||||
"esbuild": "^0.27.0",
|
||||
"eslint": "9.39.1",
|
||||
"diff": "^9.0.0",
|
||||
"esbuild": "^0.28.0",
|
||||
"eslint": "10.2.0",
|
||||
"eslint-import-resolver-alias": "^1.1.2",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-simple-header": "^1.2.1",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"eslint-plugin-unused-imports": "^4.3.0",
|
||||
"eslint-plugin-simple-import-sort": "^13.0.0",
|
||||
"eslint-plugin-unused-imports": "^4.4.1",
|
||||
"highlight.js": "11.11.1",
|
||||
"html-minifier-terser": "^7.2.0",
|
||||
"moment": "^2.22.2",
|
||||
"p-limit": "^7.3.0",
|
||||
"puppeteer-core": "^24.30.0",
|
||||
"standalone-electron-types": "^34.2.0",
|
||||
"stylelint": "^16.25.0",
|
||||
"stylelint-config-standard": "^39.0.1",
|
||||
"puppeteer-core": "^24.41.0",
|
||||
"standalone-electron-types": "^41.2.1",
|
||||
"stylelint": "^17.8.0",
|
||||
"stylelint-config-standard": "^40.0.0",
|
||||
"svgo": "^4.0.1",
|
||||
"ts-patch": "^3.3.0",
|
||||
"ts-pattern": "^5.6.0",
|
||||
"tsx": "^4.20.6",
|
||||
"type-fest": "^5.2.0",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.47.0",
|
||||
"typescript-transform-paths": "^3.5.5",
|
||||
"ts-pattern": "^5.9.0",
|
||||
"tsx": "^4.21.0",
|
||||
"type-fest": "^5.5.0",
|
||||
"typescript": "^6.0.2",
|
||||
"typescript-eslint": "^8.58.2",
|
||||
"typescript-transform-paths": "^3.5.6",
|
||||
"zip-local": "^0.3.5"
|
||||
},
|
||||
"packageManager": "pnpm@10.33.0",
|
||||
|
|
|
|||
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;
|
||||
}>;
|
||||
|
|
|
|||
1857
pnpm-lock.yaml
generated
1857
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -77,7 +77,7 @@ const globNativesPlugin = {
|
|||
});
|
||||
|
||||
build.onLoad({ filter, namespace: "import-natives" }, async () => {
|
||||
const pluginDirs = ["plugins", "userplugins", "equicordplugins"];
|
||||
const pluginDirs = ["plugins", "equicordplugins", "userplugins"];
|
||||
let code = "";
|
||||
let natives = "\n";
|
||||
let i = 0;
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ export const globPlugins = kind => ({
|
|||
});
|
||||
|
||||
build.onLoad({ filter, namespace: "import-plugins" }, async () => {
|
||||
const pluginDirs = ["plugins/_api", "plugins/_core", "plugins", "userplugins", "equicordplugins", "equicordplugins/_api"];
|
||||
const pluginDirs = ["plugins/_api", "plugins/_core", "plugins", "equicordplugins/_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;
|
||||
|
|
|
|||
|
|
@ -81,7 +81,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) {
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ export interface PluginData {
|
|||
name: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
searchTerms: string[];
|
||||
authors: Dev[];
|
||||
dependencies: string[];
|
||||
hasPatches: boolean;
|
||||
|
|
@ -152,7 +153,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) {
|
||||
|
|
@ -208,9 +210,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 { 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, 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 {
|
||||
align-items: center;
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
];
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue