Compare commits

..

29 commits

Author SHA1 Message Date
Vendicated
7781dface9
fix: apply update only on quit 2024-07-21 02:52:45 +02:00
Vendicated
f9fb3bbba7
fix watch erroring when clean building 2024-07-21 01:29:46 +02:00
Vendicated
8c5217f9f2
require node>=20 2024-07-21 00:54:41 +02:00
Vendicated
5fab0207fa
do not the version 2024-07-19 23:05:31 +02:00
Vendicated
0cf6542f3c
delete .map files 2024-07-19 23:03:49 +02:00
Vendicated
f4c19705d7
ignore 0 byte files 2024-07-19 22:57:40 +02:00
Vendicated
9f5dee00d4
workflow: run on any repo 2024-07-19 22:53:46 +02:00
Vendicated
63b359d970
version 2024-07-19 22:51:29 +02:00
Vendicated
090a9f5caf
don't use asar in dev 2024-07-19 22:02:17 +02:00
Vendicated
d48f52e8c3
remove obsolete workaround 2024-07-19 21:34:39 +02:00
Vendicated
7aeb884390
i forgot what i changed but im committing anyway 2024-07-19 21:32:10 +02:00
Vendicated
44cd30b23a
fix types 2024-07-19 20:45:01 +02:00
Vendicated
22fdde6e39
add error handling back 2024-07-19 20:43:23 +02:00
Vendicated
8fa7d006a4
update build scripts to latest esbuild & typescript 2024-07-19 20:40:24 +02:00
Vendicated
d84943a6d7
no balls :/ 2024-07-19 03:29:54 +02:00
Vendicated
839b62c2d9
update runInstaller 2024-07-19 03:29:44 +02:00
Vendicated
445dfce4a8
fix css watch 2024-07-19 02:57:11 +02:00
Vendicated
65e91cf22e
omg i love when vscode gets stuck on saving 2024-07-19 02:51:52 +02:00
Vendicated
b91cb742b1
update outdated paths 2024-07-19 02:50:24 +02:00
Vendicated
611b94b6c7
use exit instead of quit 2024-07-19 01:43:26 +02:00
Vendicated
ffb73107e6
flatpak explosion 2024-07-19 01:21:11 +02:00
Vendicated
d9c469755b
fixes 2024-07-19 01:20:09 +02:00
Vendicated
fbfbe33c0a
j 2024-07-19 00:53:40 +02:00
Vendicated
2ace675c00
migrate legacy installs 2024-07-19 00:52:32 +02:00
Vendicated
7148c29ed1
more clean 2024-07-18 21:52:31 +02:00
Vendicated
5797506569
add dev build workaround 2024-07-18 21:50:12 +02:00
Vendicated
e119f092bf
nyaa 2024-07-18 04:37:44 +02:00
Vendicated
e8910615a7
bleh 2024-07-18 04:35:51 +02:00
Vendicated
6cf2e0c2a5
[WIP] package vencord as asar 2024-07-18 04:34:09 +02:00
286 changed files with 4722 additions and 5753 deletions

98
.eslintrc.json Normal file
View file

@ -0,0 +1,98 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"ignorePatterns": ["dist", "browser", "packages/vencord-types"],
"plugins": [
"@typescript-eslint",
"simple-header",
"simple-import-sort",
"unused-imports",
"path-alias"
],
"settings": {
"import/resolver": {
"alias": {
"map": [
["@webpack", "./src/webpack"],
["@webpack/common", "./src/webpack/common"],
["@utils", "./src/utils"],
["@api", "./src/api"],
["@components", "./src/components"]
]
}
}
},
"rules": {
// Since it's only been a month and Vencord has already been stolen
// by random skids who rebranded it to "AlphaCord" and erased all license
// information
"simple-header/header": [
"error",
{
"files": ["scripts/header-new.txt", "scripts/header-old.txt"],
"templates": { "author": [".*", "Vendicated and contributors"] }
}
],
"quotes": ["error", "double", { "avoidEscape": true }],
"jsx-quotes": ["error", "prefer-double"],
"no-mixed-spaces-and-tabs": "error",
"indent": ["error", 4, { "SwitchCase": 1 }],
"arrow-parens": ["error", "as-needed"],
"eol-last": ["error", "always"],
"@typescript-eslint/func-call-spacing": ["error", "never"],
"no-multi-spaces": "error",
"no-trailing-spaces": "error",
"no-whitespace-before-property": "error",
"semi": ["error", "always"],
"semi-style": ["error", "last"],
"space-in-parens": ["error", "never"],
"block-spacing": ["error", "always"],
"object-curly-spacing": ["error", "always"],
"eqeqeq": ["error", "always", { "null": "ignore" }],
"spaced-comment": ["error", "always", { "markers": ["!"] }],
"yoda": "error",
"prefer-destructuring": ["error", {
"VariableDeclarator": { "array": false, "object": true },
"AssignmentExpression": { "array": false, "object": false }
}],
"operator-assignment": ["error", "always"],
"no-useless-computed-key": "error",
"no-unneeded-ternary": ["error", { "defaultAssignment": false }],
"no-invalid-regexp": "error",
"no-constant-condition": ["error", { "checkLoops": false }],
"no-duplicate-imports": "error",
"no-extra-semi": "error",
"dot-notation": "error",
"no-useless-escape": [
"error",
{
"extra": "i"
}
],
"no-fallthrough": "error",
"for-direction": "error",
"no-async-promise-executor": "error",
"no-cond-assign": "error",
"no-dupe-else-if": "error",
"no-duplicate-case": "error",
"no-irregular-whitespace": "error",
"no-loss-of-precision": "error",
"no-misleading-character-class": "error",
"no-prototype-builtins": "error",
"no-regex-spaces": "error",
"no-shadow-restricted-names": "error",
"no-unexpected-multiline": "error",
"no-unsafe-optional-chaining": "error",
"no-useless-backreference": "error",
"use-isnan": "error",
"prefer-const": "error",
"prefer-spread": "error",
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error",
"unused-imports/no-unused-imports": "error",
"path-alias/no-relative": "error"
}
}

View file

@ -40,9 +40,28 @@ jobs:
- name: Generate plugin list
run: pnpm generatePluginJson dist/plugins.json dist/plugin-readmes.json
- name: Clean up obsolete files
- name: Collect files to be released
run: |
rm -rf dist/*-unpacked dist/monaco Vencord.user.css vencordDesktopRenderer.css vencordDesktopRenderer.css.map
cd dist
mkdir release
cp browser/browser.* release
cp Vencord.user.{js,js.LEGAL.txt} release
# copy the plugin data jsons, the extension zips and the desktop/vesktop asars
cp *.{json,zip,asar} release
# legacy un-asared files
# FIXME: remove at some point
cp desktop/* release
for file in vesktop/*; do
filename=$(basename "$file")
cp "$file" "release/vencordDesktop${filename^}"
done
find release -size 0 -delete
rm release/package.json
rm release/*.map
- name: Get some values needed for the release
id: release_values
@ -50,16 +69,14 @@ jobs:
echo "release_tag=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Upload DevBuild as release
if: github.repository == 'Vendicated/Vencord'
run: |
gh release upload devbuild --clobber dist/*
gh release upload devbuild --clobber dist/release/*
gh release edit devbuild --title "DevBuild $RELEASE_TAG"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_TAG: ${{ env.release_tag }}
- name: Upload DevBuild to builds repo
if: github.repository == 'Vendicated/Vencord'
run: |
git config --global user.name "$USERNAME"
git config --global user.email actions@github.com
@ -69,7 +86,7 @@ jobs:
GLOBIGNORE=.git:.gitignore:README.md:LICENSE
rm -rf *
cp -r ../dist/* .
cp -r ../dist/release/* .
git add -A
git commit -m "Builds for https://github.com/$GITHUB_REPOSITORY/commit/$GITHUB_SHA"

View file

@ -22,7 +22,7 @@ jobs:
- uses: pnpm/action-setup@v3 # Install pnpm using packageManager key in package.json
- name: Use Node.js 19
- name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20
@ -36,7 +36,7 @@ jobs:
- name: Publish extension
run: |
cd dist/chromium-unpacked
cd dist/browser/chromium-unpacked
pnpx chrome-webstore-upload-cli@2.1.0 upload --auto-publish
env:
EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }}

2
.gitignore vendored
View file

@ -18,5 +18,7 @@ lerna-debug.log*
.pnpm-debug.log*
*.tsbuildinfo
src/userplugins
ExtensionCache/
settings/

3
.gitmodules vendored
View file

@ -1,3 +0,0 @@
[submodule "src/userplugins/vc-message-logger-enhanced"]
path = src/userplugins/vc-message-logger-enhanced
url = https://github.com/Syncxv/vc-message-logger-enhanced.git

View file

@ -1,6 +1,7 @@
{
"extends": "stylelint-config-standard",
"rules": {
"indentation": 4,
"selector-class-pattern": [
"^[a-z][a-zA-Z0-9]*(-[a-z0-9][a-zA-Z0-9]*)*$",
{

View file

@ -14,6 +14,8 @@
"typescript.preferences.quoteStyle": "double",
"javascript.preferences.quoteStyle": "double",
"eslint.experimental.useFlatConfig": false,
"gitlens.remotes": [
{
"domain": "codeberg.org",

View file

@ -31,7 +31,6 @@ Before starting your plugin:
- No FakeDeafen or FakeMute
- No StereoMic
- No plugins that simply hide or redesign ui elements. This can be done with CSS
- No plugins that interact with specific Discord bots (official Discord apps like Youtube WatchTogether are okay)
- No selfbots or API spam (animated status, message pruner, auto reply, nitro snipers, etc)
- No untrusted third party APIs. Popular services like Google or GitHub are fine, but absolutely no self hosted ones
- No plugins that require the user to enter their own API key

View file

@ -5,7 +5,6 @@
// @author Vendicated (https://github.com/Vendicated)
// @namespace https://github.com/Vendicated/Vencord
// @supportURL https://github.com/Vendicated/Vencord
// @icon https://raw.githubusercontent.com/Vendicated/Vencord/refs/heads/main/browser/icon.png
// @license GPL-3.0
// @match *://*.discord.com/*
// @grant GM_xmlhttpRequest

View file

@ -1,126 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
// @ts-check
import stylistic from "@stylistic/eslint-plugin";
import pathAlias from "eslint-plugin-path-alias";
import header from "eslint-plugin-simple-header";
import simpleImportSort from "eslint-plugin-simple-import-sort";
import unusedImports from "eslint-plugin-unused-imports";
import tseslint from "typescript-eslint";
export default tseslint.config(
{ ignores: ["dist", "browser", "packages/vencord-types"] },
{
files: ["src/**/*.{tsx,ts,mts,mjs,js,jsx}", "eslint.config.mjs"],
plugins: {
"simple-header": header,
"@stylistic": stylistic,
"@typescript-eslint": tseslint.plugin,
"simple-import-sort": simpleImportSort,
"unused-imports": unusedImports,
"path-alias": pathAlias,
},
settings: {
"import/resolver": {
map: [
["@webpack", "./src/webpack"],
["@webpack/common", "./src/webpack/common"],
["@utils", "./src/utils"],
["@api", "./src/api"],
["@components", "./src/components"]
]
}
},
languageOptions: {
parser: tseslint.parser,
parserOptions: {
project: ["./tsconfig.json"],
tsconfigRootDir: import.meta.dirname
}
},
rules: {
/*
* Since it's only been a month and Vencord has already been stolen
* by random skids who rebranded it to "AlphaCord" and erased all license
* information
*/
"simple-header/header": [
"error",
{
"files": ["scripts/header-new.txt", "scripts/header-old.txt"],
"templates": { "author": [".*", "Vendicated and contributors"] }
}
],
// Style Rules
"@stylistic/jsx-quotes": ["error", "prefer-double"],
"@stylistic/quotes": ["error", "double", { "avoidEscape": true }],
"@stylistic/no-mixed-spaces-and-tabs": "error",
"@stylistic/arrow-parens": ["error", "as-needed"],
"@stylistic/eol-last": ["error", "always"],
"@stylistic/no-multi-spaces": "error",
"@stylistic/no-trailing-spaces": "error",
"@stylistic/no-whitespace-before-property": "error",
"@stylistic/semi": ["error", "always"],
"@stylistic/semi-style": ["error", "last"],
"@stylistic/space-in-parens": ["error", "never"],
"@stylistic/block-spacing": ["error", "always"],
"@stylistic/object-curly-spacing": ["error", "always"],
"@stylistic/spaced-comment": ["error", "always", { "markers": ["!"] }],
"@stylistic/no-extra-semi": "error",
// TS Rules
"@stylistic/func-call-spacing": ["error", "never"],
// ESLint Rules
"yoda": "error",
"eqeqeq": ["error", "always", { "null": "ignore" }],
"prefer-destructuring": ["error", {
"VariableDeclarator": { "array": false, "object": true },
"AssignmentExpression": { "array": false, "object": false }
}],
"operator-assignment": ["error", "always"],
"no-useless-computed-key": "error",
"no-unneeded-ternary": ["error", { "defaultAssignment": false }],
"no-invalid-regexp": "error",
"no-constant-condition": ["error", { "checkLoops": false }],
"no-duplicate-imports": "error",
"dot-notation": "error",
"no-useless-escape": [
"error",
{
"extra": "i"
}
],
"no-fallthrough": "error",
"for-direction": "error",
"no-async-promise-executor": "error",
"no-cond-assign": "error",
"no-dupe-else-if": "error",
"no-duplicate-case": "error",
"no-irregular-whitespace": "error",
"no-loss-of-precision": "error",
"no-misleading-character-class": "error",
"no-prototype-builtins": "error",
"no-regex-spaces": "error",
"no-shadow-restricted-names": "error",
"no-unexpected-multiline": "error",
"no-unsafe-optional-chaining": "error",
"no-useless-backreference": "error",
"use-isnan": "error",
"prefer-const": "error",
"prefer-spread": "error",
// Plugin Rules
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error",
"unused-imports/no-unused-imports": "error",
"path-alias/no-relative": "error"
}
}
);

View file

@ -1,7 +1,7 @@
{
"name": "vencord",
"private": "true",
"version": "1.10.9",
"version": "1.9.5",
"description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": {
@ -14,9 +14,9 @@
"license": "GPL-3.0-or-later",
"author": "Vendicated",
"scripts": {
"build": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs",
"build": "tsx scripts/build/build.mts",
"buildStandalone": "pnpm build --standalone",
"buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs",
"buildWeb": "tsx scripts/build/buildWeb.mts",
"buildWebStandalone": "pnpm buildWeb --standalone",
"buildReporter": "pnpm buildWebStandalone --reporter --skip-extension",
"buildReporterDesktop": "pnpm build --reporter",
@ -27,7 +27,7 @@
"generateTypes": "tspc --emitDeclarationOnly --declaration --outDir packages/vencord-types",
"inject": "node scripts/runInstaller.mjs",
"uninject": "node scripts/runInstaller.mjs",
"lint": "eslint",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --ignore-pattern src/userplugins",
"lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins",
"lint:fix": "pnpm lint --fix",
"test": "pnpm buildStandalone && pnpm lint && pnpm lint-styles && pnpm testTsc && pnpm generatePluginJson",
@ -35,55 +35,54 @@
"testTsc": "tsc --noEmit"
},
"dependencies": {
"@intrnl/xxhash64": "^0.1.2",
"@sapphi-red/web-noise-suppressor": "0.3.5",
"@sapphi-red/web-noise-suppressor": "0.3.3",
"@vap/core": "0.0.12",
"@vap/shiki": "0.10.5",
"fflate": "^0.8.2",
"eslint-plugin-simple-header": "^1.0.2",
"fflate": "^0.7.4",
"gifenc": "github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3",
"monaco-editor": "^0.50.0",
"nanoid": "^5.0.7",
"nanoid": "^4.0.2",
"virtual-merge": "^1.0.1"
},
"devDependencies": {
"@stylistic/eslint-plugin": "^2.6.1",
"@types/chrome": "^0.0.269",
"@types/diff": "^5.2.1",
"@types/lodash": "^4.17.7",
"@types/node": "^22.0.3",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/yazl": "^2.4.5",
"diff": "^5.2.0",
"@electron/asar": "^3.2.10",
"@types/chrome": "^0.0.246",
"@types/diff": "^5.0.3",
"@types/lodash": "^4.14.194",
"@types/node": "^18.16.3",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.1",
"@types/yazl": "^2.4.2",
"@typescript-eslint/eslint-plugin": "^5.59.1",
"@typescript-eslint/parser": "^5.59.1",
"diff": "^5.1.0",
"discord-types": "^1.3.26",
"esbuild": "^0.15.18",
"eslint": "^9.8.0",
"esbuild": "^0.23.0",
"eslint": "^8.46.0",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-path-alias": "2.1.0",
"eslint-plugin-simple-header": "^1.1.1",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-unused-imports": "^4.0.1",
"highlight.js": "10.7.3",
"eslint-plugin-path-alias": "^1.0.0",
"eslint-plugin-simple-import-sort": "^10.0.0",
"eslint-plugin-unused-imports": "^2.0.0",
"highlight.js": "10.6.0",
"html-minifier-terser": "^7.2.0",
"moment": "^2.30.1",
"puppeteer-core": "^22.15.0",
"moment": "^2.29.4",
"puppeteer-core": "^19.11.1",
"standalone-electron-types": "^1.0.0",
"stylelint": "^16.8.1",
"stylelint-config-standard": "^36.0.1",
"ts-patch": "^3.2.1",
"ts-pattern": "^5.3.1",
"tsx": "^4.16.5",
"type-fest": "^4.23.0",
"typescript": "^5.5.4",
"typescript-eslint": "^8.0.0",
"stylelint": "^15.6.0",
"stylelint-config-standard": "^33.0.0",
"ts-patch": "^3.1.2",
"tsx": "^4.16.2",
"type-fest": "^3.9.0",
"typescript": "^5.4.5",
"typescript-transform-paths": "^3.4.7",
"zip-local": "^0.3.5"
},
"packageManager": "pnpm@9.1.0",
"pnpm": {
"patchedDependencies": {
"eslint@9.8.0": "patches/eslint@9.8.0.patch",
"eslint-plugin-path-alias@2.1.0": "patches/eslint-plugin-path-alias@2.1.0.patch"
"eslint-plugin-path-alias@1.0.0": "patches/eslint-plugin-path-alias@1.0.0.patch",
"eslint@8.46.0": "patches/eslint@8.46.0.patch"
},
"peerDependencyRules": {
"ignoreMissing": [
@ -106,7 +105,7 @@
"sourceDir": "./dist/firefox-unpacked"
},
"engines": {
"node": ">=18",
"node": ">=20",
"pnpm": ">=9"
}
}

View file

@ -0,0 +1,13 @@
diff --git a/lib/rules/no-relative.js b/lib/rules/no-relative.js
index 71594c83f1f4f733ffcc6047d7f7084348335dbe..d8623d87c89499c442171db3272cba07c9efabbe 100644
--- a/lib/rules/no-relative.js
+++ b/lib/rules/no-relative.js
@@ -41,7 +41,7 @@ module.exports = {
ImportDeclaration(node) {
const importPath = node.source.value;
- if (!/^(\.?\.\/)/.test(importPath)) {
+ if (!/^(\.\.\/)/.test(importPath)) {
return;
}

View file

@ -1,14 +0,0 @@
diff --git a/dist/index.js b/dist/index.js
index 67de6fb139070fd0e49beca65e3b63c531202e16..aa2883c8126e4952a42872ee920f59547a066430 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -1 +1 @@
-var C=Object.create;var f=Object.defineProperty;var I=Object.getOwnPropertyDescriptor;var U=Object.getOwnPropertyNames;var S=Object.getPrototypeOf,F=Object.prototype.hasOwnProperty;var $=(e,t)=>{for(var r in t)f(e,r,{get:t[r],enumerable:!0})},y=(e,t,r,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let s of U(t))!F.call(e,s)&&s!==r&&f(e,s,{get:()=>t[s],enumerable:!(i=I(t,s))||i.enumerable});return e};var b=(e,t,r)=>(r=e!=null?C(S(e)):{},y(t||!e||!e.__esModule?f(r,"default",{value:e,enumerable:!0}):r,e)),D=e=>y(f({},"__esModule",{value:!0}),e);var N={};$(N,{default:()=>J});module.exports=D(N);var h="eslint-plugin-path-alias",v="2.0.0";var l=require("path"),M=b(require("nanomatch"));function j(e){return`https://github/com/msfragala/eslint-plugin-path-alias/blob/master/docs/rules/${e}.md`}var R=require("get-tsconfig"),a=require("path"),w=b(require("find-pkg")),O=require("fs");function P(e){if(e.options[0]?.paths)return z(e);let t=e.getFilename?.()??e.filename,r=(0,R.getTsconfig)(t);if(r?.config?.compilerOptions?.paths)return q(r);let i=w.default.sync((0,a.dirname)(t));if(!i)return;let s=JSON.parse((0,O.readFileSync)(i).toString());if(s?.imports)return L(s,i)}function L(e,t){let r=new Map,i=e.imports??{},s=(0,a.dirname)(t);return Object.entries(i).forEach(([o,n])=>{if(!n||typeof n!="string")return;let p=(0,a.resolve)(s,n);r.set(o,[p])}),r}function q(e){let t=new Map,r=e?.config?.compilerOptions?.paths??{},i=(0,a.dirname)(e.path);return e.config.compilerOptions?.baseUrl&&(i=(0,a.resolve)((0,a.dirname)(e.path),e.config.compilerOptions.baseUrl)),Object.entries(r).forEach(([s,o])=>{s=s.replace(/\/\*$/,""),o=o.map(n=>(0,a.resolve)(i,n.replace(/\/\*$/,""))),t.set(s,o)}),t}function z(e){let t=new Map,r=e.options[0]?.paths??{};return Object.entries(r).forEach(([i,s])=>{if(!s||typeof s!="string")return;if(s.startsWith("/")){t.set(i,[s]);return}let o=e.getCwd?.()??e.cwd,n=(0,a.resolve)(o,s);t.set(i,[n])}),t}var T={meta:{type:"suggestion",docs:{description:"Ensure imports use path aliases whenever possible vs. relative paths",url:j("no-relative")},fixable:"code",schema:[{type:"object",properties:{exceptions:{type:"array",items:{type:"string"}},paths:{type:"object"}},additionalProperties:!1}],messages:{shouldUseAlias:"Import should use path alias instead of relative path"}},create(e){let t=e.options[0]?.exceptions,r=e.getFilename?.()??e.filename,i=P(e);return i?.size?{ImportExpression(s){if(s.source.type!=="Literal"||typeof s.source.value!="string")return;let o=s.source.raw,n=s.source.value;if(!/^(\.?\.\/)/.test(n))return;let p=(0,l.resolve)((0,l.dirname)(r),n);if(A(p,t))return;let c=k(p,i);c&&e.report({node:s,messageId:"shouldUseAlias",data:{alias:c},fix(m){let g=E(p,c,i.get(c)),d=o.replace(n,g);return m.replaceText(s.source,d)}})},ImportDeclaration(s){if(typeof s.source.value!="string")return;let o=s.source.value;if(!/^(\.?\.\/)/.test(o))return;let n=(0,l.resolve)((0,l.dirname)(r),o),p=A(n,t),u=k(n,i);p||u&&e.report({node:s,messageId:"shouldUseAlias",data:{alias:u},fix(c){let m=s.source.raw,g=E(n,u,i.get(u)),d=m.replace(o,g);return c.replaceText(s.source,d)}})}}:{}}};function k(e,t){return Array.from(t.keys()).find(r=>t.get(r).some(s=>e.indexOf(s)===0))}function A(e,t){if(!t)return!1;let r=(0,l.basename)(e);return(0,M.default)(r,t).includes(r)}function E(e,t,r){for(let i of r)if(e.indexOf(i)===0)return e.replace(i,t)}var J={name:h,version:v,meta:{name:h,version:v},rules:{"no-relative":T}};
+var C=Object.create;var f=Object.defineProperty;var I=Object.getOwnPropertyDescriptor;var U=Object.getOwnPropertyNames;var S=Object.getPrototypeOf,F=Object.prototype.hasOwnProperty;var $=(e,t)=>{for(var r in t)f(e,r,{get:t[r],enumerable:!0})},y=(e,t,r,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let s of U(t))!F.call(e,s)&&s!==r&&f(e,s,{get:()=>t[s],enumerable:!(i=I(t,s))||i.enumerable});return e};var b=(e,t,r)=>(r=e!=null?C(S(e)):{},y(t||!e||!e.__esModule?f(r,"default",{value:e,enumerable:!0}):r,e)),D=e=>y(f({},"__esModule",{value:!0}),e);var N={};$(N,{default:()=>J});module.exports=D(N);var h="eslint-plugin-path-alias",v="2.0.0";var l=require("path"),M=b(require("nanomatch"));function j(e){return`https://github/com/msfragala/eslint-plugin-path-alias/blob/master/docs/rules/${e}.md`}var R=require("get-tsconfig"),a=require("path"),w=b(require("find-pkg")),O=require("fs");function P(e){if(e.options[0]?.paths)return z(e);let t=e.getFilename?.()??e.filename,r=(0,R.getTsconfig)(t);if(r?.config?.compilerOptions?.paths)return q(r);let i=w.default.sync((0,a.dirname)(t));if(!i)return;let s=JSON.parse((0,O.readFileSync)(i).toString());if(s?.imports)return L(s,i)}function L(e,t){let r=new Map,i=e.imports??{},s=(0,a.dirname)(t);return Object.entries(i).forEach(([o,n])=>{if(!n||typeof n!="string")return;let p=(0,a.resolve)(s,n);r.set(o,[p])}),r}function q(e){let t=new Map,r=e?.config?.compilerOptions?.paths??{},i=(0,a.dirname)(e.path);return e.config.compilerOptions?.baseUrl&&(i=(0,a.resolve)((0,a.dirname)(e.path),e.config.compilerOptions.baseUrl)),Object.entries(r).forEach(([s,o])=>{s=s.replace(/\/\*$/,""),o=o.map(n=>(0,a.resolve)(i,n.replace(/\/\*$/,""))),t.set(s,o)}),t}function z(e){let t=new Map,r=e.options[0]?.paths??{};return Object.entries(r).forEach(([i,s])=>{if(!s||typeof s!="string")return;if(s.startsWith("/")){t.set(i,[s]);return}let o=e.getCwd?.()??e.cwd,n=(0,a.resolve)(o,s);t.set(i,[n])}),t}var T={meta:{type:"suggestion",docs:{description:"Ensure imports use path aliases whenever possible vs. relative paths",url:j("no-relative")},fixable:"code",schema:[{type:"object",properties:{exceptions:{type:"array",items:{type:"string"}},paths:{type:"object"}},additionalProperties:!1}],messages:{shouldUseAlias:"Import should use path alias instead of relative path"}},create(e){let t=e.options[0]?.exceptions,r=e.getFilename?.()??e.filename,i=P(e);return i?.size?{ImportExpression(s){if(s.source.type!=="Literal"||typeof s.source.value!="string")return;let o=s.source.raw,n=s.source.value;if(!/^(\.\.\/)/.test(n))return;let p=(0,l.resolve)((0,l.dirname)(r),n);if(A(p,t))return;let c=k(p,i);c&&e.report({node:s,messageId:"shouldUseAlias",data:{alias:c},fix(m){let g=E(p,c,i.get(c)),d=o.replace(n,g);return m.replaceText(s.source,d)}})},ImportDeclaration(s){if(typeof s.source.value!="string")return;let o=s.source.value;if(!/^(\.\.\/)/.test(o))return;let n=(0,l.resolve)((0,l.dirname)(r),o),p=A(n,t),u=k(n,i);p||u&&e.report({node:s,messageId:"shouldUseAlias",data:{alias:u},fix(c){let m=s.source.raw,g=E(n,u,i.get(u)),d=m.replace(o,g);return c.replaceText(s.source,d)}})}}:{}}};function k(e,t){return Array.from(t.keys()).find(r=>t.get(r).some(s=>e.indexOf(s)===0))}function A(e,t){if(!t)return!1;let r=(0,l.basename)(e);return(0,M.default)(r,t).includes(r)}function E(e,t,r){for(let i of r)if(e.indexOf(i)===0)return e.replace(i,t)}var J={name:h,version:v,meta:{name:h,version:v},rules:{"no-relative":T}};
diff --git a/dist/index.mjs b/dist/index.mjs
index 96de18e06d4cc413e11af038cd760e4804c32e59..27e8c4e3e2c942400cc3982e52159904ca6eedfa 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -1 +1 @@
-var d="eslint-plugin-path-alias",h="2.0.0";import{dirname as x,resolve as j,basename as I}from"path";import U from"nanomatch";function y(e){return`https://github/com/msfragala/eslint-plugin-path-alias/blob/master/docs/rules/${e}.md`}import{getTsconfig as k}from"get-tsconfig";import{resolve as c,dirname as u}from"path";import A from"find-pkg";import{readFileSync as E}from"fs";function b(e){if(e.options[0]?.paths)return C(e);let s=e.getFilename?.()??e.filename,i=k(s);if(i?.config?.compilerOptions?.paths)return T(i);let r=A.sync(u(s));if(!r)return;let t=JSON.parse(E(r).toString());if(t?.imports)return M(t,r)}function M(e,s){let i=new Map,r=e.imports??{},t=u(s);return Object.entries(r).forEach(([o,n])=>{if(!n||typeof n!="string")return;let a=c(t,n);i.set(o,[a])}),i}function T(e){let s=new Map,i=e?.config?.compilerOptions?.paths??{},r=u(e.path);return e.config.compilerOptions?.baseUrl&&(r=c(u(e.path),e.config.compilerOptions.baseUrl)),Object.entries(i).forEach(([t,o])=>{t=t.replace(/\/\*$/,""),o=o.map(n=>c(r,n.replace(/\/\*$/,""))),s.set(t,o)}),s}function C(e){let s=new Map,i=e.options[0]?.paths??{};return Object.entries(i).forEach(([r,t])=>{if(!t||typeof t!="string")return;if(t.startsWith("/")){s.set(r,[t]);return}let o=e.getCwd?.()??e.cwd,n=c(o,t);s.set(r,[n])}),s}var P={meta:{type:"suggestion",docs:{description:"Ensure imports use path aliases whenever possible vs. relative paths",url:y("no-relative")},fixable:"code",schema:[{type:"object",properties:{exceptions:{type:"array",items:{type:"string"}},paths:{type:"object"}},additionalProperties:!1}],messages:{shouldUseAlias:"Import should use path alias instead of relative path"}},create(e){let s=e.options[0]?.exceptions,i=e.getFilename?.()??e.filename,r=b(e);return r?.size?{ImportExpression(t){if(t.source.type!=="Literal"||typeof t.source.value!="string")return;let o=t.source.raw,n=t.source.value;if(!/^(\.?\.\/)/.test(n))return;let a=j(x(i),n);if(w(a,s))return;let l=R(a,r);l&&e.report({node:t,messageId:"shouldUseAlias",data:{alias:l},fix(f){let m=O(a,l,r.get(l)),g=o.replace(n,m);return f.replaceText(t.source,g)}})},ImportDeclaration(t){if(typeof t.source.value!="string")return;let o=t.source.value;if(!/^(\.?\.\/)/.test(o))return;let n=j(x(i),o),a=w(n,s),p=R(n,r);a||p&&e.report({node:t,messageId:"shouldUseAlias",data:{alias:p},fix(l){let f=t.source.raw,m=O(n,p,r.get(p)),g=f.replace(o,m);return l.replaceText(t.source,g)}})}}:{}}};function R(e,s){return Array.from(s.keys()).find(i=>s.get(i).some(t=>e.indexOf(t)===0))}function w(e,s){if(!s)return!1;let i=I(e);return U(i,s).includes(i)}function O(e,s,i){for(let r of i)if(e.indexOf(r)===0)return e.replace(r,s)}var Q={name:d,version:h,meta:{name:d,version:h},rules:{"no-relative":P}};export{Q as default};
+var d="eslint-plugin-path-alias",h="2.0.0";import{dirname as x,resolve as j,basename as I}from"path";import U from"nanomatch";function y(e){return`https://github/com/msfragala/eslint-plugin-path-alias/blob/master/docs/rules/${e}.md`}import{getTsconfig as k}from"get-tsconfig";import{resolve as c,dirname as u}from"path";import A from"find-pkg";import{readFileSync as E}from"fs";function b(e){if(e.options[0]?.paths)return C(e);let s=e.getFilename?.()??e.filename,i=k(s);if(i?.config?.compilerOptions?.paths)return T(i);let r=A.sync(u(s));if(!r)return;let t=JSON.parse(E(r).toString());if(t?.imports)return M(t,r)}function M(e,s){let i=new Map,r=e.imports??{},t=u(s);return Object.entries(r).forEach(([o,n])=>{if(!n||typeof n!="string")return;let a=c(t,n);i.set(o,[a])}),i}function T(e){let s=new Map,i=e?.config?.compilerOptions?.paths??{},r=u(e.path);return e.config.compilerOptions?.baseUrl&&(r=c(u(e.path),e.config.compilerOptions.baseUrl)),Object.entries(i).forEach(([t,o])=>{t=t.replace(/\/\*$/,""),o=o.map(n=>c(r,n.replace(/\/\*$/,""))),s.set(t,o)}),s}function C(e){let s=new Map,i=e.options[0]?.paths??{};return Object.entries(i).forEach(([r,t])=>{if(!t||typeof t!="string")return;if(t.startsWith("/")){s.set(r,[t]);return}let o=e.getCwd?.()??e.cwd,n=c(o,t);s.set(r,[n])}),s}var P={meta:{type:"suggestion",docs:{description:"Ensure imports use path aliases whenever possible vs. relative paths",url:y("no-relative")},fixable:"code",schema:[{type:"object",properties:{exceptions:{type:"array",items:{type:"string"}},paths:{type:"object"}},additionalProperties:!1}],messages:{shouldUseAlias:"Import should use path alias instead of relative path"}},create(e){let s=e.options[0]?.exceptions,i=e.getFilename?.()??e.filename,r=b(e);return r?.size?{ImportExpression(t){if(t.source.type!=="Literal"||typeof t.source.value!="string")return;let o=t.source.raw,n=t.source.value;if(!/^(\.\.\/)/.test(n))return;let a=j(x(i),n);if(w(a,s))return;let l=R(a,r);l&&e.report({node:t,messageId:"shouldUseAlias",data:{alias:l},fix(f){let m=O(a,l,r.get(l)),g=o.replace(n,m);return f.replaceText(t.source,g)}})},ImportDeclaration(t){if(typeof t.source.value!="string")return;let o=t.source.value;if(!/^(\.\.\/)/.test(o))return;let n=j(x(i),o),a=w(n,s),p=R(n,r);a||p&&e.report({node:t,messageId:"shouldUseAlias",data:{alias:p},fix(l){let f=t.source.raw,m=O(n,p,r.get(p)),g=f.replace(o,m);return l.replaceText(t.source,g)}})}}:{}}};function R(e,s){return Array.from(s.keys()).find(i=>s.get(i).some(t=>e.indexOf(t)===0))}function w(e,s){if(!s)return!1;let i=I(e);return U(i,s).includes(i)}function O(e,s,i){for(let r of i)if(e.indexOf(r)===0)return e.replace(r,s)}var Q={name:d,version:h,meta:{name:d,version:h},rules:{"no-relative":P}};export{Q as default};

File diff suppressed because it is too large Load diff

View file

@ -17,31 +17,30 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import esbuild from "esbuild";
import { readdir } from "fs/promises";
import { createPackage } from "@electron/asar";
import { BuildOptions, Plugin } from "esbuild";
import { existsSync, readdirSync } from "fs";
import { readdir, rm, writeFile } from "fs/promises";
import { join } from "path";
import { BUILD_TIMESTAMP, commonOpts, exists, globPlugins, IS_DEV, IS_REPORTER, IS_STANDALONE, IS_UPDATER_DISABLED, resolvePluginName, VERSION, commonRendererPlugins, watch } from "./common.mjs";
import { addBuild, BUILD_TIMESTAMP, buildOrWatchAll, commonOpts, exists, globPlugins, IS_DEV, IS_REPORTER, IS_STANDALONE, IS_UPDATER_DISABLED, resolvePluginName, VERSION, watch } from "./common.mjs";
const defines = {
IS_STANDALONE,
IS_DEV,
IS_REPORTER,
IS_UPDATER_DISABLED,
IS_WEB: false,
IS_EXTENSION: false,
IS_STANDALONE: String(IS_STANDALONE),
IS_DEV: String(IS_DEV),
IS_REPORTER: String(IS_REPORTER),
IS_UPDATER_DISABLED: String(IS_UPDATER_DISABLED),
IS_WEB: "false",
IS_EXTENSION: "false",
VERSION: JSON.stringify(VERSION),
BUILD_TIMESTAMP
BUILD_TIMESTAMP: String(BUILD_TIMESTAMP)
};
if (defines.IS_STANDALONE === false)
if (defines.IS_STANDALONE === "false")
// If this is a local build (not standalone), optimize
// for the specific platform we're on
defines["process.platform"] = JSON.stringify(process.platform);
/**
* @type {esbuild.BuildOptions}
*/
const nodeCommonOpts = {
...commonOpts,
format: "cjs",
@ -49,15 +48,12 @@ const nodeCommonOpts = {
target: ["esnext"],
external: ["electron", "original-fs", "~pluginNatives", ...commonOpts.external],
define: defines
};
} satisfies BuildOptions;
const sourceMapFooter = s => watch ? "" : `//# sourceMappingURL=vencord://${s}.js.map`;
const sourceMapFooter = (s: string) => watch ? "" : `//# sourceMappingURL=vencord://${s}.js.map`;
const sourcemap = watch ? "inline" : "external";
/**
* @type {import("esbuild").Plugin}
*/
const globNativesPlugin = {
const globNativesPlugin: Plugin = {
name: "glob-natives-plugin",
setup: build => {
const filter = /^~pluginNatives$/;
@ -104,26 +100,26 @@ const globNativesPlugin = {
await Promise.all([
// Discord Desktop main & renderer & preload
esbuild.build({
addBuild({
...nodeCommonOpts,
entryPoints: ["src/main/index.ts"],
outfile: "dist/patcher.js",
outfile: "dist/desktop/patcher.js",
footer: { js: "//# sourceURL=VencordPatcher\n" + sourceMapFooter("patcher") },
sourcemap,
define: {
...defines,
IS_DISCORD_DESKTOP: true,
IS_VESKTOP: false
IS_DISCORD_DESKTOP: "true",
IS_VESKTOP: "false"
},
plugins: [
...nodeCommonOpts.plugins,
globNativesPlugin
]
}),
esbuild.build({
addBuild({
...commonOpts,
entryPoints: ["src/Vencord.ts"],
outfile: "dist/renderer.js",
outfile: "dist/desktop/renderer.js",
format: "iife",
target: ["esnext"],
footer: { js: "//# sourceURL=VencordRenderer\n" + sourceMapFooter("renderer") },
@ -131,79 +127,111 @@ await Promise.all([
sourcemap,
plugins: [
globPlugins("discordDesktop"),
...commonRendererPlugins
...commonOpts.plugins
],
define: {
...defines,
IS_DISCORD_DESKTOP: true,
IS_VESKTOP: false
IS_DISCORD_DESKTOP: "true",
IS_VESKTOP: "false"
}
}),
esbuild.build({
addBuild({
...nodeCommonOpts,
entryPoints: ["src/preload.ts"],
outfile: "dist/preload.js",
outfile: "dist/desktop/preload.js",
footer: { js: "//# sourceURL=VencordPreload\n" + sourceMapFooter("preload") },
sourcemap,
define: {
...defines,
IS_DISCORD_DESKTOP: true,
IS_VESKTOP: false
IS_DISCORD_DESKTOP: "true",
IS_VESKTOP: "false"
}
}),
// Vencord Desktop main & renderer & preload
esbuild.build({
addBuild({
...nodeCommonOpts,
entryPoints: ["src/main/index.ts"],
outfile: "dist/vencordDesktopMain.js",
footer: { js: "//# sourceURL=VencordDesktopMain\n" + sourceMapFooter("vencordDesktopMain") },
outfile: "dist/vesktop/main.js",
footer: { js: "//# sourceURL=VencordMain\n" + sourceMapFooter("main") },
sourcemap,
define: {
...defines,
IS_DISCORD_DESKTOP: false,
IS_VESKTOP: true
IS_DISCORD_DESKTOP: "false",
IS_VESKTOP: "true"
},
plugins: [
...nodeCommonOpts.plugins,
globNativesPlugin
]
}),
esbuild.build({
addBuild({
...commonOpts,
entryPoints: ["src/Vencord.ts"],
outfile: "dist/vencordDesktopRenderer.js",
outfile: "dist/vesktop/renderer.js",
format: "iife",
target: ["esnext"],
footer: { js: "//# sourceURL=VencordDesktopRenderer\n" + sourceMapFooter("vencordDesktopRenderer") },
footer: { js: "//# sourceURL=VencordRenderer\n" + sourceMapFooter("renderer") },
globalName: "Vencord",
sourcemap,
plugins: [
globPlugins("vencordDesktop"),
...commonRendererPlugins
...commonOpts.plugins
],
define: {
...defines,
IS_DISCORD_DESKTOP: false,
IS_VESKTOP: true
IS_DISCORD_DESKTOP: "false",
IS_VESKTOP: "true"
}
}),
esbuild.build({
addBuild({
...nodeCommonOpts,
entryPoints: ["src/preload.ts"],
outfile: "dist/vencordDesktopPreload.js",
footer: { js: "//# sourceURL=VencordPreload\n" + sourceMapFooter("vencordDesktopPreload") },
outfile: "dist/vesktop/preload.js",
footer: { js: "//# sourceURL=VencordPreload\n" + sourceMapFooter("preload") },
sourcemap,
define: {
...defines,
IS_DISCORD_DESKTOP: false,
IS_VESKTOP: true
IS_DISCORD_DESKTOP: "false",
IS_VESKTOP: "true"
}
}),
]).catch(err => {
console.error("Build failed");
console.error(err.message);
// make ci fail
if (!commonOpts.watch)
process.exitCode = 1;
});
]);
await buildOrWatchAll();
await Promise.all([
writeFile("dist/desktop/package.json", JSON.stringify({
name: "vencord",
main: "patcher.js"
})),
writeFile("dist/vesktop/package.json", JSON.stringify({
name: "vencord",
main: "main.js"
}))
]);
await Promise.all([
createPackage("dist/desktop", "dist/desktop.asar"),
createPackage("dist/vesktop", "dist/vesktop.asar")
]);
if (existsSync("dist/renderer.js")) {
console.warn("Legacy dist folder. Cleaning up and adding shims.");
await Promise.all(
readdirSync("dist")
.filter(f =>
f.endsWith(".map") ||
f.endsWith(".LEGAL.txt") ||
["patcher", "preload", "renderer"].some(name => f.startsWith(name))
)
.map(file => rm(join("dist", file)))
);
await Promise.all([
writeFile("dist/patcher.js", 'require("./desktop")'),
writeFile("dist/vencordDesktopMain.js", 'require("./vesktop")')
]);
}

View file

@ -23,12 +23,12 @@ import { appendFile, mkdir, readdir, readFile, rm, writeFile } from "fs/promises
import { join } from "path";
import Zip from "zip-local";
import { BUILD_TIMESTAMP, commonOpts, globPlugins, IS_DEV, IS_REPORTER, VERSION, commonRendererPlugins } from "./common.mjs";
import { addBuild, BUILD_TIMESTAMP, buildOrWatchAll, commonOpts, globPlugins, IS_DEV, IS_REPORTER, VERSION } from "./common.mjs";
/**
* @type {esbuild.BuildOptions}
*/
const commonOptions = {
const commonOptions: esbuild.BuildOptions = {
...commonOpts,
entryPoints: ["browser/Vencord.ts"],
globalName: "Vencord",
@ -36,20 +36,20 @@ const commonOptions = {
external: ["~plugins", "~git-hash", "/assets/*"],
plugins: [
globPlugins("web"),
...commonRendererPlugins
...commonOpts.plugins,
],
target: ["esnext"],
define: {
IS_WEB: true,
IS_EXTENSION: false,
IS_STANDALONE: true,
IS_DEV,
IS_REPORTER,
IS_DISCORD_DESKTOP: false,
IS_VESKTOP: false,
IS_UPDATER_DISABLED: true,
IS_WEB: "true",
IS_EXTENSION: "false",
IS_STANDALONE: "true",
IS_DEV: String(IS_DEV),
IS_REPORTER: String(IS_REPORTER),
IS_DISCORD_DESKTOP: "false",
IS_VESKTOP: "false",
IS_UPDATER_DISABLED: "true",
VERSION: JSON.stringify(VERSION),
BUILD_TIMESTAMP
BUILD_TIMESTAMP: String(BUILD_TIMESTAMP)
}
};
@ -67,39 +67,39 @@ const RnNoiseFiles = [
await Promise.all(
[
esbuild.build({
addBuild({
entryPoints: MonacoWorkerEntryPoints.map(entry => `node_modules/monaco-editor/esm/${entry}`),
bundle: true,
minify: true,
format: "iife",
outbase: "node_modules/monaco-editor/esm/",
outdir: "dist/monaco"
outdir: "dist/browser/monaco"
}),
esbuild.build({
addBuild({
entryPoints: ["browser/monaco.ts"],
bundle: true,
minify: true,
format: "iife",
outfile: "dist/monaco/index.js",
outfile: "dist/browser/monaco/index.js",
loader: {
".ttf": "file"
}
}),
esbuild.build({
addBuild({
...commonOptions,
outfile: "dist/browser.js",
outfile: "dist/browser/browser.js",
footer: { js: "//# sourceURL=VencordWeb" }
}),
esbuild.build({
addBuild({
...commonOptions,
outfile: "dist/extension.js",
outfile: "dist/browser/extension.js",
define: {
...commonOptions?.define,
IS_EXTENSION: true,
IS_EXTENSION: "true",
},
footer: { js: "//# sourceURL=VencordWeb" }
}),
esbuild.build({
addBuild({
...commonOptions,
inject: ["browser/GMPolyfill.js", ...(commonOptions?.inject || [])],
define: {
@ -116,18 +116,12 @@ await Promise.all(
}
})
]
).catch(err => {
console.error("Build failed");
console.error(err.message);
if (!commonOpts.watch)
process.exit(1);
});;
);
/**
* @type {(dir: string) => Promise<string[]>}
*/
async function globDir(dir) {
const files = [];
await buildOrWatchAll();
async function globDir(dir: string): Promise<string[]> {
const files = [] as string[];
for (const child of await readdir(dir, { withFileTypes: true })) {
const p = join(dir, child.name);
@ -140,27 +134,23 @@ async function globDir(dir) {
return files;
}
/**
* @type {(dir: string, basePath?: string) => Promise<Record<string, string>>}
*/
async function loadDir(dir, basePath = "") {
async function loadDir(dir: string, basePath = "") {
const files = await globDir(dir);
return Object.fromEntries(await Promise.all(files.map(async f => [f.slice(basePath.length), await readFile(f)])));
return Object.fromEntries(await Promise.all(files.map(async f =>
[f.slice(basePath.length), await readFile(f)] as const
)));
}
/**
* @type {(target: string, files: string[]) => Promise<void>}
*/
async function buildExtension(target, files) {
const entries = {
"dist/Vencord.js": await readFile("dist/extension.js"),
"dist/Vencord.css": await readFile("dist/extension.css"),
...await loadDir("dist/monaco"),
async function buildExtension(target: string, files: string[]): Promise<void> {
const entries: Record<string, Buffer> = {
"dist/Vencord.js": await readFile("dist/browser/extension.js"),
"dist/Vencord.css": await readFile("dist/browser/extension.css"),
...await loadDir("dist/browser/monaco"),
...Object.fromEntries(await Promise.all(RnNoiseFiles.map(async file =>
[`third-party/rnnoise/${file.replace(/^dist\//, "")}`, await readFile(`node_modules/@sapphi-red/web-noise-suppressor/${file}`)]
[`third-party/rnnoise/${file.replace(/^dist\//, "")}`, await readFile(`node_modules/@sapphi-red/web-noise-suppressor/${file}`)] as const
))),
...Object.fromEntries(await Promise.all(files.map(async f => {
let content = await readFile(join("browser", f));
let content: Uint8Array | Buffer = await readFile(join("browser", f));
if (f.startsWith("manifest")) {
const json = JSON.parse(content.toString("utf-8"));
json.version = VERSION;
@ -170,19 +160,19 @@ async function buildExtension(target, files) {
return [
f.startsWith("manifest") ? "manifest.json" : f,
content
];
] as const;
})))
};
await rm(target, { recursive: true, force: true });
await Promise.all(Object.entries(entries).map(async ([file, content]) => {
const dest = join("dist", target, file);
const dest = join("dist/browser", target, file);
const parentDirectory = join(dest, "..");
await mkdir(parentDirectory, { recursive: true });
await writeFile(dest, content);
}));
console.info("Unpacked Extension written to dist/" + target);
console.info("Unpacked Extension written to dist/browser/" + target);
}
const appendCssRuntime = readFile("dist/Vencord.user.css", "utf-8").then(content => {
@ -205,12 +195,14 @@ if (!process.argv.includes("--skip-extension")) {
buildExtension("firefox-unpacked", ["background.js", "content.js", "manifestv2.json", "icon.png"]),
]);
Zip.sync.zip("dist/chromium-unpacked").compress().save("dist/extension-chrome.zip");
console.info("Packed Chromium Extension written to dist/extension-chrome.zip");
Zip.sync.zip("dist/firefox-unpacked").compress().save("dist/extension-firefox.zip");
console.info("Packed Firefox Extension written to dist/extension-firefox.zip");
Zip.zip("dist/browser/chromium-unpacked", (_err, zip) => {
zip.compress().save("dist/extension-chrome.zip");
console.info("Packed Chromium Extension written to dist/extension-chrome.zip");
});
Zip.zip("dist/browser/firefox-unpacked", (_err, zip) => {
zip.compress().save("dist/extension-firefox.zip");
console.info("Packed Firefox Extension written to dist/extension-firefox.zip");
});
} else {
await appendCssRuntime;
}

View file

@ -20,7 +20,7 @@ import "../suppressExperimentalWarnings.js";
import "../checkNodeVersion.js";
import { exec, execSync } from "child_process";
import esbuild from "esbuild";
import esbuild, { build, BuildOptions, context, Plugin } from "esbuild";
import { constants as FsConstants, readFileSync } from "fs";
import { access, readdir, readFile } from "fs/promises";
import { minify as minifyHtml } from "html-minifier-terser";
@ -28,10 +28,8 @@ import { join, relative } from "path";
import { promisify } from "util";
import { getPluginTarget } from "../utils.mjs";
import { builtinModules } from "module";
/** @type {import("../../package.json")} */
const PackageJSON = JSON.parse(readFileSync("package.json"));
const PackageJSON: typeof import("../../package.json") = JSON.parse(readFileSync("package.json", "utf-8"));
export const VERSION = PackageJSON.version;
// https://reproducible-builds.org/docs/source-date-epoch/
@ -55,11 +53,8 @@ export const banner = {
};
const PluginDefinitionNameMatcher = /definePlugin\(\{\s*(["'])?name\1:\s*(["'`])(.+?)\2/;
/**
* @param {string} base
* @param {import("fs").Dirent} dirent
*/
export async function resolvePluginName(base, dirent) {
export async function resolvePluginName(base: string, dirent: import("fs").Dirent) {
const fullPath = join(base, dirent.name);
const content = dirent.isFile()
? await readFile(fullPath, "utf-8")
@ -80,28 +75,13 @@ export async function resolvePluginName(base, dirent) {
})();
}
export async function exists(path) {
export async function exists(path: string) {
return await access(path, FsConstants.F_OK)
.then(() => true)
.catch(() => false);
}
// https://github.com/evanw/esbuild/issues/619#issuecomment-751995294
/**
* @type {import("esbuild").Plugin}
*/
export const makeAllPackagesExternalPlugin = {
name: "make-all-packages-external",
setup(build) {
const filter = /^[^./]|^\.[^./]|^\.\.[^/]/; // Must not start with "/" or "./" or "../"
build.onResolve({ filter }, args => ({ path: args.path, external: true }));
}
};
/**
* @type {(kind: "web" | "discordDesktop" | "vencordDesktop") => import("esbuild").Plugin}
*/
export const globPlugins = kind => ({
export const globPlugins: (kind: "web" | "discordDesktop" | "vencordDesktop") => Plugin = kind => ({
name: "glob-plugins",
setup: build => {
const filter = /^~plugins$/;
@ -165,10 +145,7 @@ export const globPlugins = kind => ({
}
});
/**
* @type {import("esbuild").Plugin}
*/
export const gitHashPlugin = {
export const gitHashPlugin: Plugin = {
name: "git-hash-plugin",
setup: build => {
const filter = /^~git-hash$/;
@ -181,10 +158,7 @@ export const gitHashPlugin = {
}
};
/**
* @type {import("esbuild").Plugin}
*/
export const gitRemotePlugin = {
export const gitRemotePlugin: Plugin = {
name: "git-remote-plugin",
setup: build => {
const filter = /^~git-remote$/;
@ -206,10 +180,7 @@ export const gitRemotePlugin = {
}
};
/**
* @type {import("esbuild").Plugin}
*/
export const fileUrlPlugin = {
export const fileUrlPlugin: Plugin = {
name: "file-uri-plugin",
setup: build => {
const filter = /^file:\/\/.+$/;
@ -229,7 +200,7 @@ export const fileUrlPlugin = {
const encoding = base64 ? "base64" : "utf-8";
let content;
let content: string;
if (!minify) {
content = await readFile(path, encoding);
if (!noTrim) content = content.trimEnd();
@ -269,10 +240,7 @@ export const fileUrlPlugin = {
};
const styleModule = readFileSync("./scripts/build/module/style.js", "utf-8");
/**
* @type {import("esbuild").Plugin}
*/
export const stylePlugin = {
export const stylePlugin: Plugin = {
name: "style-plugin",
setup: ({ onResolve, onLoad }) => {
onResolve({ filter: /\.css\?managed$/, namespace: "file" }, ({ path, resolveDir }) => ({
@ -293,47 +261,59 @@ export const stylePlugin = {
}
};
/**
* @type {(filter: RegExp, message: string) => import("esbuild").Plugin}
*/
export const banImportPlugin = (filter, message) => ({
name: "ban-imports",
setup: build => {
build.onResolve({ filter }, () => {
return { errors: [{ text: message }] };
});
}
});
let buildsFinished = Promise.resolve();
const buildsFinishedPlugin: Plugin = {
name: "builds-finished-plugin",
setup({ onEnd }) {
if (!watch) return;
let resolve: () => void;
const done = new Promise<void>(r => resolve = r);
buildsFinished = buildsFinished.then(() => done);
onEnd(() => resolve());
},
};
/**
* @type {import("esbuild").BuildOptions}
*/
export const commonOpts = {
logLevel: "info",
bundle: true,
watch,
minify: !watch,
sourcemap: watch ? "inline" : "",
sourcemap: watch ? "inline" : "external",
legalComments: "linked",
banner,
plugins: [fileUrlPlugin, gitHashPlugin, gitRemotePlugin, stylePlugin],
plugins: [fileUrlPlugin, gitHashPlugin, gitRemotePlugin, stylePlugin, buildsFinishedPlugin],
external: ["~plugins", "~git-hash", "~git-remote", "/assets/*"],
inject: ["./scripts/build/inject/react.mjs"],
jsxFactory: "VencordCreateElement",
jsxFragment: "VencordFragment",
// Work around https://github.com/evanw/esbuild/issues/2460
tsconfig: "./scripts/build/tsconfig.esbuild.json"
};
jsx: "transform"
} satisfies BuildOptions;
const escapedBuiltinModules = builtinModules
.map(m => m.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"))
.join("|");
const builtinModuleRegex = new RegExp(`^(node:)?(${escapedBuiltinModules})$`);
export const commonRendererPlugins = [
banImportPlugin(builtinModuleRegex, "Cannot import node inbuilt modules in browser code. You need to use a native.ts file"),
banImportPlugin(/^react$/, "Cannot import from react. React and hooks should be imported from @webpack/common"),
banImportPlugin(/^electron(\/.*)?$/, "Cannot import electron in browser code. You need to use a native.ts file"),
banImportPlugin(/^ts-pattern$/, "Cannot import from ts-pattern. match and P should be imported from @webpack/common"),
...commonOpts.plugins
];
const builds = [] as BuildOptions[];
export function addBuild(options: BuildOptions) {
builds.push(options);
}
export async function buildOrWatchAll() {
if (watch) {
const contexts = await Promise.all(builds.map(context));
await Promise.all(contexts.map(ctx => ctx.watch()));
await buildsFinished;
} else {
try {
await Promise.all(builds.map(build));
} catch (err) {
const reason = err instanceof Error
? err.message
: err;
console.error("Build failed");
console.error(reason);
// make ci fail
process.exitCode = 1;
}
}
}

View file

@ -1,7 +0,0 @@
// Work around https://github.com/evanw/esbuild/issues/2460
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"jsx": "react"
}
}

View file

@ -36,7 +36,7 @@ for (const variable of ["DISCORD_TOKEN", "CHROMIUM_BIN"]) {
const CANARY = process.env.USE_CANARY === "true";
const browser = await pup.launch({
headless: true,
headless: "new",
executablePath: process.env.CHROMIUM_BIN
});
@ -225,7 +225,7 @@ page.on("console", async e => {
plugin,
type,
id,
match: regex.replace(/\(\?:\[A-Za-z_\$\]\[\\w\$\]\*\)/g, "\\i"),
match: regex.replace(/\[A-Za-z_\$\]\[\\w\$\]\*/g, "\\i"),
error: await maybeGetError(e.args()[3])
});

View file

@ -124,6 +124,7 @@ try {
env: {
...process.env,
VENCORD_USER_DATA_DIR: BASE_DIR,
VENCORD_DIRECTORY: join(BASE_DIR, "dist/desktop"),
VENCORD_DEV_INSTALL: "1"
}
});

View file

@ -16,15 +16,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/**
* @param {string} filePath
* @returns {string | null}
*/
export function getPluginTarget(filePath) {
export function getPluginTarget(filePath: string) {
const pathParts = filePath.split(/[/\\]/);
if (/^index\.tsx?$/.test(pathParts.at(-1))) pathParts.pop();
if (/^index\.tsx?$/.test(pathParts.at(-1)!)) pathParts.pop();
const identifier = pathParts.at(-1).replace(/\.tsx?$/, "");
const identifier = pathParts.at(-1)!.replace(/\.tsx?$/, "");
const identiferBits = identifier.split(".");
return identiferBits.length === 1 ? null : identiferBits.at(-1);
}

View file

@ -1,2 +0,0 @@
git remote add upstream https://github.com/Vendicated/Vencord.git
git remote set-url --pull upstream DISABLED

View file

@ -99,8 +99,7 @@ export interface ChatBarButtonProps {
tooltip: string;
onClick: MouseEventHandler<HTMLButtonElement>;
onContextMenu?: MouseEventHandler<HTMLButtonElement>;
onAuxClick?: MouseEventHandler<HTMLButtonElement>;
buttonProps?: Omit<HTMLProps<HTMLButtonElement>, "size" | "onClick" | "onContextMenu" | "onAuxClick">;
buttonProps?: Omit<HTMLProps<HTMLButtonElement>, "size" | "onClick" | "onContextMenu">;
}
export const ChatBarButton = ErrorBoundary.wrap((props: ChatBarButtonProps) => {
return (
@ -116,7 +115,6 @@ export const ChatBarButton = ErrorBoundary.wrap((props: ChatBarButtonProps) => {
innerClassName={`${ButtonWrapperClasses.button} ${ChannelTextAreaClasses?.button}`}
onClick={props.onClick}
onContextMenu={props.onContextMenu}
onAuxClick={props.onAuxClick}
{...props.buttonProps}
>
<div className={ButtonWrapperClasses.buttonWrapper}>

View file

@ -54,5 +54,5 @@ export function sendBotMessage(channelId: string, message: PartialDeep<Message>)
export function findOption<T>(args: Argument[], name: string): T & {} | undefined;
export function findOption<T>(args: Argument[], name: string, fallbackValue: T): T & {};
export function findOption(args: Argument[], name: string, fallbackValue?: any) {
return (args.find(a => a.name === name)?.value ?? fallbackValue) as any;
return (args.find(a => a.name === name)?.value || fallbackValue) as any;
}

View file

@ -16,7 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Logger } from "@utils/Logger";
import { makeCodeblock } from "@utils/text";
import { sendBotMessage } from "./commandHelpers";
@ -47,10 +46,10 @@ export let RequiredMessageOption: Option = ReqPlaceholder;
export const _init = function (cmds: Command[]) {
try {
BUILT_IN = cmds;
OptionalMessageOption = cmds.find(c => (c.untranslatedName || c.displayName) === "shrug")!.options![0];
RequiredMessageOption = cmds.find(c => (c.untranslatedName || c.displayName) === "me")!.options![0];
OptionalMessageOption = cmds.find(c => c.name === "shrug")!.options![0];
RequiredMessageOption = cmds.find(c => c.name === "me")!.options![0];
} catch (e) {
new Logger("CommandsAPI").error("Failed to load CommandsApi", e, " - cmds is", cmds);
console.error("Failed to load CommandsApi");
}
return cmds;
} as never;
@ -110,7 +109,6 @@ function registerSubCommands(cmd: Command, plugin: string) {
const subCmd = {
...cmd,
...o,
options: o.options !== undefined ? o.options : undefined,
type: ApplicationCommandType.CHAT_INPUT,
name: `${cmd.name} ${o.name}`,
id: `${o.name}-${cmd.id}`,
@ -140,8 +138,6 @@ export function registerCommand<C extends Command>(command: C, plugin: string) {
throw new Error(`Command '${command.name}' already exists.`);
command.isVencordCommand = true;
command.untranslatedName ??= command.name;
command.untranslatedDescription ??= command.description;
command.id ??= `-${BUILT_IN.length + 1}`;
command.applicationId ??= "-1"; // BUILT_IN;
command.type ??= ApplicationCommandType.CHAT_INPUT;

View file

@ -93,10 +93,8 @@ export interface Command {
isVencordCommand?: boolean;
name: string;
untranslatedName?: string;
displayName?: string;
description: string;
untranslatedDescription?: string;
displayDescription?: string;
options?: Option[];

View file

@ -90,20 +90,19 @@ export function removeGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallba
* A helper function for finding the children array of a group nested inside a context menu based on the id(s) of its children
* @param id The id of the child. If an array is specified, all ids will be tried
* @param children The context menu children
* @param matchSubstring Whether to check if the id is a substring of the child id
*/
export function findGroupChildrenByChildId(id: string | string[], children: Array<ReactElement | null | undefined>, matchSubstring = false): Array<ReactElement | null | undefined> | null {
export function findGroupChildrenByChildId(id: string | string[], children: Array<ReactElement | null>): Array<ReactElement | null> | null {
for (const child of children) {
if (child == null) continue;
if (Array.isArray(child)) {
const found = findGroupChildrenByChildId(id, child, matchSubstring);
const found = findGroupChildrenByChildId(id, child);
if (found !== null) return found;
}
if (
(Array.isArray(id) && id.some(id => matchSubstring ? child.props?.id?.includes(id) : child.props?.id === id))
|| (matchSubstring ? child.props?.id?.includes(id) : child.props?.id === id)
(Array.isArray(id) && id.some(id => child.props?.id === id))
|| child.props?.id === id
) return children;
let nextChildren = child.props?.children;
@ -113,7 +112,7 @@ export function findGroupChildrenByChildId(id: string | string[], children: Arra
child.props.children = nextChildren;
}
const found = findGroupChildrenByChildId(id, nextChildren, matchSubstring);
const found = findGroupChildrenByChildId(id, nextChildren);
if (found !== null) return found;
}
}

View file

@ -16,17 +16,16 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import ErrorBoundary from "@components/ErrorBoundary";
import { Logger } from "@utils/Logger";
import { Channel, Message } from "discord-types/general";
import type { ComponentType, MouseEventHandler } from "react";
import type { MouseEventHandler } from "react";
const logger = new Logger("MessagePopover");
export interface ButtonItem {
key?: string,
label: string,
icon: ComponentType<any>,
icon: React.ComponentType<any>,
message: Message,
channel: Channel,
onClick?: MouseEventHandler<HTMLButtonElement>,
@ -49,26 +48,22 @@ export function removeButton(identifier: string) {
}
export function _buildPopoverElements(
Component: React.ComponentType<ButtonItem>,
message: Message
msg: Message,
makeButton: (item: ButtonItem) => React.ComponentType
) {
const items: React.ReactNode[] = [];
const items = [] as React.ComponentType[];
for (const [identifier, getItem] of buttons.entries()) {
try {
const item = getItem(message);
const item = getItem(msg);
if (item) {
item.key ??= identifier;
items.push(
<ErrorBoundary noop>
<Component {...item} />
</ErrorBoundary>
);
items.push(makeButton(item));
}
} catch (err) {
logger.error(`[${identifier}]`, err);
}
}
return <>{items}</>;
return items;
}

View file

@ -230,10 +230,6 @@ export function definePluginSettings<
if (!definedSettings.pluginName) throw new Error("Cannot access settings before plugin is initialized");
return Settings.plugins[definedSettings.pluginName] as any;
},
get plain() {
if (!definedSettings.pluginName) throw new Error("Cannot access settings before plugin is initialized");
return PlainSettings.plugins[definedSettings.pluginName] as any;
},
use: settings => useSettings(
settings?.map(name => `plugins.${definedSettings.pluginName}.${name}`) as UseSettings<Settings>[]
).plugins[definedSettings.pluginName] as any,

View file

@ -0,0 +1,12 @@
.vc-expandableheader-center-flex {
display: flex;
justify-items: center;
align-items: center;
}
.vc-expandableheader-btn {
all: unset;
cursor: pointer;
width: 24px;
height: 24px;
}

View file

@ -0,0 +1,121 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "./ExpandableHeader.css";
import { classNameFactory } from "@api/Styles";
import { Text, Tooltip, useState } from "@webpack/common";
const cl = classNameFactory("vc-expandableheader-");
export interface ExpandableHeaderProps {
onMoreClick?: () => void;
moreTooltipText?: string;
onDropDownClick?: (state: boolean) => void;
defaultState?: boolean;
headerText: string;
children: React.ReactNode;
buttons?: React.ReactNode[];
forceOpen?: boolean;
}
export function ExpandableHeader({
children,
onMoreClick,
buttons,
moreTooltipText,
onDropDownClick,
headerText,
defaultState = false,
forceOpen = false,
}: ExpandableHeaderProps) {
const [showContent, setShowContent] = useState(defaultState || forceOpen);
return (
<>
<div style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "8px"
}}>
<Text
tag="h2"
variant="eyebrow"
style={{
color: "var(--header-primary)",
display: "inline"
}}
>
{headerText}
</Text>
<div className={cl("center-flex")}>
{
buttons ?? null
}
{
onMoreClick && // only show more button if callback is provided
<Tooltip text={moreTooltipText}>
{tooltipProps => (
<button
{...tooltipProps}
className={cl("btn")}
onClick={onMoreClick}>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
>
<path fill="var(--text-normal)" d="M7 12.001C7 10.8964 6.10457 10.001 5 10.001C3.89543 10.001 3 10.8964 3 12.001C3 13.1055 3.89543 14.001 5 14.001C6.10457 14.001 7 13.1055 7 12.001ZM14 12.001C14 10.8964 13.1046 10.001 12 10.001C10.8954 10.001 10 10.8964 10 12.001C10 13.1055 10.8954 14.001 12 14.001C13.1046 14.001 14 13.1055 14 12.001ZM19 10.001C20.1046 10.001 21 10.8964 21 12.001C21 13.1055 20.1046 14.001 19 14.001C17.8954 14.001 17 13.1055 17 12.001C17 10.8964 17.8954 10.001 19 10.001Z" />
</svg>
</button>
)}
</Tooltip>
}
<Tooltip text={showContent ? "Hide " + headerText : "Show " + headerText}>
{tooltipProps => (
<button
{...tooltipProps}
className={cl("btn")}
onClick={() => {
setShowContent(v => !v);
onDropDownClick?.(showContent);
}}
disabled={forceOpen}
>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
transform={showContent ? "scale(1 -1)" : "scale(1 1)"}
>
<path fill="var(--text-normal)" d="M16.59 8.59003L12 13.17L7.41 8.59003L6 10L12 16L18 10L16.59 8.59003Z" />
</svg>
</button>
)}
</Tooltip>
</div>
</div>
{showContent && children}
</>
);
}

View file

@ -18,8 +18,9 @@
import "./iconStyles.css";
import { getIntlMessage } from "@utils/discord";
import { getTheme, Theme } from "@utils/discord";
import { classes } from "@utils/misc";
import { i18n } from "@webpack/common";
import type { PropsWithChildren } from "react";
interface BaseIconProps extends IconProps {
@ -64,7 +65,8 @@ export function LinkIcon({ height = 24, width = 24, className }: IconProps) {
}
/**
* Discord's copy icon, as seen in the user panel popout on the right of the username and in large code blocks
* Discord's copy icon, as seen in the user popout right of the username when clicking
* your own username in the bottom left user panel
*/
export function CopyIcon(props: IconProps) {
return (
@ -74,9 +76,8 @@ export function CopyIcon(props: IconProps) {
viewBox="0 0 24 24"
>
<g fill="currentColor">
<path d="M3 16a1 1 0 0 1-1-1v-5a8 8 0 0 1 8-8h5a1 1 0 0 1 1 1v.5a.5.5 0 0 1-.5.5H10a6 6 0 0 0-6 6v5.5a.5.5 0 0 1-.5.5H3Z" />
<path d="M6 18a4 4 0 0 0 4 4h8a4 4 0 0 0 4-4v-4h-3a5 5 0 0 1-5-5V6h-4a4 4 0 0 0-4 4v8Z" />
<path d="M21.73 12a3 3 0 0 0-.6-.88l-4.25-4.24a3 3 0 0 0-.88-.61V9a3 3 0 0 0 3 3h2.73Z" />
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1z" />
<path d="M15 5H8c-1.1 0-1.99.9-1.99 2L6 21c0 1.1.89 2 1.99 2H19c1.1 0 2-.9 2-2V11l-6-6zM8 21V7h6v5h5v9H8z" />
</g>
</Icon>
);
@ -122,8 +123,8 @@ export function InfoIcon(props: IconProps) {
>
<path
fill="currentColor"
fill-rule="evenodd"
d="M23 12a11 11 0 1 1-22 0 11 11 0 0 1 22 0Zm-9.5-4.75a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0Zm-.77 3.96a1 1 0 1 0-1.96-.42l-1.04 4.86a2.77 2.77 0 0 0 4.31 2.83l.24-.17a1 1 0 1 0-1.16-1.62l-.24.17a.77.77 0 0 1-1.2-.79l1.05-4.86Z" clip-rule="evenodd"
transform="translate(2 2)"
d="M9,7 L11,7 L11,5 L9,5 L9,7 Z M10,18 C5.59,18 2,14.41 2,10 C2,5.59 5.59,2 10,2 C14.41,2 18,5.59 18,10 C18,14.41 14.41,18 10,18 L10,18 Z M10,4.4408921e-16 C4.4771525,-1.77635684e-15 4.4408921e-16,4.4771525 0,10 C-1.33226763e-15,12.6521649 1.0535684,15.195704 2.92893219,17.0710678 C4.80429597,18.9464316 7.3478351,20 10,20 C12.6521649,20 15.195704,18.9464316 17.0710678,17.0710678 C18.9464316,15.195704 20,12.6521649 20,10 C20,7.3478351 18.9464316,4.80429597 17.0710678,2.92893219 C15.195704,1.0535684 12.6521649,2.22044605e-16 10,0 L10,4.4408921e-16 Z M9,15 L11,15 L11,9 L9,9 L9,15 L9,15 Z"
/>
</Icon>
);
@ -132,7 +133,7 @@ export function InfoIcon(props: IconProps) {
export function OwnerCrownIcon(props: IconProps) {
return (
<Icon
aria-label={getIntlMessage("GUILD_OWNER")}
aria-label={i18n.Messages.GUILD_OWNER}
{...props}
className={classes(props.className, "vc-owner-crown-icon")}
role="img"
@ -211,10 +212,9 @@ export function CogWheel(props: IconProps) {
viewBox="0 0 24 24"
>
<path
clipRule="evenodd"
fill="currentColor"
fill-rule="evenodd"
d="M10.56 1.1c-.46.05-.7.53-.64.98.18 1.16-.19 2.2-.98 2.53-.8.33-1.79-.15-2.49-1.1-.27-.36-.78-.52-1.14-.24-.77.59-1.45 1.27-2.04 2.04-.28.36-.12.87.24 1.14.96.7 1.43 1.7 1.1 2.49-.33.8-1.37 1.16-2.53.98-.45-.07-.93.18-.99.64a11.1 11.1 0 0 0 0 2.88c.06.46.54.7.99.64 1.16-.18 2.2.19 2.53.98.33.8-.14 1.79-1.1 2.49-.36.27-.52.78-.24 1.14.59.77 1.27 1.45 2.04 2.04.36.28.87.12 1.14-.24.7-.95 1.7-1.43 2.49-1.1.8.33 1.16 1.37.98 2.53-.07.45.18.93.64.99a11.1 11.1 0 0 0 2.88 0c.46-.06.7-.54.64-.99-.18-1.16.19-2.2.98-2.53.8-.33 1.79.14 2.49 1.1.27.36.78.52 1.14.24.77-.59 1.45-1.27 2.04-2.04.28-.36.12-.87-.24-1.14-.96-.7-1.43-1.7-1.1-2.49.33-.8 1.37-1.16 2.53-.98.45.07.93-.18.99-.64a11.1 11.1 0 0 0 0-2.88c-.06-.46-.54-.7-.99-.64-1.16.18-2.2-.19-2.53-.98-.33-.8.14-1.79 1.1-2.49.36-.27.52-.78.24-1.14a11.07 11.07 0 0 0-2.04-2.04c-.36-.28-.87-.12-1.14.24-.7.96-1.7 1.43-2.49 1.1-.8-.33-1.16-1.37-.98-2.53.07-.45-.18-.93-.64-.99a11.1 11.1 0 0 0-2.88 0ZM16 12a4 4 0 1 1-8 0 4 4 0 0 1 8 0Z"
clip-rule="evenodd"
d="M19.738 10H22V14H19.739C19.498 14.931 19.1 15.798 18.565 16.564L20 18L18 20L16.565 18.564C15.797 19.099 14.932 19.498 14 19.738V22H10V19.738C9.069 19.498 8.203 19.099 7.436 18.564L6 20L4 18L5.436 16.564C4.901 15.799 4.502 14.932 4.262 14H2V10H4.262C4.502 9.068 4.9 8.202 5.436 7.436L4 6L6 4L7.436 5.436C8.202 4.9 9.068 4.502 10 4.262V2H14V4.261C14.932 4.502 15.797 4.9 16.565 5.435L18 3.999L20 5.999L18.564 7.436C19.099 8.202 19.498 9.069 19.738 10ZM12 16C14.2091 16 16 14.2091 16 12C16 9.79086 14.2091 8 12 8C9.79086 8 8 9.79086 8 12C8 14.2091 9.79086 16 12 16Z"
/>
</Icon>
);
@ -407,30 +407,23 @@ export function PencilIcon(props: IconProps) {
);
}
export function GithubIcon(props: IconProps) {
return (
<Icon
{...props}
viewBox="-3 -3 30 30"
>
<path
fill={props.fill || "currentColor"}
d="M12 0C5.37 0 0 5.37 0 12c0 5.3 3.438 9.8 8.205 11.385.6.11.82-.26.82-.577v-2.17c-3.338.726-4.042-1.61-4.042-1.61-.546-1.387-1.333-1.757-1.333-1.757-1.09-.745.083-.73.083-.73 1.205.084 1.84 1.237 1.84 1.237 1.07 1.835 2.807 1.305 3.492.998.108-.775.42-1.305.763-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.467-2.38 1.235-3.22-.123-.303-.535-1.523.117-3.176 0 0 1.008-.322 3.3 1.23.957-.266 1.98-.398 3-.403 1.02.005 2.043.137 3 .403 2.29-1.552 3.297-1.23 3.297-1.23.653 1.653.24 2.873.118 3.176.77.84 1.233 1.91 1.233 3.22 0 4.61-2.803 5.625-5.475 5.92.43.37.823 1.102.823 2.222v3.293c0 .32.218.694.825.577C20.565 21.797 24 17.298 24 12c0-6.63-5.37-12-12-12z"
/>
</Icon>
);
const WebsiteIconDark = "/assets/e1e96d89e192de1997f73730db26e94f.svg";
const WebsiteIconLight = "/assets/730f58bcfd5a57a5e22460c445a0c6cf.svg";
const GithubIconLight = "/assets/3ff98ad75ac94fa883af5ed62d17c459.svg";
const GithubIconDark = "/assets/6a853b4c87fce386cbfef4a2efbacb09.svg";
export function GithubIcon(props: ImageProps) {
const src = getTheme() === Theme.Light
? GithubIconLight
: GithubIconDark;
return <img {...props} src={src} />;
}
export function WebsiteIcon(props: IconProps) {
return (
<Icon
{...props}
viewBox="0 0 24 24"
>
<path
fill={props.fill || "currentColor"}
d="M12 2C6.486 2 2 6.486 2 12s4.486 10 10 10 10-4.486 10-10S17.514 2 12 2zM4 12c0-.899.156-1.762.431-2.569L6 11l2 2v2l2 2 1 1v1.931C7.061 19.436 4 16.072 4 12zm14.33 4.873C17.677 16.347 16.687 16 16 16v-1a2 2 0 0 0-2-2h-4v-3a2 2 0 0 0 2-2V7h1a2 2 0 0 0 2-2v-.411C17.928 5.778 20 8.65 20 12a7.947 7.947 0 0 1-1.67 4.873z"
/>
</Icon>
);
export function WebsiteIcon(props: ImageProps) {
const src = getTheme() === Theme.Light
? WebsiteIconLight
: WebsiteIconDark;
return <img {...props} src={src} />;
}

View file

@ -6,19 +6,16 @@
import "./LinkIconButton.css";
import { getTheme, Theme } from "@utils/discord";
import { MaskedLink, Tooltip } from "@webpack/common";
import { GithubIcon, WebsiteIcon } from "..";
export function GithubLinkIcon() {
const theme = getTheme() === Theme.Light ? "#000000" : "#FFFFFF";
return <GithubIcon aria-hidden fill={theme} className={"vc-settings-modal-link-icon"} />;
return <GithubIcon aria-hidden className={"vc-settings-modal-link-icon"} />;
}
export function WebsiteLinkIcon() {
const theme = getTheme() === Theme.Light ? "#000000" : "#FFFFFF";
return <WebsiteIcon aria-hidden fill={theme} className={"vc-settings-modal-link-icon"} />;
return <WebsiteIcon aria-hidden className={"vc-settings-modal-link-icon"} />;
}
interface Props {

View file

@ -16,8 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Margins } from "@utils/margins";
import { wordsFromCamel, wordsToTitle } from "@utils/text";
import { OptionType, PluginOptionNumber } from "@utils/types";
import { Forms, React, TextInput } from "@webpack/common";
@ -56,8 +54,7 @@ export function SettingNumericComponent({ option, pluginSettings, definedSetting
return (
<Forms.FormSection>
<Forms.FormTitle>{wordsToTitle(wordsFromCamel(id))}</Forms.FormTitle>
<Forms.FormText className={Margins.bottom20} type="description">{option.description}</Forms.FormText>
<Forms.FormTitle>{option.description}</Forms.FormTitle>
<TextInput
type="number"
pattern="-?[0-9]+"

View file

@ -16,8 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Margins } from "@utils/margins";
import { wordsFromCamel, wordsToTitle } from "@utils/text";
import { PluginOptionSelect } from "@utils/types";
import { Forms, React, Select } from "@webpack/common";
@ -46,8 +44,7 @@ export function SettingSelectComponent({ option, pluginSettings, definedSettings
return (
<Forms.FormSection>
<Forms.FormTitle>{wordsToTitle(wordsFromCamel(id))}</Forms.FormTitle>
<Forms.FormText className={Margins.bottom16} type="description">{option.description}</Forms.FormText>
<Forms.FormTitle>{option.description}</Forms.FormTitle>
<Select
isDisabled={option.disabled?.call(definedSettings) ?? false}
options={option.options}

View file

@ -16,8 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Margins } from "@utils/margins";
import { wordsFromCamel, wordsToTitle } from "@utils/text";
import { PluginOptionSlider } from "@utils/types";
import { Forms, React, Slider } from "@webpack/common";
@ -52,8 +50,7 @@ export function SettingSliderComponent({ option, pluginSettings, definedSettings
return (
<Forms.FormSection>
<Forms.FormTitle>{wordsToTitle(wordsFromCamel(id))}</Forms.FormTitle>
<Forms.FormText className={Margins.bottom20} type="description">{option.description}</Forms.FormText>
<Forms.FormTitle>{option.description}</Forms.FormTitle>
<Slider
disabled={option.disabled?.call(definedSettings) ?? false}
markers={option.markers}

View file

@ -16,8 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Margins } from "@utils/margins";
import { wordsFromCamel, wordsToTitle } from "@utils/text";
import { PluginOptionString } from "@utils/types";
import { Forms, React, TextInput } from "@webpack/common";
@ -43,8 +41,7 @@ export function SettingTextComponent({ option, pluginSettings, definedSettings,
return (
<Forms.FormSection>
<Forms.FormTitle>{wordsToTitle(wordsFromCamel(id))}</Forms.FormTitle>
<Forms.FormText className={Margins.bottom20} type="description">{option.description}</Forms.FormText>
<Forms.FormTitle>{option.description}</Forms.FormTitle>
<TextInput
type="text"
value={state}

View file

@ -93,7 +93,7 @@ interface PluginCardProps extends React.HTMLProps<HTMLDivElement> {
export function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave, isNew }: PluginCardProps) {
const settings = Settings.plugins[plugin.name];
const isEnabled = () => Vencord.Plugins.isPluginEnabled(plugin.name);
const isEnabled = () => settings.enabled ?? false;
function toggleEnabled() {
const wasEnabled = isEnabled();
@ -292,10 +292,10 @@ export default function PluginSettings() {
if (!pluginFilter(p)) continue;
const isRequired = p.required || p.isDependency || depMap[p.name]?.some(d => settings.plugins[d].enabled);
const isRequired = p.required || depMap[p.name]?.some(d => settings.plugins[d].enabled);
if (isRequired) {
const tooltipText = p.required || !depMap[p.name]
const tooltipText = p.required
? "This plugin is required for Vencord to function."
: makeDependencyList(depMap[p.name]?.filter(d => settings.plugins[d].enabled));

View file

@ -247,7 +247,7 @@ function FullPatchInput({ setFind, setParsedFind, setMatch, setReplacement }: Fu
}
try {
const parsed = (0, eval)(`([${fullPatch}][0])`) as Patch;
const parsed = (0, eval)(`(${fullPatch})`) as Patch;
if (!parsed.find) throw new Error("No 'find' field");
if (!parsed.replacement) throw new Error("No 'replacement' field");
@ -382,7 +382,6 @@ function PatchHelper() {
<Forms.FormTitle className={Margins.top20}>Code</Forms.FormTitle>
<CodeBlock lang="js" content={code} />
<Button onClick={() => Clipboard.copy(code)}>Copy to Clipboard</Button>
<Button className={Margins.top8} onClick={() => Clipboard.copy("```ts\n" + code + "\n```")}>Copy as Codeblock</Button>
</>
)}
</SettingsTab>

View file

@ -25,9 +25,10 @@ import { openPluginModal } from "@components/PluginSettings/PluginModal";
import type { UserThemeHeader } from "@main/themes";
import { openInviteModal } from "@utils/discord";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { showItemInFolder } from "@utils/native";
import { useAwaiter } from "@utils/react";
import { findLazy } from "@webpack";
import { findByPropsLazy, findLazy } from "@webpack";
import { Card, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common";
import type { ComponentType, Ref, SyntheticEvent } from "react";
@ -44,7 +45,9 @@ type FileInput = ComponentType<{
filters?: { name?: string; extensions: string[]; }[];
}>;
const InviteActions = findByPropsLazy("resolveInvite");
const FileInput: FileInput = findLazy(m => m.prototype?.activateUploadDialogue && m.prototype.setRef);
const TextAreaProps = findLazy(m => typeof m.textarea === "string");
const cl = classNameFactory("vc-settings-theme-");
@ -77,16 +80,8 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) {
<Forms.FormTitle className={Margins.top20} tag="h5">Validator</Forms.FormTitle>
<Forms.FormText>This section will tell you whether your themes can successfully be loaded</Forms.FormText>
<div>
{themeLinks.map(rawLink => {
const { label, link } = (() => {
const match = /^@(light|dark) (.*)/.exec(rawLink);
if (!match) return { label: rawLink, link: rawLink };
const [, mode, link] = match;
return { label: `[${mode} mode only] ${link}`, link };
})();
return <Card style={{
{themeLinks.map(link => (
<Card style={{
padding: ".5em",
marginBottom: ".5em",
marginTop: ".5em"
@ -94,11 +89,11 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) {
<Forms.FormTitle tag="h5" style={{
overflowWrap: "break-word"
}}>
{label}
{link}
</Forms.FormTitle>
<Validator link={link} />
</Card>;
})}
</Card>
))}
</div>
</>
);
@ -304,7 +299,6 @@ function ThemesTab() {
<Card className="vc-settings-card vc-text-selectable">
<Forms.FormTitle tag="h5">Paste links to css files here</Forms.FormTitle>
<Forms.FormText>One link per line</Forms.FormText>
<Forms.FormText>You can prefix lines with @light or @dark to toggle them based on your Discord theme</Forms.FormText>
<Forms.FormText>Make sure to use direct links to files (raw or github.io)!</Forms.FormText>
</Card>
@ -312,7 +306,7 @@ function ThemesTab() {
<TextArea
value={themeText}
onChange={setThemeText}
className={"vc-settings-theme-links"}
className={classes(TextAreaProps.textarea, "vc-settings-theme-links")}
placeholder="Theme Links"
spellCheck={false}
onBlur={onBlur}

View file

@ -33,20 +33,6 @@
padding: 0.5em;
border: 1px solid var(--background-modifier-accent);
max-height: unset;
background-color: transparent;
box-sizing: border-box;
font-size: 12px;
line-height: 14px;
resize: none;
width: 100%;
}
.vc-settings-theme-links::placeholder {
color: var(--header-secondary);
}
.vc-settings-theme-links:focus {
background-color: var(--background-tertiary);
}
.vc-cloud-settings-sync-grid {

View file

@ -10,6 +10,7 @@ export * from "./CodeBlock";
export * from "./DonateButton";
export { default as ErrorBoundary } from "./ErrorBoundary";
export * from "./ErrorCard";
export * from "./ExpandableHeader";
export * from "./Flex";
export * from "./Heart";
export * from "./Icons";

View file

@ -15,9 +15,9 @@ export async function loadLazyChunks() {
try {
LazyChunkLoaderLogger.log("Loading all chunks...");
const validChunks = new Set<number>();
const invalidChunks = new Set<number>();
const deferredRequires = new Set<number>();
const validChunks = new Set<string>();
const invalidChunks = new Set<string>();
const deferredRequires = new Set<string>();
let chunksSearchingResolve: (value: void | PromiseLike<void>) => void;
const chunksSearchingDone = new Promise<void>(r => chunksSearchingResolve = r);
@ -27,19 +27,16 @@ export async function loadLazyChunks() {
const LazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?(\i\.e\("?[^)]+?"?\)[^\]]*?)(?:\]\))?)\.then\(\i\.bind\(\i,"?([^)]+?)"?\)\)/g);
let foundCssDebuggingLoad = false;
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 lazyChunks = factoryCode.matchAll(LazyChunkRegex);
const validChunkGroups = new Set<[chunkIds: number[], entryPoint: number]>();
const validChunkGroups = new Set<[chunkIds: string[], entryPoint: string]>();
const shouldForceDefer = false;
// Workaround for a chunk that depends on the ChannelMessage component but may be be force loaded before
// the chunk containing the component
const shouldForceDefer = factoryCode.includes(".Messages.GUILD_FEED_UNFEATURE_BUTTON_TEXT");
await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => {
const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Webpack.ChunkIdsRegex)).map(m => Number(m[1])) : [];
const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Webpack.ChunkIdsRegex)).map(m => m[1]) : [];
if (chunkIds.length === 0) {
return;
@ -48,16 +45,6 @@ export async function loadLazyChunks() {
let invalidChunkGroup = false;
for (const id of chunkIds) {
if (hasCssDebuggingLoad) {
if (chunkIds.length > 1) {
throw new Error("Found multiple chunks in factory that loads the CSS debugging chunk");
}
invalidChunks.add(id);
invalidChunkGroup = true;
break;
}
if (wreq.u(id) == null || wreq.u(id) === "undefined.js") continue;
const isWorkerAsset = await fetch(wreq.p + wreq.u(id))
@ -74,7 +61,7 @@ export async function loadLazyChunks() {
}
if (!invalidChunkGroup) {
validChunkGroups.add([chunkIds, Number(entryPoint)]);
validChunkGroups.add([chunkIds, entryPoint]);
}
}));
@ -144,14 +131,14 @@ export async function loadLazyChunks() {
}
// All chunks Discord has mapped to asset files, even if they are not used anymore
const allChunks = [] as number[];
const allChunks = [] as string[];
// Matches "id" or id:
for (const currentMatch of wreq!.u.toString().matchAll(/(?:"([\deE]+?)"(?![,}]))|(?:([\deE]+?):)/g)) {
for (const currentMatch of wreq!.u.toString().matchAll(/(?:"(\d+?)")|(?:(\d+?):)/g)) {
const id = currentMatch[1] ?? currentMatch[2];
if (id == null) continue;
allChunks.push(Number(id));
allChunks.push(id);
}
if (allChunks.length === 0) throw new Error("Failed to get all chunks");

View file

@ -43,11 +43,9 @@ if (IS_VESKTOP || !IS_VANILLA) {
}
switch (url) {
case "renderer.js.map":
case "vencordDesktopRenderer.js.map":
case "preload.js.map":
case "vencordDesktopPreload.js.map":
case "patcher.js.map":
case "vencordDesktopMain.js.map":
case "main.js.map":
cb(join(__dirname, url));
break;
default:

View file

@ -131,7 +131,7 @@ ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => {
autoHideMenuBar: true,
darkTheme: true,
webPreferences: {
preload: join(__dirname, IS_DISCORD_DESKTOP ? "preload.js" : "vencordDesktopPreload.js"),
preload: join(__dirname, "preload.js"),
contextIsolation: true,
nodeIntegration: false,
sandbox: false

View file

@ -26,14 +26,14 @@ import { IS_VANILLA } from "./utils/constants";
console.log("[Vencord] Starting up...");
// FIXME: remove at some point
export const isLegacyNonAsarVencord = IS_STANDALONE && !__dirname.endsWith(".asar");
// Our injector file at app/index.js
const injectorPath = require.main!.filename;
// special discord_arch_electron injection method
const asarName = require.main!.path.endsWith("app.asar") ? "_app.asar" : "app.asar";
// The original app.asar
const asarPath = join(dirname(injectorPath), "..", asarName);
const asarPath = join(dirname(injectorPath), "..", "_app.asar");
const discordPkg = require(join(asarPath, "package.json"));
require.main!.filename = join(asarPath, discordPkg.main);
@ -41,7 +41,7 @@ require.main!.filename = join(asarPath, discordPkg.main);
// @ts-ignore Untyped method? Dies from cringe
app.setAppPath(asarPath);
if (!IS_VANILLA) {
if (!IS_VANILLA && !isLegacyNonAsarVencord) {
const settings = RendererSettings.store;
// Repatch after host updates on Windows
if (process.platform === "win32") {
@ -71,7 +71,7 @@ if (!IS_VANILLA) {
constructor(options: BrowserWindowConstructorOptions) {
if (options?.webPreferences?.preload && options.title) {
const original = options.webPreferences.preload;
options.webPreferences.preload = join(__dirname, IS_DISCORD_DESKTOP ? "preload.js" : "vencordDesktopPreload.js");
options.webPreferences.preload = join(__dirname, "preload.js");
options.webPreferences.sandbox = false;
// work around discord unloading when in background
options.webPreferences.backgroundThrottling = false;
@ -157,5 +157,7 @@ if (!IS_VANILLA) {
console.log("[Vencord] Running in vanilla mode. Not loading Vencord");
}
console.log("[Vencord] Loading original Discord app.asar");
require(require.main!.filename);
if (!isLegacyNonAsarVencord) {
console.log("[Vencord] Loading original Discord app.asar");
require(require.main!.filename);
}

View file

@ -16,12 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export const VENCORD_FILES = [
IS_DISCORD_DESKTOP ? "patcher.js" : "vencordDesktopMain.js",
IS_DISCORD_DESKTOP ? "preload.js" : "vencordDesktopPreload.js",
IS_DISCORD_DESKTOP ? "renderer.js" : "vencordDesktopRenderer.js",
IS_DISCORD_DESKTOP ? "renderer.css" : "vencordDesktopRenderer.css",
];
export const ASAR_FILE = IS_VESKTOP
? "vesktop.asar"
: "desktop.asar";
export function serializeErrors(func: (...args: any[]) => any) {
return async function () {

View file

@ -16,20 +16,27 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { get } from "@main/utils/simpleGet";
import { isLegacyNonAsarVencord } from "@main/patcher";
import { IpcEvents } from "@shared/IpcEvents";
import { VENCORD_USER_AGENT } from "@shared/vencordUserAgent";
import { ipcMain } from "electron";
import { writeFile } from "fs/promises";
import { app, dialog, ipcMain } from "electron";
import {
existsSync as originalExistsSync,
renameSync as originalRenameSync,
writeFileSync as originalWriteFileSync,
} from "original-fs";
import { join } from "path";
import gitHash from "~git-hash";
import gitRemote from "~git-remote";
import { serializeErrors, VENCORD_FILES } from "./common";
import { get } from "../utils/simpleGet";
import { ASAR_FILE, serializeErrors } from "./common";
const API_BASE = `https://api.github.com/repos/${gitRemote}`;
let PendingUpdates = [] as [string, string][];
let PendingUpdate: string | null = null;
let hasUpdateToApplyOnQuit = false;
async function githubGet(endpoint: string) {
return get(API_BASE + endpoint, {
@ -65,22 +72,22 @@ async function fetchUpdates() {
if (hash === gitHash)
return false;
data.assets.forEach(({ name, browser_download_url }) => {
if (VENCORD_FILES.some(s => name.startsWith(s))) {
PendingUpdates.push([name, browser_download_url]);
}
});
const asset = data.assets.find(a => a.name === ASAR_FILE);
PendingUpdate = asset.browser_download_url;
return true;
}
async function applyUpdates() {
await Promise.all(PendingUpdates.map(
async ([name, data]) => writeFile(
join(__dirname, name),
await get(data)
)
));
PendingUpdates = [];
if (!PendingUpdate) return true;
const data = await get(PendingUpdate);
originalWriteFileSync(__dirname + ".new", data);
hasUpdateToApplyOnQuit = true;
PendingUpdate = null;
return true;
}
@ -88,3 +95,51 @@ ipcMain.handle(IpcEvents.GET_REPO, serializeErrors(() => `https://github.com/${g
ipcMain.handle(IpcEvents.GET_UPDATES, serializeErrors(calculateGitChanges));
ipcMain.handle(IpcEvents.UPDATE, serializeErrors(fetchUpdates));
ipcMain.handle(IpcEvents.BUILD, serializeErrors(applyUpdates));
async function migrateLegacyToAsar() {
try {
const isFlatpak = process.platform === "linux" && !!process.env.FLATPAK_ID;
if (isFlatpak) throw "Flatpak Discord can't automatically be migrated.";
const data = await get(`https://github.com/${gitRemote}/releases/latest/download/desktop.asar`);
originalWriteFileSync(join(__dirname, "../vencord.asar"), data);
originalWriteFileSync(__filename, '// Legacy shim for new asar\n\nrequire("../vencord.asar");');
app.relaunch();
app.exit();
} catch (e) {
console.error("Failed to migrate to asar", e);
app.whenReady().then(() => {
dialog.showErrorBox(
"Legacy Install",
"The way Vencord loaded was changed and the updater failed to migrate. Please reinstall using the Vencord Installer!"
);
app.exit(1);
});
}
}
function applyPreviousUpdate() {
originalRenameSync(__dirname + ".new", __dirname);
app.relaunch();
app.exit();
}
app.on("will-quit", () => {
if (hasUpdateToApplyOnQuit)
originalRenameSync(__dirname + ".new", __dirname);
});
if (isLegacyNonAsarVencord) {
console.warn("This is a legacy non asar install! Migrating to asar and restarting...");
migrateLegacyToAsar();
}
if (originalExistsSync(__dirname + ".new")) {
console.warn("Found previous not applied update, applying now and restarting...");
applyPreviousUpdate();
}

View file

@ -35,8 +35,7 @@ export const ALLOWED_PROTOCOLS = [
"steam:",
"spotify:",
"com.epicgames.launcher:",
"tidal:",
"itunes:",
"tidal:"
];
export const IS_VANILLA = /* @__PURE__ */ process.argv.includes("--vanilla");

1
src/modules.d.ts vendored
View file

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// eslint-disable-next-line spaced-comment
/// <reference types="standalone-electron-types"/>
declare module "~plugins" {

View file

@ -0,0 +1,3 @@
[class*="profileBadges"] {
flex: none;
}

View file

@ -1,5 +0,0 @@
/* the profile popout badge container(s) */
[class*="biteSize_"] [class*="tags_"] [class*="container_"] {
/* Discord has padding set to 2px instead of 1px, which causes the 12th badge to wrap to a new line. */
padding: 0 1px;
}

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "./fixDiscordBadgePadding.css";
import "./fixBadgeOverflow.css";
import { _getBadges, BadgePosition, BadgeUserArgs, ProfileBadge } from "@api/Badges";
import DonateButton from "@components/DonateButton";
@ -62,6 +62,34 @@ export default definePlugin({
authors: [Devs.Megu, Devs.Ven, Devs.TheSun],
required: true,
patches: [
/* Patch the badge list component on user profiles */
{
find: 'id:"premium",',
replacement: [
{
match: /&&(\i)\.push\(\{id:"premium".+?\}\);/,
replace: "$&$1.unshift(...$self.getBadges(arguments[0]));",
},
{
// alt: "", aria-hidden: false, src: originalSrc
match: /alt:" ","aria-hidden":!0,src:(?=(\i)\.src)/,
// ...badge.props, ..., src: badge.image ?? ...
replace: "...$1.props,$& $1.image??"
},
// replace their component with ours if applicable
{
match: /(?<=text:(\i)\.description,spacing:12,.{0,50})children:/,
replace: "children:$1.component ? () => $self.renderBadgeComponent($1) :"
},
// conditionally override their onClick with badge.onClick if it exists
{
match: /href:(\i)\.link/,
replace: "...($1.onClick && { onClick: vcE => $1.onClick(vcE, $1) }),$&"
}
]
},
/* new profiles */
{
find: ".FULL_SIZE]:26",
replacement: {
@ -79,7 +107,7 @@ export default definePlugin({
replace: "...$1.props,$& $1.image??"
},
{
match: /(?<=text:(\i)\.description,.{0,200})children:/,
match: /(?<=text:(\i)\.description,.{0,50})children:/,
replace: "children:$1.component ? $self.renderBadgeComponent({ ...$1 }) :"
},
// conditionally override their onClick with badge.onClick if it exists

View file

@ -1,24 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 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: "DynamicImageModalAPI",
authors: [Devs.sadan, Devs.Nuckyz],
description: "Allows you to omit either width or height when opening an image modal",
patches: [
{
find: "SCALE_DOWN:",
replacement: {
match: /!\(null==(\i)\|\|0===\i\|\|null==(\i)\|\|0===\i\)/,
replace: (_, width, height) => `!((null == ${width} || 0 === ${width}) && (null == ${height} || 0 === ${height}))`
}
}
]
});

View file

@ -31,7 +31,7 @@ export default definePlugin({
match: /let\{[^}]*lostPermissionTooltipText:\i[^}]*\}=(\i),/,
replace: "$&vencordProps=$1,"
}, {
match: /#{intl::GUILD_OWNER}(?=.+?decorators:(\i)\(\)).+?\1=?\(\)=>.+?children:\[/,
match: /\.Messages\.GUILD_OWNER(?=.+?decorators:(\i)\(\)).+?\1=?\(\)=>.+?children:\[/,
replace: "$&...(typeof vencordProps=='undefined'?[]:Vencord.Api.MemberListDecorators.__getDecorators(vencordProps)),"
}
]

View file

@ -25,7 +25,7 @@ export default definePlugin({
authors: [Devs.Cyn],
patches: [
{
find: "#{intl::REMOVE_ATTACHMENT_BODY}",
find: ".Messages.REMOVE_ATTACHMENT_BODY",
replacement: {
match: /(?<=.container\)?,children:)(\[.+?\])/,
replace: "Vencord.Api.MessageAccessories._modifyAccessories($1,this.props)",

View file

@ -27,7 +27,7 @@ export default definePlugin({
{
find: '"Message Username"',
replacement: {
match: /#{intl::GUILD_COMMUNICATION_DISABLED_BOTTOM_SHEET_TITLE}.+?}\),\i(?=\])/,
match: /\.Messages\.GUILD_COMMUNICATION_DISABLED_BOTTOM_SHEET_TITLE.+?}\),\i(?=\])/,
replace: "$&,...Vencord.Api.MessageDecorations.__addDecorationsToMessage(arguments[0])"
}
}

View file

@ -25,7 +25,7 @@ export default definePlugin({
authors: [Devs.Arjix, Devs.hunt, Devs.Ven],
patches: [
{
find: "#{intl::EDIT_TEXTAREA_HELP}",
find: ".Messages.EDIT_TEXTAREA_HELP",
replacement: {
match: /(?<=,channel:\i\}\)\.then\().+?(?=return \i\.content!==this\.props\.message\.content&&\i\((.+?)\))/,
replace: (match, args) => "" +

View file

@ -23,14 +23,16 @@ export default definePlugin({
name: "MessagePopoverAPI",
description: "API to add buttons to message popovers.",
authors: [Devs.KingFish, Devs.Ven, Devs.Nuckyz],
patches: [
{
find: "#{intl::MESSAGE_UTILITIES_A11Y_LABEL}",
replacement: {
match: /(?<=:null),(.{0,40}togglePopout:.+?}\))\]}\):null,(?<=\((\i\.\i),{label:.+?:null,(\i&&!\i)\?\(0,\i\.jsxs?\)\(\i\.Fragment.+?message:(\i).+?)/,
replace: (_, ReactButton, ButtonComponent, showReactButton, message) => "" +
`]}):null,Vencord.Api.MessagePopover._buildPopoverElements(${ButtonComponent},${message}),${showReactButton}?${ReactButton}:null,`
patches: [{
find: "Messages.MESSAGE_UTILITIES_A11Y_LABEL",
replacement: {
// foo && !bar ? createElement(reactionStuffs)... createElement(blah,...makeElement(reply-other))
match: /\i&&!\i\?\(0,\i\.jsxs?\)\(.{0,200}renderEmojiPicker:.{0,500}\?(\i)\(\{key:"reply-other"/,
replace: (m, makeElement) => {
const msg = m.match(/message:(.{1,3}),/)?.[1];
if (!msg) throw new Error("Could not find message variable");
return `...Vencord.Api.MessagePopover._buildPopoverElements(${msg},${makeElement}),${m}`;
}
}
]
}],
});

View file

@ -25,16 +25,16 @@ export default definePlugin({
description: "Api required for plugins that modify the server list",
patches: [
{
find: "#{intl::DISCODO_DISABLED}",
find: "Messages.DISCODO_DISABLED",
replacement: {
match: /(?<=#{intl::DISCODO_DISABLED}.+?return)(\(.{0,75}?tutorialContainer.+?}\))(?=}function)/,
match: /(?<=Messages\.DISCODO_DISABLED.+?return)(\(.{0,75}?tutorialContainer.+?}\))(?=}function)/,
replace: "[$1].concat(Vencord.Api.ServerList.renderAll(Vencord.Api.ServerList.ServerListRenderPosition.Above))"
}
},
{
find: "#{intl::SERVERS}),children",
find: "Messages.SERVERS,children",
replacement: {
match: /(?<=#{intl::SERVERS}\),children:)\i\.map\(\i\)/,
match: /(?<=Messages\.SERVERS,children:).+?default:return null\}\}\)/,
replace: "Vencord.Api.ServerList.renderAll(Vencord.Api.ServerList.ServerListRenderPosition.In).concat($&)"
}
}

View file

@ -48,7 +48,7 @@ export default definePlugin({
},
},
{
find: ".METRICS",
find: ".METRICS,",
replacement: [
{
match: /this\._intervalId=/,
@ -61,13 +61,13 @@ export default definePlugin({
]
},
{
find: ".BetterDiscord||null!=",
find: ".installedLogHooks)",
replacement: {
// Make hasClientMods return false
match: /(?=let \i=window;)/,
replace: "return false;"
// if getDebugLogging() returns false, the hooks don't get installed.
match: "getDebugLogging(){",
replace: "getDebugLogging(){return false;"
}
}
},
],
startAt: StartAt.Init,

View file

@ -25,9 +25,8 @@ import ThemesTab from "@components/VencordSettings/ThemesTab";
import UpdaterTab from "@components/VencordSettings/UpdaterTab";
import VencordTab from "@components/VencordSettings/VencordTab";
import { Devs } from "@utils/constants";
import { getIntlMessage } from "@utils/discord";
import definePlugin, { OptionType } from "@utils/types";
import { React } from "@webpack/common";
import { i18n, React } from "@webpack/common";
import gitHash from "~git-hash";
@ -58,20 +57,20 @@ export default definePlugin({
]
},
{
find: ".SEARCH_NO_RESULTS&&0===",
find: "Messages.ACTIVITY_SETTINGS",
replacement: [
{
match: /(?<=section:(.{0,50})\.DIVIDER\}\))([,;])(?=.{0,200}(\i)\.push.{0,100}label:(\i)\.header)/,
replace: (_, sectionTypes, commaOrSemi, elements, element) => `${commaOrSemi} $self.addSettings(${elements}, ${element}, ${sectionTypes}) ${commaOrSemi}`
},
{
match: /({(?=.+?function (\i).{0,160}(\i)=\i\.useMemo.{0,140}return \i\.useMemo\(\(\)=>\i\(\3).+?function\(\){return )\2(?=})/,
match: /({(?=.+?function (\i).{0,120}(\i)=\i\.useMemo.{0,30}return \i\.useMemo\(\(\)=>\i\(\3).+?function\(\){return )\2(?=})/,
replace: (_, rest, settingsHook) => `${rest}$self.wrapSettingsHook(${settingsHook})`
}
]
},
{
find: "#{intl::USER_SETTINGS_ACTIONS_MENU_LABEL}",
find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",
replacement: {
match: /(?<=function\((\i),\i\)\{)(?=let \i=Object.values\(\i.\i\).*?(\i\.\i)\.open\()/,
replace: "$2.open($1);return;"
@ -149,18 +148,13 @@ export default definePlugin({
if (!header) return;
try {
const names = {
top: getIntlMessage("USER_SETTINGS"),
aboveNitro: getIntlMessage("BILLING_SETTINGS"),
belowNitro: getIntlMessage("APP_SETTINGS"),
aboveActivity: getIntlMessage("ACTIVITY_SETTINGS")
};
return header === names[settingsLocation];
} catch {
return firstChild === "PREMIUM";
}
const names = {
top: i18n.Messages.USER_SETTINGS,
aboveNitro: i18n.Messages.BILLING_SETTINGS,
belowNitro: i18n.Messages.APP_SETTINGS,
aboveActivity: i18n.Messages.ACTIVITY_SETTINGS
};
return header === names[settingsLocation];
},
patchedSettings: new WeakSet(),
@ -203,7 +197,7 @@ export default definePlugin({
},
get electronVersion() {
return VencordNative.native.getVersions().electron || window.legcord?.electron || null;
return VencordNative.native.getVersions().electron || window.armcord?.electron || null;
},
get chromiumVersion() {

View file

@ -77,7 +77,7 @@ async function generateDebugInfoMessage() {
const client = (() => {
if (IS_DISCORD_DESKTOP) return `Discord Desktop v${DiscordNative.app.getVersion()}`;
if (IS_VESKTOP) return `Vesktop v${VesktopNative.app.getVersion()}`;
if ("legcord" in window) return `Legcord v${window.legcord.version}`;
if ("armcord" in window) return `ArmCord v${window.armcord.version}`;
// @ts-expect-error
const name = typeof unsafeWindow !== "undefined" ? "UserScript" : "Web";
@ -142,15 +142,15 @@ export default definePlugin({
required: true,
description: "Helps us provide support to you",
authors: [Devs.Ven],
dependencies: ["UserSettingsAPI", "MessageAccessoriesAPI"],
dependencies: ["CommandsAPI", "UserSettingsAPI", "MessageAccessoriesAPI"],
settings,
patches: [{
find: "#{intl::BEGINNING_DM}",
find: ".BEGINNING_DM.format",
replacement: {
match: /#{intl::BEGINNING_DM},{.+?}\),(?=.{0,300}(\i)\.isMultiUserDM)/,
replace: "$& $self.renderContributorDmWarningCard({ channel: $1 }),"
match: /BEGINNING_DM\.format\(\{.+?\}\),(?=.{0,100}userId:(\i\.getRecipientId\(\)))/,
replace: "$& $self.ContributorDmWarningCard({ userId: $1 }),"
}
}],
@ -235,8 +235,7 @@ export default definePlugin({
}
},
renderContributorDmWarningCard: ErrorBoundary.wrap(({ channel }) => {
const userId = channel.getRecipientId();
ContributorDmWarningCard: ErrorBoundary.wrap(({ userId }) => {
if (!isPluginDev(userId)) return null;
if (RelationshipStore.isFriend(userId) || isPluginDev(UserStore.getCurrentUser()?.id)) return null;

View file

@ -1,7 +0,0 @@
# AccountPanelServerProfile
Right click your account panel in the bottom left to view your profile in the current server
![](https://github.com/user-attachments/assets/3228497d-488f-479c-93d2-a32ccdb08f0f)
![](https://github.com/user-attachments/assets/6fc45363-d95f-4810-812f-2f9fb28b41b5)

View file

@ -1,134 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { getCurrentChannel } from "@utils/discord";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { ContextMenuApi, Menu, useEffect, useRef } from "@webpack/common";
import { User } from "discord-types/general";
interface UserProfileProps {
popoutProps: Record<string, any>;
currentUser: User;
originalPopout: () => React.ReactNode;
}
const UserProfile = findComponentByCodeLazy("UserProfilePopoutWrapper: user cannot be undefined");
const styles = findByPropsLazy("accountProfilePopoutWrapper");
let openAlternatePopout = false;
let accountPanelRef: React.MutableRefObject<Record<PropertyKey, any> | null> = { current: null };
const AccountPanelContextMenu = ErrorBoundary.wrap(() => {
const { prioritizeServerProfile } = settings.use(["prioritizeServerProfile"]);
return (
<Menu.Menu
navId="vc-ap-server-profile"
onClose={ContextMenuApi.closeContextMenu}
>
<Menu.MenuItem
id="vc-ap-view-alternate-popout"
label={prioritizeServerProfile ? "View Account Profile" : "View Server Profile"}
disabled={getCurrentChannel()?.getGuildId() == null}
action={e => {
openAlternatePopout = true;
accountPanelRef.current?.props.onMouseDown();
accountPanelRef.current?.props.onClick(e);
}}
/>
<Menu.MenuCheckboxItem
id="vc-ap-prioritize-server-profile"
label="Prioritize Server Profile"
checked={prioritizeServerProfile}
action={() => settings.store.prioritizeServerProfile = !prioritizeServerProfile}
/>
</Menu.Menu>
);
}, { noop: true });
const settings = definePluginSettings({
prioritizeServerProfile: {
type: OptionType.BOOLEAN,
description: "Prioritize Server Profile when left clicking your account panel",
default: false
}
});
export default definePlugin({
name: "AccountPanelServerProfile",
description: "Right click your account panel in the bottom left to view your profile in the current server",
authors: [Devs.Nuckyz, Devs.relitrix],
settings,
patches: [
{
find: "#{intl::ACCOUNT_SPEAKING_WHILE_MUTED}",
group: true,
replacement: [
{
match: /(?<=\.SIZE_32\)}\);)/,
replace: "$self.useAccountPanelRef();"
},
{
match: /(\.AVATAR,children:.+?renderPopout:(\i)=>){(.+?)}(?=,position)(?<=currentUser:(\i).+?)/,
replace: (_, rest, popoutProps, originalPopout, currentUser) => `${rest}$self.UserProfile({popoutProps:${popoutProps},currentUser:${currentUser},originalPopout:()=>{${originalPopout}}})`
},
{
match: /\.AVATAR,children:.+?(?=renderPopout:)/,
replace: "$&onRequestClose:$self.onPopoutClose,"
},
{
match: /(?<=.avatarWrapper,)/,
replace: "ref:$self.accountPanelRef,onContextMenu:$self.openAccountPanelContextMenu,"
}
]
}
],
get accountPanelRef() {
return accountPanelRef;
},
useAccountPanelRef() {
useEffect(() => () => {
accountPanelRef.current = null;
}, []);
return (accountPanelRef = useRef(null));
},
openAccountPanelContextMenu(event: React.UIEvent) {
ContextMenuApi.openContextMenu(event, AccountPanelContextMenu);
},
onPopoutClose() {
openAlternatePopout = false;
},
UserProfile: ErrorBoundary.wrap(({ popoutProps, currentUser, originalPopout }: UserProfileProps) => {
if (
(settings.store.prioritizeServerProfile && openAlternatePopout) ||
(!settings.store.prioritizeServerProfile && !openAlternatePopout)
) {
return originalPopout();
}
const currentChannel = getCurrentChannel();
if (currentChannel?.getGuildId() == null) {
return originalPopout();
}
return (
<div className={styles.accountProfilePopoutWrapper}>
<UserProfile {...popoutProps} userId={currentUser.id} guildId={currentChannel.getGuildId()} channelId={currentChannel.id} />
</div>
);
}, { noop: true })
});

View file

@ -41,7 +41,7 @@ export default definePlugin({
},
{
// Status emojis
find: "#{intl::GUILD_OWNER}",
find: ".Messages.GUILD_OWNER,",
replacement: {
match: /(?<=\.activityEmoji,.+?animate:)\i/,
replace: "!0"

View file

@ -1,3 +0,0 @@
# Always Expand Roles
Always expands the role list in profile popouts

View file

@ -1,45 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { migratePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
migratePluginSettings("AlwaysExpandRoles", "ShowAllRoles");
export default definePlugin({
name: "AlwaysExpandRoles",
description: "Always expands the role list in profile popouts",
authors: [Devs.surgedevs],
patches: [
{
find: 'action:"EXPAND_ROLES"',
replacement: [
{
match: /(roles:\i(?=.+?(\i)\(!0\)[,;]\i\({action:"EXPAND_ROLES"}\)).+?\[\i,\2\]=\i\.useState\()!1\)/,
replace: (_, rest, setExpandedRoles) => `${rest}!0)`
},
{
// Fix not calculating non-expanded roles because the above patch makes the default "expanded",
// which makes the collapse button never show up and calculation never occur
match: /(?<=useLayoutEffect\(\(\)=>{if\()\i/,
replace: isExpanded => "false"
}
]
}
]
});

View file

@ -51,7 +51,7 @@ export default definePlugin({
{
find: "bitbucket.org",
replacement: {
match: /function \i\(\i\){(?=.{0,30}pathname:\i)/,
match: /function \i\(\i\){(?=.{0,60}\.parse\(\i\))/,
replace: "$&return null;"
},
predicate: () => settings.store.file

View file

@ -71,7 +71,7 @@ export default definePlugin({
description: "Anonymise uploaded file names",
patches: [
{
find: "instantBatchUpload:",
find: "instantBatchUpload:function",
replacement: {
match: /uploadFiles:(\i),/,
replace:
@ -86,9 +86,9 @@ export default definePlugin({
}
},
{
find: "#{intl::ATTACHMENT_UTILITIES_SPOILER}",
find: ".Messages.ATTACHMENT_UTILITIES_SPOILER",
replacement: {
match: /(?<=children:\[)(?=.{10,80}tooltip:.{0,100}#{intl::ATTACHMENT_UTILITIES_SPOILER})/,
match: /(?<=children:\[)(?=.{10,80}tooltip:.{0,100}\i\.\i\.Messages\.ATTACHMENT_UTILITIES_SPOILER)/,
replace: "arguments[0].canEdit!==false?$self.renderIcon(arguments[0]):null,"
},
},

View file

@ -24,7 +24,7 @@ interface ActivityButton {
}
interface Activity {
state?: string;
state: string;
details?: string;
timestamps?: {
start?: number;
@ -52,8 +52,8 @@ const enum ActivityFlag {
export interface TrackData {
name: string;
album?: string;
artist?: string;
album: string;
artist: string;
appleMusicLink?: string;
songLink?: string;
@ -61,8 +61,8 @@ export interface TrackData {
albumArtwork?: string;
artistArtwork?: string;
playerPosition?: number;
duration?: number;
playerPosition: number;
duration: number;
}
const enum AssetImageType {
@ -120,7 +120,7 @@ const settings = definePluginSettings({
stateString: {
type: OptionType.STRING,
description: "Activity state format string",
default: "{artist} · {album}"
default: "{artist}"
},
largeImageType: {
type: OptionType.SELECT,
@ -155,8 +155,8 @@ const settings = definePluginSettings({
function customFormat(formatStr: string, data: TrackData) {
return formatStr
.replaceAll("{name}", data.name)
.replaceAll("{album}", data.album ?? "")
.replaceAll("{artist}", data.artist ?? "");
.replaceAll("{album}", data.album)
.replaceAll("{artist}", data.artist);
}
function getImageAsset(type: AssetImageType, data: TrackData) {
@ -212,16 +212,14 @@ export default definePlugin({
const assets: ActivityAssets = {};
const isRadio = Number.isNaN(trackData.duration) && (trackData.playerPosition === 0);
if (settings.store.largeImageType !== AssetImageType.Disabled) {
assets.large_image = largeImageAsset;
if (!isRadio) assets.large_text = customFormat(settings.store.largeTextString, trackData);
assets.large_text = customFormat(settings.store.largeTextString, trackData);
}
if (settings.store.smallImageType !== AssetImageType.Disabled) {
assets.small_image = smallImageAsset;
if (!isRadio) assets.small_text = customFormat(settings.store.smallTextString, trackData);
assets.small_text = customFormat(settings.store.smallTextString, trackData);
}
const buttons: ActivityButton[] = [];
@ -245,17 +243,17 @@ export default definePlugin({
name: customFormat(settings.store.nameString, trackData),
details: customFormat(settings.store.detailsString, trackData),
state: isRadio ? undefined : customFormat(settings.store.stateString, trackData),
state: customFormat(settings.store.stateString, trackData),
timestamps: (trackData.playerPosition && trackData.duration && settings.store.enableTimestamps) ? {
timestamps: (settings.store.enableTimestamps ? {
start: Date.now() - (trackData.playerPosition * 1000),
end: Date.now() - (trackData.playerPosition * 1000) + (trackData.duration * 1000),
} : undefined,
} : undefined),
assets,
buttons: !isRadio && buttons.length ? buttons.map(v => v.label) : undefined,
metadata: !isRadio && buttons.length ? { button_urls: buttons.map(v => v.url) } : undefined,
buttons: buttons.length ? buttons.map(v => v.label) : undefined,
metadata: { button_urls: buttons.map(v => v.url) || undefined, },
type: settings.store.activityType,
flags: ActivityFlag.INSTANCE,

View file

@ -4,7 +4,6 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { canonicalizeMatch } from "@utils/patches";
import { execFile } from "child_process";
import { promisify } from "util";
@ -12,11 +11,37 @@ import type { TrackData } from ".";
const exec = promisify(execFile);
// function exec(file: string, args: string[] = []) {
// return new Promise<{ code: number | null, stdout: string | null, stderr: string | null; }>((resolve, reject) => {
// const process = spawn(file, args, { stdio: [null, "pipe", "pipe"] });
// let stdout: string | null = null;
// process.stdout.on("data", (chunk: string) => { stdout ??= ""; stdout += chunk; });
// let stderr: string | null = null;
// process.stderr.on("data", (chunk: string) => { stdout ??= ""; stderr += chunk; });
// process.on("exit", code => { resolve({ code, stdout, stderr }); });
// process.on("error", err => reject(err));
// });
// }
async function applescript(cmds: string[]) {
const { stdout } = await exec("osascript", cmds.map(c => ["-e", c]).flat());
return stdout;
}
function makeSearchUrl(type: string, query: string) {
const url = new URL("https://tools.applemediaservices.com/api/apple-media/music/US/search.json");
url.searchParams.set("types", type);
url.searchParams.set("limit", "1");
url.searchParams.set("term", query);
return url;
}
const requestOptions: RequestInit = {
headers: { "user-agent": "Mozilla/5.0 (Windows NT 10.0; rv:125.0) Gecko/20100101 Firefox/125.0" },
};
interface RemoteData {
appleMusicLink?: string,
songLink?: string,
@ -26,24 +51,6 @@ interface RemoteData {
let cachedRemoteData: { id: string, data: RemoteData; } | { id: string, failures: number; } | null = null;
const APPLE_MUSIC_BUNDLE_REGEX = /<script type="module" crossorigin src="([a-zA-Z0-9.\-/]+)"><\/script>/;
const APPLE_MUSIC_TOKEN_REGEX = canonicalizeMatch(/Promise.allSettled\(\i\)\}const \i="([A-Za-z0-9-_]*\.[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*)"/);
let cachedToken: string | undefined = undefined;
const getToken = async () => {
if (cachedToken) return cachedToken;
const html = await fetch("https://music.apple.com/").then(r => r.text());
const bundleUrl = new URL(html.match(APPLE_MUSIC_BUNDLE_REGEX)![1], "https://music.apple.com/");
const bundle = await fetch(bundleUrl).then(r => r.text());
const token = bundle.match(APPLE_MUSIC_TOKEN_REGEX)![1];
cachedToken = token;
return token;
};
async function fetchRemoteData({ id, name, artist, album }: { id: string, name: string, artist: string, album: string; }) {
if (id === cachedRemoteData?.id) {
if ("data" in cachedRemoteData) return cachedRemoteData.data;
@ -51,39 +58,21 @@ async function fetchRemoteData({ id, name, artist, album }: { id: string, name:
}
try {
const dataUrl = new URL("https://amp-api-edge.music.apple.com/v1/catalog/us/search");
dataUrl.searchParams.set("platform", "web");
dataUrl.searchParams.set("l", "en-US");
dataUrl.searchParams.set("limit", "1");
dataUrl.searchParams.set("with", "serverBubbles");
dataUrl.searchParams.set("types", "songs");
dataUrl.searchParams.set("term", `${name} ${artist} ${album}`);
dataUrl.searchParams.set("include[songs]", "artists");
const [songData, artistData] = await Promise.all([
fetch(makeSearchUrl("songs", artist + " " + album + " " + name), requestOptions).then(r => r.json()),
fetch(makeSearchUrl("artists", artist.split(/ *[,&] */)[0]), requestOptions).then(r => r.json())
]);
const token = await getToken();
const appleMusicLink = songData?.songs?.data[0]?.attributes.url;
const songLink = songData?.songs?.data[0]?.id ? `https://song.link/i/${songData?.songs?.data[0]?.id}` : undefined;
const songData = await fetch(dataUrl, {
headers: {
"accept": "*/*",
"accept-language": "en-US,en;q=0.9",
"authorization": `Bearer ${token}`,
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
"origin": "https://music.apple.com",
},
})
.then(r => r.json())
.then(data => data.results.song.data[0]);
const albumArtwork = songData?.songs?.data[0]?.attributes.artwork.url.replace("{w}", "512").replace("{h}", "512");
const artistArtwork = artistData?.artists?.data[0]?.attributes.artwork.url.replace("{w}", "512").replace("{h}", "512");
cachedRemoteData = {
id,
data: {
appleMusicLink: songData.attributes.url,
songLink: `https://song.link/i/${songData.id}`,
albumArtwork: songData.attributes.artwork.url.replace("{w}x{h}", "512x512"),
artistArtwork: songData.relationships.artists.data[0].attributes.artwork.url.replace("{w}x{h}", "512x512"),
}
data: { appleMusicLink, songLink, albumArtwork, artistArtwork }
};
return cachedRemoteData.data;
} catch (e) {
console.error("[AppleMusicRichPresence] Failed to fetch remote data:", e);

View file

@ -73,8 +73,8 @@ export default definePlugin({
},
async start() {
// Legcord comes with its own arRPC implementation, so this plugin just confuses users
if ("legcord" in window) return;
// ArmCord comes with its own arRPC implementation, so this plugin just confuses users
if ("armcord" in window) return;
if (ws) ws.close();
ws = new WebSocket("ws://127.0.0.1:1337"); // try to open WebSocket

View file

@ -0,0 +1,5 @@
# AutomodContext
Allows you to jump to the messages surrounding an automod hit
![Visualization](https://github.com/Vendicated/Vencord/assets/61953774/d13740c8-2062-4553-b975-82fd3d6cc08b)

View file

@ -0,0 +1,73 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { Button, ChannelStore, Text } from "@webpack/common";
const { selectChannel } = findByPropsLazy("selectChannel", "selectVoiceChannel");
function jumpToMessage(channelId: string, messageId: string) {
const guildId = ChannelStore.getChannel(channelId)?.guild_id;
selectChannel({
guildId,
channelId,
messageId,
jumpType: "INSTANT"
});
}
function findChannelId(message: any): string | null {
const { embeds: [embed] } = message;
const channelField = embed.fields.find(({ rawName }) => rawName === "channel_id");
if (!channelField) {
return null;
}
return channelField.rawValue;
}
export default definePlugin({
name: "AutomodContext",
description: "Allows you to jump to the messages surrounding an automod hit.",
authors: [Devs.JohnyTheCarrot],
patches: [
{
find: ".Messages.GUILD_AUTOMOD_REPORT_ISSUES",
replacement: {
match: /\.Messages\.ACTIONS.+?}\)(?=,(\(0.{0,40}\.dot.*?}\)),)/,
replace: (m, dot) => `${m},${dot},$self.renderJumpButton({message:arguments[0].message})`
}
}
],
renderJumpButton: ErrorBoundary.wrap(({ message }: { message: any; }) => {
const channelId = findChannelId(message);
if (!channelId) {
return null;
}
return (
<Button
style={{ padding: "2px 8px" }}
look={Button.Looks.LINK}
size={Button.Sizes.SMALL}
color={Button.Colors.LINK}
onClick={() => jumpToMessage(channelId, message.id)}
>
<Text color="text-link" variant="text-xs/normal">
Jump to Surrounding
</Text>
</Button>
);
}, { noop: true })
});

View file

@ -36,7 +36,7 @@ export default definePlugin({
settings,
patches: [
{
find: "#{intl::BAN_CONFIRM_TITLE}",
find: "BAN_CONFIRM_TITLE.",
replacement: {
match: /src:\i\("?\d+"?\)/g,
replace: "src:$self.source"

View file

@ -1,11 +0,0 @@
# Better Folders
Better Folders offers a variety of options to improve your folder experience
Always show the folder icon, regardless of if the folder is open or not
Only have one folder open at a time
Open folders in a sidebar:
![A folder open in a separate sidebar](https://github.com/user-attachments/assets/432d3146-8091-4bae-9c1e-c19046c72947)

View file

@ -18,10 +18,9 @@
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import { getIntlMessage } from "@utils/discord";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findLazy, findStoreLazy } from "@webpack";
import { FluxDispatcher, useMemo } from "@webpack/common";
import { FluxDispatcher, i18n, useMemo } from "@webpack/common";
import FolderSideBar from "./FolderSideBar";
@ -31,9 +30,9 @@ enum FolderIconDisplay {
MoreThanOneFolderExpanded
}
export const ExpandedGuildFolderStore = findStoreLazy("ExpandedGuildFolderStore");
const SortedGuildStore = findStoreLazy("SortedGuildStore");
const GuildsTree = findLazy(m => m.prototype?.moveNextTo);
const SortedGuildStore = findStoreLazy("SortedGuildStore");
export const ExpandedGuildFolderStore = findStoreLazy("ExpandedGuildFolderStore");
const FolderUtils = findByPropsLazy("move", "toggleGuildFolderExpand");
let lastGuildId = null as string | null;
@ -119,22 +118,22 @@ export default definePlugin({
// If we are rendering the Better Folders sidebar, we filter out guilds that are not in folders and unexpanded folders
{
match: /\[(\i)\]=(\(0,\i\.\i\).{0,40}getGuildsTree\(\).+?}\))(?=,)/,
replace: (_, originalTreeVar, rest) => `[betterFoldersOriginalTree]=${rest},${originalTreeVar}=$self.getGuildTree(!!arguments[0]?.isBetterFolders,betterFoldersOriginalTree,arguments[0]?.betterFoldersExpandedIds)`
replace: (_, originalTreeVar, rest) => `[betterFoldersOriginalTree]=${rest},${originalTreeVar}=$self.getGuildTree(!!arguments[0].isBetterFolders,betterFoldersOriginalTree,arguments[0].betterFoldersExpandedIds)`
},
// If we are rendering the Better Folders sidebar, we filter out everything but the servers and folders from the GuildsBar Guild List children
{
match: /lastTargetNode:\i\[\i\.length-1\].+?}\)\](?=}\))/,
replace: "$&.filter($self.makeGuildsBarGuildListFilter(!!arguments[0]?.isBetterFolders))"
match: /lastTargetNode:\i\[\i\.length-1\].+?Fragment.+?\]}\)\]/,
replace: "$&.filter($self.makeGuildsBarGuildListFilter(!!arguments[0].isBetterFolders))"
},
// If we are rendering the Better Folders sidebar, we filter out everything but the scroller for the guild list from the GuildsBar Tree children
{
match: /unreadMentionsIndicatorBottom,.+?}\)\]/,
replace: "$&.filter($self.makeGuildsBarTreeFilter(!!arguments[0]?.isBetterFolders))"
replace: "$&.filter($self.makeGuildsBarTreeFilter(!!arguments[0].isBetterFolders))"
},
// Export the isBetterFolders variable to the folders component
{
match: /switch\(\i\.type\){case \i\.\i\.FOLDER:.+?folderNode:\i,/,
replace: '$&isBetterFolders:typeof isBetterFolders!=="undefined"?isBetterFolders:false,'
match: /(?<=\.Messages\.SERVERS.+?switch\((\i)\.type\){case \i\.\i\.FOLDER:.+?folderNode:\i,)/,
replace: 'isBetterFolders:typeof isBetterFolders!=="undefined"?isBetterFolders:false,'
}
]
},
@ -159,7 +158,7 @@ export default definePlugin({
]
},
{
find: ".expandedFolderBackground,",
find: ".FOLDER_ITEM_GUILD_ICON_MARGIN);",
predicate: () => settings.store.sidebar,
replacement: [
// We use arguments[0] to access the isBetterFolders variable in this nested folder component (the parent exports all the props so we don't have to patch it)
@ -168,31 +167,31 @@ export default definePlugin({
{
predicate: () => settings.store.keepIcons,
match: /(?<=let{folderNode:\i,setNodeRef:\i,.+?expanded:(\i),.+?;)(?=let)/,
replace: (_, isExpanded) => `${isExpanded}=!!arguments[0]?.isBetterFolders&&${isExpanded};`
replace: (_, isExpanded) => `${isExpanded}=!!arguments[0].isBetterFolders&&${isExpanded};`
},
// Disable expanding and collapsing folders transition in the normal GuildsBar sidebar
{
predicate: () => !settings.store.keepIcons,
match: /(?<=#{intl::SERVER_FOLDER_PLACEHOLDER}.+?useTransition\)\()/,
replace: "$self.shouldShowTransition(arguments[0])&&"
match: /(?<=\.Messages\.SERVER_FOLDER_PLACEHOLDER.+?useTransition\)\()/,
replace: "!!arguments[0].isBetterFolders&&"
},
// If we are rendering the normal GuildsBar sidebar, we avoid rendering guilds from folders that are expanded
{
predicate: () => !settings.store.keepIcons,
match: /expandedFolderBackground,.+?,(?=\i\(\(\i,\i,\i\)=>{let{key.{0,45}ul)(?<=selected:\i,expanded:(\i),.+?)/,
replace: (m, isExpanded) => `${m}$self.shouldRenderContents(arguments[0],${isExpanded})?null:`
replace: (m, isExpanded) => `${m}!arguments[0].isBetterFolders&&${isExpanded}?null:`
},
{
// Decide if we should render the expanded folder background if we are rendering the Better Folders sidebar
predicate: () => settings.store.showFolderIcon !== FolderIconDisplay.Always,
match: /(?<=\.isExpanded\),children:\[)/,
replace: "$self.shouldShowFolderIconAndBackground(!!arguments[0]?.isBetterFolders,arguments[0]?.betterFoldersExpandedIds)&&"
match: /(?<=\.wrapper,children:\[)/,
replace: "$self.shouldShowFolderIconAndBackground(!!arguments[0].isBetterFolders,arguments[0].betterFoldersExpandedIds)&&"
},
{
// Decide if we should render the expanded folder icon if we are rendering the Better Folders sidebar
predicate: () => settings.store.showFolderIcon !== FolderIconDisplay.Always,
match: /(?<=\.expandedFolderBackground.+?}\),)(?=\i,)/,
replace: "!$self.shouldShowFolderIconAndBackground(!!arguments[0]?.isBetterFolders,arguments[0]?.betterFoldersExpandedIds)?null:"
replace: "!$self.shouldShowFolderIconAndBackground(!!arguments[0].isBetterFolders,arguments[0].betterFoldersExpandedIds)?null:"
}
]
},
@ -201,12 +200,12 @@ export default definePlugin({
predicate: () => settings.store.sidebar,
replacement: {
// Render the Better Folders sidebar
match: /(container.{0,50}({className:\i\.guilds,themeOverride:\i})\))/,
replace: "$1,$self.FolderSideBar({...$2})"
match: /(?<=({className:\i\.guilds,themeOverride:\i})\))/,
replace: ",$self.FolderSideBar($1)"
}
},
{
find: "#{intl::DISCODO_DISABLED}",
find: ".Messages.DISCODO_DISABLED",
predicate: () => settings.store.closeAllHomeButton,
replacement: {
// Close all folders when clicking the home button
@ -250,10 +249,6 @@ export default definePlugin({
dispatchingFoldersClose = false;
});
}
},
LOGOUT() {
closeFolders();
}
},
@ -276,29 +271,19 @@ export default definePlugin({
makeGuildsBarGuildListFilter(isBetterFolders: boolean) {
return child => {
if (!isBetterFolders) return true;
try {
return child?.props?.["aria-label"] === getIntlMessage("SERVERS");
} catch (e) {
console.error(e);
if (isBetterFolders) {
return child?.props?.["aria-label"] === i18n.Messages.SERVERS;
}
return true;
};
},
makeGuildsBarTreeFilter(isBetterFolders: boolean) {
return child => {
if (!isBetterFolders) return true;
if (child?.props?.className?.includes("itemsContainer") && child.props.children != null) {
// Filter out everything but the scroller for the guild list
child.props.children = child.props.children.filter(child => child?.props?.onScroll != null);
return true;
if (isBetterFolders) {
return child?.props?.onScroll != null;
}
return false;
return true;
};
},
@ -317,20 +302,7 @@ export default definePlugin({
}
},
shouldShowTransition(props: any) {
// Pending guilds
if (props?.folderNode?.id === 1) return true;
FolderSideBar: guildsBarProps => <FolderSideBar {...guildsBarProps} />,
return !!props?.isBetterFolders;
},
shouldRenderContents(props: any, isExpanded: boolean) {
// Pending guilds
if (props?.folderNode?.id === 1) return false;
return !props?.isBetterFolders && isExpanded;
},
FolderSideBar,
closeFolders,
closeFolders
});

View file

@ -34,9 +34,9 @@ export default definePlugin({
},
},
{
find: "#{intl::GIF}",
find: ".Messages.GIF,",
replacement: {
match: /alt:(\i)=(\i\.\i\.string\(\i\.\i#{intl::GIF}\))(?=,[^}]*\}=(\i))/,
match: /alt:(\i)=(\i\.\i\.Messages\.GIF)(?=,[^}]*\}=(\i))/,
replace:
// rename prop so we can always use default value
"alt_$$:$1=$self.altify($3)||$2",

View file

@ -17,9 +17,13 @@
*/
import { definePluginSettings, Settings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { canonicalizeMatch } from "@utils/patches";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
const UserPopoutSectionCssClasses = findByPropsLazy("section", "lastSection");
const settings = definePluginSettings({
hide: {
@ -63,14 +67,28 @@ export default definePlugin({
}
},
{
find: "#{intl::NOTE_PLACEHOLDER}",
find: "Messages.NOTE_PLACEHOLDER",
replacement: {
match: /#{intl::NOTE_PLACEHOLDER}\),/,
match: /\.NOTE_PLACEHOLDER,/,
replace: "$&spellCheck:!$self.noSpellCheck,"
}
},
{
find: ".popularApplicationCommandIds,",
replacement: {
match: /lastSection:(!?\i)}\),/,
replace: "$&$self.patchPadding({lastSection:$1}),"
}
}
],
patchPadding: ErrorBoundary.wrap(({ lastSection }) => {
if (!lastSection) return null;
return (
<div className={UserPopoutSectionCssClasses.lastSection} ></div>
);
}),
get noSpellCheck() {
return settings.store.noSpellCheck;
}

View file

@ -99,11 +99,7 @@ export default definePlugin({
id="vc-view-role-icon"
label="View Role Icon"
action={() => {
openImageModal({
url: `${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/role-icons/${role.id}/${role.icon}.${settings.store.roleIconFileFormat}`,
height: 128,
width: 128
});
openImageModal(`${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/role-icons/${role.id}/${role.icon}.${settings.store.roleIconFileFormat}`);
}}
icon={ImageIcon}
/>

View file

@ -47,7 +47,7 @@ export default definePlugin({
},
{
find: "#{intl::ADD_ROLE_A11Y_LABEL}",
find: ".ADD_ROLE_A11Y_LABEL",
all: true,
predicate: () => Settings.plugins.BetterRoleDot.copyRoleColorInProfilePopout && !Settings.plugins.BetterRoleDot.bothStyles,
noWarn: true,

View file

@ -60,7 +60,7 @@ export default definePlugin({
patches: [
{
find: "#{intl::AUTH_SESSIONS_SESSION_LOG_OUT}",
find: "Messages.AUTH_SESSIONS_SESSION_LOG_OUT",
replacement: [
// Replace children with a single label with state
{

View file

@ -1,69 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { openPluginModal } from "@components/PluginSettings/PluginModal";
import { getIntlMessage } from "@utils/discord";
import { isObjectEmpty } from "@utils/misc";
import { Alerts, Menu, useMemo, useState } from "@webpack/common";
import Plugins from "~plugins";
function onRestartNeeded() {
Alerts.show({
title: "Restart required",
body: <p>You have changed settings that require a restart.</p>,
confirmText: "Restart now",
cancelText: "Later!",
onConfirm: () => location.reload()
});
}
export default function PluginsSubmenu() {
const sortedPlugins = useMemo(() => Object.values(Plugins)
.sort((a, b) => a.name.localeCompare(b.name)), []);
const [query, setQuery] = useState("");
const search = query.toLowerCase();
const include = (p: typeof Plugins[keyof typeof Plugins]) => (
Vencord.Plugins.isPluginEnabled(p.name)
&& p.options && !isObjectEmpty(p.options)
&& (
p.name.toLowerCase().includes(search)
|| p.description.toLowerCase().includes(search)
|| p.tags?.some(t => t.toLowerCase().includes(search))
)
);
const plugins = sortedPlugins.filter(include);
return (
<>
<Menu.MenuControlItem
id="vc-plugins-search"
control={(props, ref) => (
<Menu.MenuSearchControl
{...props}
query={query}
onChange={setQuery}
ref={ref}
placeholder={getIntlMessage("SEARCH")}
/>
)}
/>
{!!plugins.length && <Menu.MenuSeparator />}
{plugins.map(p => (
<Menu.MenuItem
key={p.name}
id={p.name}
label={p.name}
action={() => openPluginModal(p, onRestartNeeded)}
/>
))}
</>
);
}

View file

@ -7,15 +7,12 @@
import { definePluginSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import { Devs } from "@utils/constants";
import { getIntlMessage } from "@utils/discord";
import { Logger } from "@utils/Logger";
import definePlugin, { OptionType } from "@utils/types";
import { waitFor } from "@webpack";
import { ComponentDispatch, FocusLock, Menu, useEffect, useRef } from "@webpack/common";
import { ComponentDispatch, FocusLock, i18n, Menu, useEffect, useRef } from "@webpack/common";
import type { HTMLAttributes, ReactElement } from "react";
import PluginsSubmenu from "./PluginsSubmenu";
type SettingsEntry = { section: string, label: string; };
const cl = classNameFactory("");
@ -112,7 +109,7 @@ export default definePlugin({
predicate: () => settings.store.disableFade
},
{ // Load menu TOC eagerly
find: "#{intl::USER_SETTINGS_WITH_BUILD_OVERRIDE}",
find: "Messages.USER_SETTINGS_WITH_BUILD_OVERRIDE.format",
replacement: {
match: /(\i)\(this,"handleOpenSettingsContextMenu",.{0,100}?null!=\i&&.{0,100}?(await Promise\.all[^};]*?\)\)).*?,(?=\1\(this)/,
replace: "$&(async ()=>$2)(),"
@ -120,22 +117,14 @@ export default definePlugin({
predicate: () => settings.store.eagerLoad
},
{ // Settings cog context menu
find: "#{intl::USER_SETTINGS_ACTIONS_MENU_LABEL}",
replacement: [
{
match: /(EXPERIMENTS:.+?)(\(0,\i.\i\)\(\))(?=\.filter\(\i=>\{let\{section:\i\}=)/,
replace: "$1$self.wrapMenu($2)"
},
{
match: /case \i\.\i\.DEVELOPER_OPTIONS:return \i;/,
replace: "$&case 'VencordPlugins':return $self.PluginsSubmenu();"
}
]
},
find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",
replacement: {
match: /(EXPERIMENTS:.+?)(\(0,\i.\i\)\(\))(?=\.filter\(\i=>\{let\{section:\i\}=)/,
replace: "$1$self.wrapMenu($2)"
}
}
],
PluginsSubmenu,
// This is the very outer layer of the entire ui, so we can't wrap this in an ErrorBoundary
// without possibly also catching unrelated errors of children.
//
@ -160,7 +149,7 @@ export default definePlugin({
if (item.section === "HEADER") {
items.push({ label: item.label, items: [] });
} else if (item.section === "DIVIDER") {
items.push({ label: getIntlMessage("OTHER_OPTIONS"), items: [] });
items.push({ label: i18n.Messages.OTHER_OPTIONS, items: [] });
} else {
items.at(-1)!.items.push(item);
}

View file

@ -25,9 +25,11 @@ export default definePlugin({
description: "Upload with a single click, open menu with right click",
patches: [
{
find: '"ChannelAttachButton"',
find: "Messages.CHAT_ATTACH_UPLOAD_OR_INVITE",
replacement: {
match: /\.attachButtonInner,"aria-label":.{0,50},onDoubleClick:(.+?:void 0),\.\.\.(\i),/,
// Discord merges multiple props here with Object.assign()
// This patch passes a third object to it with which we override onClick and onContextMenu
match: /CHAT_ATTACH_UPLOAD_OR_INVITE,onDoubleClick:(.+?:void 0),\.\.\.(\i),/,
replace: "$&onClick:$1,onContextMenu:$2.onClick,",
},
},

View file

@ -57,11 +57,7 @@ export const handleViewPreview = async ({ guildId, channelId, ownerId }: Applica
const previewUrl = await ApplicationStreamPreviewStore.getPreviewURL(guildId, channelId, ownerId);
if (!previewUrl) return;
openImageModal({
url: previewUrl,
height: 720,
width: 1280
});
openImageModal(previewUrl);
};
export const addViewStreamContext: NavContextMenuPatchCallback = (children, { userId }: { userId: string | bigint; }) => {

View file

@ -45,8 +45,8 @@ export default definePlugin({
{
find: ".embedWrapper,embed",
replacement: [{
match: /\.container/,
replace: "$&+(this.props.channel.nsfw? ' vc-nsfw-img': '')"
match: /\.embedWrapper(?=.+?channel_id:(\i)\.id)/g,
replace: "$&+($1.nsfw?' vc-nsfw-img':'')"
}]
}
],

View file

@ -75,11 +75,10 @@ export default definePlugin({
patches: [{
find: "renderConnectionStatus(){",
replacement: {
match: /(?<=renderConnectionStatus\(\){.+\.channel,children:).+?}\):\i(?=}\))/,
match: /(?<=renderConnectionStatus\(\)\{.+\.channel,children:)\i/,
replace: "[$&, $self.renderTimer(this.props.channel.id)]"
}
}],
renderTimer(channelId: string) {
return <ErrorBoundary noop>
<this.Timer channelId={channelId} />

View file

@ -155,5 +155,4 @@ export const defaultRules = [
"igshid",
"igsh",
"share_id@reddit.com",
"si@soundcloud.com",
];

View file

@ -12,9 +12,9 @@ import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import definePlugin, { OptionType, StartAt } from "@utils/types";
import { findByCodeLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
import { Button, Forms, ThemeStore, useStateFromStores } from "@webpack/common";
import { Button, Forms, useStateFromStores } from "@webpack/common";
const ColorPicker = findComponentByCodeLazy("#{intl::USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR}", ".BACKGROUND_PRIMARY)");
const ColorPicker = findComponentByCodeLazy(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)");
const colorPresets = [
"#1E1514", "#172019", "#13171B", "#1C1C28", "#402D2D",
@ -30,12 +30,13 @@ function onPickColor(color: number) {
updateColorVars(hexColor);
}
const saveClientTheme = findByCodeLazy('type:"UNSYNCED_USER_SETTINGS_UPDATE', '"system"===');
const saveClientTheme = findByCodeLazy('type:"UNSYNCED_USER_SETTINGS_UPDATE",settings:{useSystemTheme:"system"===');
function setTheme(theme: string) {
saveClientTheme({ theme });
}
const ThemeStore = findStoreLazy("ThemeStore");
const NitroThemeStore = findStoreLazy("ClientThemesBackgroundStore");
function ThemeSettings() {
@ -110,7 +111,7 @@ const settings = definePluginSettings({
export default definePlugin({
name: "ClientTheme",
authors: [Devs.Nuckyz],
authors: [Devs.F53, Devs.Nuckyz],
description: "Recreation of the old client theme experiment. Add a color to your Discord client theme",
settings,

View file

@ -1,5 +1,5 @@
# ConsoleJanitor
Disables annoying console messages/errors. This plugin mainly removes errors/warnings that happen all the time and Discord logger messages.
Disables annoying console messages/errors. This plugin mainly removes errors/warnings that happen all the time and noisy/spammy logging messages.
One of the disabled messages is the "Window state not initialized" warning, for example.
Some of the disabled messages include the "notosans-400-normalitalic" error and MessageActionCreators, Routing/Utils loggers.

View file

@ -6,7 +6,7 @@
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType, StartAt } from "@utils/types";
import definePlugin, { OptionType } from "@utils/types";
const Noop = () => { };
const NoopLogger = {
@ -22,12 +22,10 @@ const NoopLogger = {
fileOnly: Noop
};
const logAllow = new Set();
const settings = definePluginSettings({
disableLoggers: {
disableNoisyLoggers: {
type: OptionType.BOOLEAN,
description: "Disables Discords loggers",
description: "Disable noisy loggers like the MessageActionCreators",
default: false,
restartNeeded: true
},
@ -36,50 +34,18 @@ const settings = definePluginSettings({
description: "Disable the Spotify logger, which leaks account information and access token",
default: true,
restartNeeded: true
},
whitelistedLoggers: {
type: OptionType.STRING,
description: "Semi colon separated list of loggers to allow even if others are hidden",
default: "GatewaySocket; Routing/Utils",
onChange(newVal: string) {
logAllow.clear();
newVal.split(";").map(x => x.trim()).forEach(logAllow.add.bind(logAllow));
}
}
});
export default definePlugin({
name: "ConsoleJanitor",
description: "Disables annoying console messages/errors",
authors: [Devs.Nuckyz, Devs.sadan],
authors: [Devs.Nuckyz],
settings,
startAt: StartAt.Init,
start() {
logAllow.clear();
this.settings.store.whitelistedLoggers?.split(";").map(x => x.trim()).forEach(logAllow.add.bind(logAllow));
},
NoopLogger: () => NoopLogger,
shouldLog(logger: string) {
return logAllow.has(logger);
},
patches: [
{
find: "https://github.com/highlightjs/highlight.js/issues/2277",
replacement: {
match: /(?<=&&\()console.log\(`Deprecated.+?`\),/,
replace: ""
}
},
{
find: 'The "interpolate" function is deprecated in v10 (use "to" instead)',
replacement: {
match: /,console.warn\(\i\+'The "interpolate" function is deprecated in v10 \(use "to" instead\)'\)/,
replace: ""
}
},
{
find: 'console.warn("Window state not initialized"',
replacement: {
@ -94,6 +60,13 @@ export default definePlugin({
replace: ""
}
},
{
find: "notosans-400-normalitalic",
replacement: {
match: /,"notosans-.+?"/g,
replace: ""
}
},
{
find: 'console.warn("[DEPRECATED] Please use `subscribeWithSelector` middleware");',
all: true,
@ -133,17 +106,38 @@ export default definePlugin({
{
find: "Slow dispatch on",
replacement: {
match: /\i\.totalTime>\i&&\i\.verbose\("Slow dispatch on ".+?\)\);/,
match: /\i\.totalTime>100&&\i\.verbose\("Slow dispatch on ".+?\)\);/,
replace: ""
}
},
// Patches discords generic logger function
{
find: "Σ:",
predicate: () => settings.store.disableLoggers,
...[
'("MessageActionCreators")', '("ChannelMessages")',
'("Routing/Utils")', '("RTCControlSocket")',
'("ConnectionEventFramerateReducer")', '("RTCLatencyTestManager")',
'("OverlayBridgeStore")', '("RPCServer:WSS")', '("RPCServer:IPC")'
].map(logger => ({
find: logger,
predicate: () => settings.store.disableNoisyLoggers,
all: true,
replacement: {
match: /(?<=&&)(?=console)/,
replace: "$self.shouldLog(arguments[0])&&"
match: new RegExp(String.raw`new \i\.\i${logger.replace(/([()])/g, "\\$1")}`),
replace: `$self.NoopLogger${logger}`
}
})),
{
find: '"Experimental codecs: "',
predicate: () => settings.store.disableNoisyLoggers,
replacement: {
match: /new \i\.\i\("Connection\("\.concat\(\i,"\)"\)\)/,
replace: "$self.NoopLogger()"
}
},
{
find: '"Handling ping: "',
predicate: () => settings.store.disableNoisyLoggers,
replacement: {
match: /new \i\.\i\("RTCConnection\("\.concat.+?\)\)(?=,)/,
replace: "$self.NoopLogger()"
}
},
{

View file

@ -18,7 +18,6 @@
import { Devs } from "@utils/constants";
import { getCurrentChannel, getCurrentGuild } from "@utils/discord";
import { runtimeHashMessageKey } from "@utils/intlHash";
import { SYM_LAZY_CACHED, SYM_LAZY_GET } from "@utils/lazy";
import { relaunch } from "@utils/native";
import { canonicalizeMatch, canonicalizeReplace, canonicalizeReplacement } from "@utils/patches";
@ -105,7 +104,6 @@ function makeShortcuts() {
canonicalizeMatch,
canonicalizeReplace,
canonicalizeReplacement,
runtimeHashMessageKey,
fakeRender: (component: ComponentType, props: any) => {
const prevWin = fakeRenderWin?.deref();
const win = prevWin?.closed === false

View file

@ -1,5 +0,0 @@
# CopyFileContents
Adds a button to text file attachments to copy their contents.
![](https://github.com/user-attachments/assets/b1a0f6f4-106f-4953-94d9-4c5ef5810bca)

View file

@ -1,60 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./style.css";
import ErrorBoundary from "@components/ErrorBoundary";
import { CopyIcon, NoEntrySignIcon } from "@components/Icons";
import { Devs } from "@utils/constants";
import { copyWithToast } from "@utils/misc";
import definePlugin from "@utils/types";
import { Tooltip, useState } from "@webpack/common";
const CheckMarkIcon = () => {
return <svg width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor" d="M21.7 5.3a1 1 0 0 1 0 1.4l-12 12a1 1 0 0 1-1.4 0l-6-6a1 1 0 1 1 1.4-1.4L9 16.58l11.3-11.3a1 1 0 0 1 1.4 0Z"></path>
</svg>;
};
export default definePlugin({
name: "CopyFileContents",
description: "Adds a button to text file attachments to copy their contents",
authors: [Devs.Obsidian, Devs.Nuckyz],
patches: [
{
find: "#{intl::PREVIEW_BYTES_LEFT}",
replacement: {
match: /\.footerGap.+?url:\i,fileName:\i,fileSize:\i}\),(?<=fileContents:(\i),bytesLeft:(\i).+?)/g,
replace: "$&$self.addCopyButton({fileContents:$1,bytesLeft:$2}),"
}
}
],
addCopyButton: ErrorBoundary.wrap(({ fileContents, bytesLeft }: { fileContents: string, bytesLeft: number; }) => {
const [recentlyCopied, setRecentlyCopied] = useState(false);
return (
<Tooltip text={recentlyCopied ? "Copied!" : bytesLeft > 0 ? "File too large to copy" : "Copy File Contents"}>
{tooltipProps => (
<div
{...tooltipProps}
className="vc-cfc-button"
role="button"
onClick={() => {
if (!recentlyCopied && bytesLeft <= 0) {
copyWithToast(fileContents);
setRecentlyCopied(true);
setTimeout(() => setRecentlyCopied(false), 2000);
}
}}
>
{recentlyCopied ? <CheckMarkIcon /> : bytesLeft > 0 ? <NoEntrySignIcon color="var(--channel-icon)" /> : <CopyIcon />}
</div>
)}
</Tooltip>
);
}, { noop: true }),
});

View file

@ -1,9 +0,0 @@
.vc-cfc-button {
color: var(--interactive-normal);
cursor: pointer;
padding-left: 4px;
}
.vc-cfc-button:hover {
color: var(--interactive-hover);
}

View file

@ -67,7 +67,7 @@ export default definePlugin({
patches: [
{
find: "#{intl::ERRORS_UNEXPECTED_CRASH}",
find: ".Messages.ERRORS_UNEXPECTED_CRASH",
replacement: {
match: /this\.setState\((.+?)\)/,
replace: "$self.handleCrash(this,$1);"
@ -175,7 +175,7 @@ export default definePlugin({
}
if (settings.store.attemptToNavigateToHome) {
try {
NavigationRouter.transitionToGuild("@me");
NavigationRouter.transitionTo("/channels/@me");
} catch (err) {
CrashHandlerLogger.debug("Failed to navigate to home", err);
}

View file

@ -26,11 +26,12 @@ import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { useAwaiter } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy, findComponentByCodeLazy } from "@webpack";
import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { ApplicationAssetUtils, Button, FluxDispatcher, Forms, GuildStore, React, SelectedChannelStore, SelectedGuildStore, UserStore } from "@webpack/common";
const useProfileThemeStyle = findByCodeLazy("profileThemeStyle:", "--profile-gradient-primary-color");
const ActivityComponent = findComponentByCodeLazy("onOpenGameProfile");
const ActivityClassName = findByPropsLazy("activity", "buttonColor");
const ShowCurrentGame = getUserSettingLazy<boolean>("status", "showCurrentGame")!;
@ -435,8 +436,8 @@ export default definePlugin({
<Forms.FormDivider className={Margins.top8} />
<div style={{ width: "284px", ...profileThemeStyle, padding: 8, marginTop: 8, borderRadius: 8, background: "var(--bg-mod-faint)" }}>
{activity[0] && <ActivityComponent activity={activity[0]} channelId={SelectedChannelStore.getChannelId()}
<div style={{ width: "284px", ...profileThemeStyle }}>
{activity[0] && <ActivityComponent activity={activity[0]} className={ActivityClassName.activity} channelId={SelectedChannelStore.getChannelId()}
guild={GuildStore.getGuild(SelectedGuildStore.getLastSelectedGuildId())}
application={{ id: settings.store.appID }}
user={UserStore.getCurrentUser()} />}

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