Compare commits
1 commit
39fa23cfb7
...
1807d60731
Author | SHA1 | Date | |
---|---|---|---|
1807d60731 |
289 changed files with 4358 additions and 12190 deletions
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"root": true,
|
"root": true,
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"ignorePatterns": ["dist", "browser", "packages/vencord-types"],
|
"ignorePatterns": ["dist", "browser"],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"@typescript-eslint",
|
"@typescript-eslint",
|
||||||
"simple-header",
|
"simple-header",
|
||||||
|
|
3
.github/ISSUE_TEMPLATE/blank.yml
vendored
3
.github/ISSUE_TEMPLATE/blank.yml
vendored
|
@ -12,8 +12,7 @@ body:
|
||||||
DO NOT USE THIS FORM, unless
|
DO NOT USE THIS FORM, unless
|
||||||
- you are a vencord contributor
|
- you are a vencord contributor
|
||||||
- you were given explicit permission to use this form by a moderator in our support server
|
- you were given explicit permission to use this form by a moderator in our support server
|
||||||
|
- you are filing a security related report
|
||||||
DO NOT USE THIS FORM FOR SECURITY RELATED ISSUES. [CREATE A SECURITY ADVISORY INSTEAD.](https://github.com/Vendicated/Vencord/security/advisories/new)
|
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: content
|
id: content
|
||||||
|
|
12
.github/workflows/build.yml
vendored
12
.github/workflows/build.yml
vendored
|
@ -18,21 +18,21 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v3 # Install pnpm using packageManager key in package.json
|
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
|
||||||
|
|
||||||
- name: Use Node.js 20
|
- name: Use Node.js 19
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 19
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build web
|
- name: Build web
|
||||||
run: pnpm buildWebStandalone
|
run: pnpm buildWeb --standalone
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: pnpm build --standalone
|
run: pnpm build --standalone
|
||||||
|
|
2
.github/workflows/codeberg-mirror.yml
vendored
2
.github/workflows/codeberg-mirror.yml
vendored
|
@ -13,7 +13,7 @@ jobs:
|
||||||
if: github.repository == 'Vendicated/Vencord'
|
if: github.repository == 'Vendicated/Vencord'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- uses: pixta-dev/repository-mirroring-action@674e65a7d483ca28dafaacba0d07351bdcc8bd75 # v1.1.1
|
- uses: pixta-dev/repository-mirroring-action@674e65a7d483ca28dafaacba0d07351bdcc8bd75 # v1.1.1
|
||||||
|
|
10
.github/workflows/publish.yml
vendored
10
.github/workflows/publish.yml
vendored
|
@ -10,7 +10,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: check that tag matches package.json version
|
- name: check that tag matches package.json version
|
||||||
run: |
|
run: |
|
||||||
|
@ -20,19 +20,19 @@ jobs:
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v3 # Install pnpm using packageManager key in package.json
|
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
|
||||||
|
|
||||||
- name: Use Node.js 19
|
- name: Use Node.js 19
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 19
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build web
|
- name: Build web
|
||||||
run: pnpm buildWebStandalone
|
run: pnpm buildWeb --standalone
|
||||||
|
|
||||||
- name: Publish extension
|
- name: Publish extension
|
||||||
run: |
|
run: |
|
||||||
|
|
27
.github/workflows/reportBrokenPlugins.yml
vendored
27
.github/workflows/reportBrokenPlugins.yml
vendored
|
@ -11,40 +11,37 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
if: ${{ github.event_name == 'schedule' }}
|
if: ${{ github.event_name == 'schedule' }}
|
||||||
with:
|
with:
|
||||||
ref: dev
|
ref: dev
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v3 # Install pnpm using packageManager key in package.json
|
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
|
||||||
|
|
||||||
- name: Use Node.js 20
|
- name: Use Node.js 19
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 19
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
pnpm install --frozen-lockfile
|
pnpm install --frozen-lockfile
|
||||||
|
pnpm add puppeteer
|
||||||
|
|
||||||
- name: Install Google Chrome
|
sudo apt-get install -y chromium-browser
|
||||||
id: setup-chrome
|
|
||||||
uses: browser-actions/setup-chrome@82b9ce628cc5595478a9ebadc480958a36457dc2
|
|
||||||
with:
|
|
||||||
chrome-version: stable
|
|
||||||
|
|
||||||
- name: Build Vencord Reporter Version
|
- name: Build web
|
||||||
run: pnpm buildReporter
|
run: pnpm buildWeb --standalone --dev
|
||||||
|
|
||||||
- name: Create Report
|
- name: Create Report
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
run: |
|
run: |
|
||||||
export PATH="$PWD/node_modules/.bin:$PATH"
|
export PATH="$PWD/node_modules/.bin:$PATH"
|
||||||
export CHROMIUM_BIN=${{ steps.setup-chrome.outputs.chrome-path }}
|
export CHROMIUM_BIN=$(which chromium-browser)
|
||||||
|
|
||||||
esbuild scripts/generateReport.ts > dist/report.mjs
|
esbuild scripts/generateReport.ts > dist/report.mjs
|
||||||
node dist/report.mjs >> $GITHUB_STEP_SUMMARY
|
node dist/report.mjs >> $GITHUB_STEP_SUMMARY
|
||||||
|
@ -57,7 +54,7 @@ jobs:
|
||||||
if: success() || failure() # even run if previous one failed
|
if: success() || failure() # even run if previous one failed
|
||||||
run: |
|
run: |
|
||||||
export PATH="$PWD/node_modules/.bin:$PATH"
|
export PATH="$PWD/node_modules/.bin:$PATH"
|
||||||
export CHROMIUM_BIN=${{ steps.setup-chrome.outputs.chrome-path }}
|
export CHROMIUM_BIN=$(which chromium-browser)
|
||||||
export USE_CANARY=true
|
export USE_CANARY=true
|
||||||
|
|
||||||
esbuild scripts/generateReport.ts > dist/report.mjs
|
esbuild scripts/generateReport.ts > dist/report.mjs
|
||||||
|
|
10
.github/workflows/test.yml
vendored
10
.github/workflows/test.yml
vendored
|
@ -10,13 +10,13 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- uses: pnpm/action-setup@v3 # Install pnpm using packageManager key in package.json
|
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
|
||||||
|
|
||||||
- name: Use Node.js 20
|
- name: Use Node.js 18
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 18
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
|
1
.npmrc
1
.npmrc
|
@ -1,2 +1 @@
|
||||||
strict-peer-dependencies=false
|
strict-peer-dependencies=false
|
||||||
package-manager-strict=false
|
|
||||||
|
|
6
.vscode/extensions.json
vendored
6
.vscode/extensions.json
vendored
|
@ -1,9 +1,11 @@
|
||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
|
"eamodio.gitlens",
|
||||||
"EditorConfig.EditorConfig",
|
"EditorConfig.EditorConfig",
|
||||||
|
"ExodiusStudios.comment-anchors",
|
||||||
|
"formulahendry.auto-rename-tag",
|
||||||
"GregorBiswanger.json2ts",
|
"GregorBiswanger.json2ts",
|
||||||
"stylelint.vscode-stylelint",
|
"stylelint.vscode-stylelint"
|
||||||
"Vendicated.vencord-companion"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
@ -14,8 +14,6 @@
|
||||||
"typescript.preferences.quoteStyle": "double",
|
"typescript.preferences.quoteStyle": "double",
|
||||||
"javascript.preferences.quoteStyle": "double",
|
"javascript.preferences.quoteStyle": "double",
|
||||||
|
|
||||||
"eslint.experimental.useFlatConfig": false,
|
|
||||||
|
|
||||||
"gitlens.remotes": [
|
"gitlens.remotes": [
|
||||||
{
|
{
|
||||||
"domain": "codeberg.org",
|
"domain": "codeberg.org",
|
||||||
|
|
|
@ -19,14 +19,13 @@
|
||||||
/// <reference path="../src/modules.d.ts" />
|
/// <reference path="../src/modules.d.ts" />
|
||||||
/// <reference path="../src/globals.d.ts" />
|
/// <reference path="../src/globals.d.ts" />
|
||||||
|
|
||||||
import monacoHtmlLocal from "file://monacoWin.html?minify";
|
import monacoHtmlLocal from "~fileContent/monacoWin.html";
|
||||||
import monacoHtmlCdn from "file://../src/main/monacoWin.html?minify";
|
import monacoHtmlCdn from "~fileContent/../src/main/monacoWin.html";
|
||||||
import * as DataStore from "../src/api/DataStore";
|
import * as DataStore from "../src/api/DataStore";
|
||||||
import { debounce } from "../src/utils";
|
import { debounce } from "../src/utils";
|
||||||
import { EXTENSION_BASE_URL } from "../src/utils/web-metadata";
|
import { EXTENSION_BASE_URL } from "../src/utils/web-metadata";
|
||||||
import { getTheme, Theme } from "../src/utils/discord";
|
import { getTheme, Theme } from "../src/utils/discord";
|
||||||
import { getThemeInfo } from "../src/main/themes";
|
import { getThemeInfo } from "../src/main/themes";
|
||||||
import { Settings } from "../src/Vencord";
|
|
||||||
|
|
||||||
// Discord deletes this so need to store in variable
|
// Discord deletes this so need to store in variable
|
||||||
const { localStorage } = window;
|
const { localStorage } = window;
|
||||||
|
@ -97,15 +96,8 @@ window.VencordNative = {
|
||||||
},
|
},
|
||||||
|
|
||||||
settings: {
|
settings: {
|
||||||
get: () => {
|
get: () => localStorage.getItem("VencordSettings") || "{}",
|
||||||
try {
|
set: async (s: string) => localStorage.setItem("VencordSettings", s),
|
||||||
return JSON.parse(localStorage.getItem("VencordSettings") || "{}");
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to parse settings from localStorage: ", e);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
set: async (s: Settings) => localStorage.setItem("VencordSettings", JSON.stringify(s)),
|
|
||||||
getSettingsDir: async () => "LocalStorage"
|
getSettingsDir: async () => "LocalStorage"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -95,3 +95,5 @@ Simply run:
|
||||||
```shell
|
```shell
|
||||||
pnpm uninject
|
pnpm uninject
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you need more help, ask in the support channel in our [Discord Server](https://discord.gg/D9uwnFnqmd).
|
||||||
|
|
28
package.json
28
package.json
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "vencord",
|
"name": "vencord",
|
||||||
"private": "true",
|
"private": "true",
|
||||||
"version": "1.8.8",
|
"version": "1.7.2",
|
||||||
"description": "The cutest Discord client mod",
|
"description": "The cutest Discord client mod",
|
||||||
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
|
@ -18,23 +18,17 @@
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs",
|
"build": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs",
|
||||||
"buildStandalone": "pnpm build --standalone",
|
|
||||||
"buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs",
|
"buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs",
|
||||||
"buildWebStandalone": "pnpm buildWeb --standalone",
|
|
||||||
"buildReporter": "pnpm buildWebStandalone --reporter --skip-extension",
|
|
||||||
"buildReporterDesktop": "pnpm build --reporter",
|
|
||||||
"watch": "pnpm build --watch",
|
|
||||||
"watchWeb": "pnpm buildWeb --watch",
|
|
||||||
"generatePluginJson": "tsx scripts/generatePluginList.ts",
|
"generatePluginJson": "tsx scripts/generatePluginList.ts",
|
||||||
"generateTypes": "tspc --emitDeclarationOnly --declaration --outDir packages/vencord-types",
|
|
||||||
"inject": "node scripts/runInstaller.mjs",
|
"inject": "node scripts/runInstaller.mjs",
|
||||||
"uninject": "node scripts/runInstaller.mjs",
|
|
||||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --ignore-pattern src/userplugins",
|
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --ignore-pattern src/userplugins",
|
||||||
"lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins",
|
"lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins",
|
||||||
"lint:fix": "pnpm lint --fix",
|
"lint:fix": "pnpm lint --fix",
|
||||||
"test": "pnpm buildStandalone && pnpm lint && pnpm lint-styles && pnpm testTsc && pnpm generatePluginJson",
|
"test": "pnpm build && pnpm lint && pnpm lint-styles && pnpm testTsc && pnpm generatePluginJson",
|
||||||
"testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc",
|
"testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc",
|
||||||
"testTsc": "tsc --noEmit"
|
"testTsc": "tsc --noEmit",
|
||||||
|
"uninject": "node scripts/runInstaller.mjs",
|
||||||
|
"watch": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs --watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sapphi-red/web-noise-suppressor": "0.3.3",
|
"@sapphi-red/web-noise-suppressor": "0.3.3",
|
||||||
|
@ -66,20 +60,18 @@
|
||||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||||
"eslint-plugin-unused-imports": "^2.0.0",
|
"eslint-plugin-unused-imports": "^2.0.0",
|
||||||
"highlight.js": "10.6.0",
|
"highlight.js": "10.6.0",
|
||||||
"html-minifier-terser": "^7.2.0",
|
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"puppeteer-core": "^19.11.1",
|
"puppeteer-core": "^19.11.1",
|
||||||
"standalone-electron-types": "^1.0.0",
|
"standalone-electron-types": "^1.0.0",
|
||||||
"stylelint": "^15.6.0",
|
"stylelint": "^15.6.0",
|
||||||
"stylelint-config-standard": "^33.0.0",
|
"stylelint-config-standard": "^33.0.0",
|
||||||
"ts-patch": "^3.1.2",
|
|
||||||
"tsx": "^3.12.7",
|
"tsx": "^3.12.7",
|
||||||
"type-fest": "^3.9.0",
|
"type-fest": "^3.9.0",
|
||||||
"typescript": "^5.4.5",
|
"typescript": "^5.0.4",
|
||||||
"typescript-transform-paths": "^3.4.7",
|
"zip-local": "^0.3.5",
|
||||||
"zip-local": "^0.3.5"
|
"zustand": "^3.7.2"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.1.0",
|
"packageManager": "pnpm@8.10.2",
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"eslint-plugin-path-alias@1.0.0": "patches/eslint-plugin-path-alias@1.0.0.patch",
|
"eslint-plugin-path-alias@1.0.0": "patches/eslint-plugin-path-alias@1.0.0.patch",
|
||||||
|
@ -107,6 +99,6 @@
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18",
|
"node": ">=18",
|
||||||
"pnpm": ">=9"
|
"pnpm": ">=8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
7
packages/vencord-types/.gitignore
vendored
7
packages/vencord-types/.gitignore
vendored
|
@ -1,7 +0,0 @@
|
||||||
*
|
|
||||||
!.*ignore
|
|
||||||
!package.json
|
|
||||||
!*.md
|
|
||||||
!prepare.ts
|
|
||||||
!index.d.ts
|
|
||||||
!globals.d.ts
|
|
|
@ -1,4 +0,0 @@
|
||||||
node_modules
|
|
||||||
prepare.ts
|
|
||||||
.gitignore
|
|
||||||
HOW2PUB.md
|
|
|
@ -1,5 +0,0 @@
|
||||||
# How to publish
|
|
||||||
|
|
||||||
1. run `pnpm generateTypes` in the project root
|
|
||||||
2. bump package.json version
|
|
||||||
3. npm publish
|
|
|
@ -1,11 +0,0 @@
|
||||||
# Vencord Types
|
|
||||||
|
|
||||||
Typings for Vencord's api, published to npm
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm i @vencord/types
|
|
||||||
|
|
||||||
yarn add @vencord/types
|
|
||||||
|
|
||||||
pnpm add @vencord/types
|
|
||||||
```
|
|
24
packages/vencord-types/globals.d.ts
vendored
24
packages/vencord-types/globals.d.ts
vendored
|
@ -1,24 +0,0 @@
|
||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
export var VencordNative: typeof import("./VencordNative").default;
|
|
||||||
export var Vencord: typeof import("./Vencord");
|
|
||||||
}
|
|
||||||
|
|
||||||
export { };
|
|
5
packages/vencord-types/index.d.ts
vendored
5
packages/vencord-types/index.d.ts
vendored
|
@ -1,5 +0,0 @@
|
||||||
/* eslint-disable */
|
|
||||||
|
|
||||||
/// <reference path="Vencord.d.ts" />
|
|
||||||
/// <reference path="globals.d.ts" />
|
|
||||||
/// <reference path="modules.d.ts" />
|
|
|
@ -1,28 +0,0 @@
|
||||||
{
|
|
||||||
"name": "@vencord/types",
|
|
||||||
"private": false,
|
|
||||||
"version": "0.1.3",
|
|
||||||
"description": "",
|
|
||||||
"types": "index.d.ts",
|
|
||||||
"scripts": {
|
|
||||||
"prepublishOnly": "tsx ./prepare.ts",
|
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
|
||||||
},
|
|
||||||
"keywords": [],
|
|
||||||
"author": "Vencord",
|
|
||||||
"license": "GPL-3.0",
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/fs-extra": "^11.0.4",
|
|
||||||
"fs-extra": "^11.2.0",
|
|
||||||
"tsx": "^3.12.6"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@types/lodash": "^4.14.191",
|
|
||||||
"@types/node": "^18.11.18",
|
|
||||||
"@types/react": "^18.2.0",
|
|
||||||
"@types/react-dom": "^18.0.10",
|
|
||||||
"discord-types": "^1.3.26",
|
|
||||||
"standalone-electron-types": "^1.0.0",
|
|
||||||
"type-fest": "^3.5.3"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,47 +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 { cpSync, moveSync, readdirSync, rmSync } from "fs-extra";
|
|
||||||
import { join } from "path";
|
|
||||||
|
|
||||||
readdirSync(join(__dirname, "src"))
|
|
||||||
.forEach(child => moveSync(join(__dirname, "src", child), join(__dirname, child), { overwrite: true }));
|
|
||||||
|
|
||||||
const VencordSrc = join(__dirname, "..", "..", "src");
|
|
||||||
|
|
||||||
for (const file of ["preload.d.ts", "userplugins", "main", "debug", "src", "browser", "scripts"]) {
|
|
||||||
rmSync(join(__dirname, file), { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyDtsFiles(from: string, to: string) {
|
|
||||||
for (const file of readdirSync(from, { withFileTypes: true })) {
|
|
||||||
// bad
|
|
||||||
if (from === VencordSrc && file.name === "globals.d.ts") continue;
|
|
||||||
|
|
||||||
const fullFrom = join(from, file.name);
|
|
||||||
const fullTo = join(to, file.name);
|
|
||||||
|
|
||||||
if (file.isDirectory()) {
|
|
||||||
copyDtsFiles(fullFrom, fullTo);
|
|
||||||
} else if (file.name.endsWith(".d.ts")) {
|
|
||||||
cpSync(fullFrom, fullTo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
copyDtsFiles(VencordSrc, __dirname);
|
|
5590
pnpm-lock.yaml
5590
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -1,2 +0,0 @@
|
||||||
packages:
|
|
||||||
- packages/*
|
|
|
@ -21,21 +21,19 @@ import esbuild from "esbuild";
|
||||||
import { readdir } from "fs/promises";
|
import { readdir } from "fs/promises";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
|
|
||||||
import { BUILD_TIMESTAMP, commonOpts, exists, globPlugins, IS_DEV, IS_REPORTER, IS_STANDALONE, IS_UPDATER_DISABLED, VERSION, watch } from "./common.mjs";
|
import { BUILD_TIMESTAMP, commonOpts, existsAsync, globPlugins, isDev, isStandalone, updaterDisabled, VERSION, watch } from "./common.mjs";
|
||||||
|
|
||||||
const defines = {
|
const defines = {
|
||||||
IS_STANDALONE,
|
IS_STANDALONE: isStandalone,
|
||||||
IS_DEV,
|
IS_DEV: JSON.stringify(isDev),
|
||||||
IS_REPORTER,
|
IS_UPDATER_DISABLED: updaterDisabled,
|
||||||
IS_UPDATER_DISABLED,
|
|
||||||
IS_WEB: false,
|
IS_WEB: false,
|
||||||
IS_EXTENSION: false,
|
IS_EXTENSION: false,
|
||||||
VERSION: JSON.stringify(VERSION),
|
VERSION: JSON.stringify(VERSION),
|
||||||
BUILD_TIMESTAMP
|
BUILD_TIMESTAMP,
|
||||||
};
|
};
|
||||||
|
if (defines.IS_STANDALONE === "false")
|
||||||
if (defines.IS_STANDALONE === false)
|
// If this is a local build (not standalone), optimise
|
||||||
// If this is a local build (not standalone), optimize
|
|
||||||
// for the specific platform we're on
|
// for the specific platform we're on
|
||||||
defines["process.platform"] = JSON.stringify(process.platform);
|
defines["process.platform"] = JSON.stringify(process.platform);
|
||||||
|
|
||||||
|
@ -48,7 +46,7 @@ const nodeCommonOpts = {
|
||||||
platform: "node",
|
platform: "node",
|
||||||
target: ["esnext"],
|
target: ["esnext"],
|
||||||
external: ["electron", "original-fs", "~pluginNatives", ...commonOpts.external],
|
external: ["electron", "original-fs", "~pluginNatives", ...commonOpts.external],
|
||||||
define: defines
|
define: defines,
|
||||||
};
|
};
|
||||||
|
|
||||||
const sourceMapFooter = s => watch ? "" : `//# sourceMappingURL=vencord://${s}.js.map`;
|
const sourceMapFooter = s => watch ? "" : `//# sourceMappingURL=vencord://${s}.js.map`;
|
||||||
|
@ -75,13 +73,13 @@ const globNativesPlugin = {
|
||||||
let i = 0;
|
let i = 0;
|
||||||
for (const dir of pluginDirs) {
|
for (const dir of pluginDirs) {
|
||||||
const dirPath = join("src", dir);
|
const dirPath = join("src", dir);
|
||||||
if (!await exists(dirPath)) continue;
|
if (!await existsAsync(dirPath)) continue;
|
||||||
const plugins = await readdir(dirPath);
|
const plugins = await readdir(dirPath);
|
||||||
for (const p of plugins) {
|
for (const p of plugins) {
|
||||||
const nativePath = join(dirPath, p, "native.ts");
|
const nativePath = join(dirPath, p, "native.ts");
|
||||||
const indexNativePath = join(dirPath, p, "native/index.ts");
|
const indexNativePath = join(dirPath, p, "native/index.ts");
|
||||||
|
|
||||||
if (!(await exists(nativePath)) && !(await exists(indexNativePath)))
|
if (!(await existsAsync(nativePath)) && !(await existsAsync(indexNativePath)))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
const nameParts = p.split(".");
|
const nameParts = p.split(".");
|
||||||
|
|
|
@ -23,7 +23,7 @@ import { appendFile, mkdir, readdir, readFile, rm, writeFile } from "fs/promises
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import Zip from "zip-local";
|
import Zip from "zip-local";
|
||||||
|
|
||||||
import { BUILD_TIMESTAMP, commonOpts, globPlugins, IS_DEV, IS_REPORTER, VERSION } from "./common.mjs";
|
import { BUILD_TIMESTAMP, commonOpts, globPlugins, isDev, VERSION } from "./common.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {esbuild.BuildOptions}
|
* @type {esbuild.BuildOptions}
|
||||||
|
@ -33,23 +33,22 @@ const commonOptions = {
|
||||||
entryPoints: ["browser/Vencord.ts"],
|
entryPoints: ["browser/Vencord.ts"],
|
||||||
globalName: "Vencord",
|
globalName: "Vencord",
|
||||||
format: "iife",
|
format: "iife",
|
||||||
external: ["~plugins", "~git-hash", "/assets/*"],
|
external: ["plugins", "git-hash", "/assets/*"],
|
||||||
plugins: [
|
plugins: [
|
||||||
globPlugins("web"),
|
globPlugins("web"),
|
||||||
...commonOpts.plugins,
|
...commonOpts.plugins,
|
||||||
],
|
],
|
||||||
target: ["esnext"],
|
target: ["esnext"],
|
||||||
define: {
|
define: {
|
||||||
IS_WEB: true,
|
IS_WEB: "true",
|
||||||
IS_EXTENSION: false,
|
IS_EXTENSION: "false",
|
||||||
IS_STANDALONE: true,
|
IS_STANDALONE: "true",
|
||||||
IS_DEV,
|
IS_DEV: JSON.stringify(isDev),
|
||||||
IS_REPORTER,
|
IS_DISCORD_DESKTOP: "false",
|
||||||
IS_DISCORD_DESKTOP: false,
|
IS_VESKTOP: "false",
|
||||||
IS_VESKTOP: false,
|
IS_UPDATER_DISABLED: "true",
|
||||||
IS_UPDATER_DISABLED: true,
|
|
||||||
VERSION: JSON.stringify(VERSION),
|
VERSION: JSON.stringify(VERSION),
|
||||||
BUILD_TIMESTAMP
|
BUILD_TIMESTAMP,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -88,16 +87,16 @@ await Promise.all(
|
||||||
esbuild.build({
|
esbuild.build({
|
||||||
...commonOptions,
|
...commonOptions,
|
||||||
outfile: "dist/browser.js",
|
outfile: "dist/browser.js",
|
||||||
footer: { js: "//# sourceURL=VencordWeb" }
|
footer: { js: "//# sourceURL=VencordWeb" },
|
||||||
}),
|
}),
|
||||||
esbuild.build({
|
esbuild.build({
|
||||||
...commonOptions,
|
...commonOptions,
|
||||||
outfile: "dist/extension.js",
|
outfile: "dist/extension.js",
|
||||||
define: {
|
define: {
|
||||||
...commonOptions?.define,
|
...commonOptions?.define,
|
||||||
IS_EXTENSION: true,
|
IS_EXTENSION: "true",
|
||||||
},
|
},
|
||||||
footer: { js: "//# sourceURL=VencordWeb" }
|
footer: { js: "//# sourceURL=VencordWeb" },
|
||||||
}),
|
}),
|
||||||
esbuild.build({
|
esbuild.build({
|
||||||
...commonOptions,
|
...commonOptions,
|
||||||
|
@ -113,7 +112,7 @@ await Promise.all(
|
||||||
footer: {
|
footer: {
|
||||||
// UserScripts get wrapped in an iife, so define Vencord prop on window that returns our local
|
// UserScripts get wrapped in an iife, so define Vencord prop on window that returns our local
|
||||||
js: "Object.defineProperty(unsafeWindow,'Vencord',{get:()=>Vencord});"
|
js: "Object.defineProperty(unsafeWindow,'Vencord',{get:()=>Vencord});"
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
@ -166,7 +165,7 @@ async function buildExtension(target, files) {
|
||||||
f.startsWith("manifest") ? "manifest.json" : f,
|
f.startsWith("manifest") ? "manifest.json" : f,
|
||||||
content
|
content
|
||||||
];
|
];
|
||||||
})))
|
}))),
|
||||||
};
|
};
|
||||||
|
|
||||||
await rm(target, { recursive: true, force: true });
|
await rm(target, { recursive: true, force: true });
|
||||||
|
@ -193,19 +192,14 @@ const appendCssRuntime = readFile("dist/Vencord.user.css", "utf-8").then(content
|
||||||
return appendFile("dist/Vencord.user.js", cssRuntime);
|
return appendFile("dist/Vencord.user.js", cssRuntime);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!process.argv.includes("--skip-extension")) {
|
await Promise.all([
|
||||||
await Promise.all([
|
appendCssRuntime,
|
||||||
appendCssRuntime,
|
buildExtension("chromium-unpacked", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"]),
|
||||||
buildExtension("chromium-unpacked", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"]),
|
buildExtension("firefox-unpacked", ["background.js", "content.js", "manifestv2.json", "icon.png"]),
|
||||||
buildExtension("firefox-unpacked", ["background.js", "content.js", "manifestv2.json", "icon.png"]),
|
]);
|
||||||
]);
|
|
||||||
|
|
||||||
Zip.sync.zip("dist/chromium-unpacked").compress().save("dist/extension-chrome.zip");
|
Zip.sync.zip("dist/chromium-unpacked").compress().save("dist/extension-chrome.zip");
|
||||||
console.info("Packed Chromium Extension written to 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");
|
Zip.sync.zip("dist/firefox-unpacked").compress().save("dist/extension-firefox.zip");
|
||||||
console.info("Packed Firefox Extension written to dist/extension-firefox.zip");
|
console.info("Packed Firefox Extension written to dist/extension-firefox.zip");
|
||||||
|
|
||||||
} else {
|
|
||||||
await appendCssRuntime;
|
|
||||||
}
|
|
||||||
|
|
|
@ -20,41 +20,36 @@ import "../suppressExperimentalWarnings.js";
|
||||||
import "../checkNodeVersion.js";
|
import "../checkNodeVersion.js";
|
||||||
|
|
||||||
import { exec, execSync } from "child_process";
|
import { exec, execSync } from "child_process";
|
||||||
import esbuild from "esbuild";
|
|
||||||
import { constants as FsConstants, readFileSync } from "fs";
|
import { constants as FsConstants, readFileSync } from "fs";
|
||||||
import { access, readdir, readFile } from "fs/promises";
|
import { access, readdir, readFile } from "fs/promises";
|
||||||
import { minify as minifyHtml } from "html-minifier-terser";
|
|
||||||
import { join, relative } from "path";
|
import { join, relative } from "path";
|
||||||
import { promisify } from "util";
|
import { promisify } from "util";
|
||||||
|
|
||||||
|
// wtf is this assert syntax
|
||||||
|
import PackageJSON from "../../package.json" assert { type: "json" };
|
||||||
import { getPluginTarget } from "../utils.mjs";
|
import { getPluginTarget } from "../utils.mjs";
|
||||||
|
|
||||||
/** @type {import("../../package.json")} */
|
|
||||||
const PackageJSON = JSON.parse(readFileSync("package.json"));
|
|
||||||
|
|
||||||
export const VERSION = PackageJSON.version;
|
export const VERSION = PackageJSON.version;
|
||||||
// https://reproducible-builds.org/docs/source-date-epoch/
|
// https://reproducible-builds.org/docs/source-date-epoch/
|
||||||
export const BUILD_TIMESTAMP = Number(process.env.SOURCE_DATE_EPOCH) || Date.now();
|
export const BUILD_TIMESTAMP = Number(process.env.SOURCE_DATE_EPOCH) || Date.now();
|
||||||
|
|
||||||
export const watch = process.argv.includes("--watch");
|
export const watch = process.argv.includes("--watch");
|
||||||
export const IS_DEV = watch || process.argv.includes("--dev");
|
export const isDev = watch || process.argv.includes("--dev");
|
||||||
export const IS_REPORTER = process.argv.includes("--reporter");
|
export const isStandalone = JSON.stringify(process.argv.includes("--standalone"));
|
||||||
export const IS_STANDALONE = process.argv.includes("--standalone");
|
export const updaterDisabled = JSON.stringify(process.argv.includes("--disable-updater"));
|
||||||
|
|
||||||
export const IS_UPDATER_DISABLED = process.argv.includes("--disable-updater");
|
|
||||||
export const gitHash = process.env.VENCORD_HASH || execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
|
export const gitHash = process.env.VENCORD_HASH || execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
|
||||||
|
|
||||||
export const banner = {
|
export const banner = {
|
||||||
js: `
|
js: `
|
||||||
// Vencord ${gitHash}
|
// Vencord ${gitHash}
|
||||||
// Standalone: ${IS_STANDALONE}
|
// Standalone: ${isStandalone}
|
||||||
// Platform: ${IS_STANDALONE === false ? process.platform : "Universal"}
|
// Platform: ${isStandalone === "false" ? process.platform : "Universal"}
|
||||||
// Updater Disabled: ${IS_UPDATER_DISABLED}
|
// Updater disabled: ${updaterDisabled}
|
||||||
`.trim()
|
`.trim()
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function exists(path) {
|
const isWeb = process.argv.slice(0, 2).some(f => f.endsWith("buildWeb.mjs"));
|
||||||
return await access(path, FsConstants.F_OK)
|
|
||||||
|
export function existsAsync(path) {
|
||||||
|
return access(path, FsConstants.F_OK)
|
||||||
.then(() => true)
|
.then(() => true)
|
||||||
.catch(() => false);
|
.catch(() => false);
|
||||||
}
|
}
|
||||||
|
@ -68,7 +63,7 @@ export const makeAllPackagesExternalPlugin = {
|
||||||
setup(build) {
|
setup(build) {
|
||||||
const filter = /^[^./]|^\.[^./]|^\.\.[^/]/; // Must not start with "/" or "./" or "../"
|
const filter = /^[^./]|^\.[^./]|^\.\.[^/]/; // Must not start with "/" or "./" or "../"
|
||||||
build.onResolve({ filter }, args => ({ path: args.path, external: true }));
|
build.onResolve({ filter }, args => ({ path: args.path, external: true }));
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -91,14 +86,14 @@ export const globPlugins = kind => ({
|
||||||
let plugins = "\n";
|
let plugins = "\n";
|
||||||
let i = 0;
|
let i = 0;
|
||||||
for (const dir of pluginDirs) {
|
for (const dir of pluginDirs) {
|
||||||
if (!await exists(`./src/${dir}`)) continue;
|
if (!await existsAsync(`./src/${dir}`)) continue;
|
||||||
const files = await readdir(`./src/${dir}`);
|
const files = await readdir(`./src/${dir}`);
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if (file.startsWith("_") || file.startsWith(".")) continue;
|
if (file.startsWith("_") || file.startsWith(".")) continue;
|
||||||
if (file === "index.ts") continue;
|
if (file === "index.ts") continue;
|
||||||
|
|
||||||
const target = getPluginTarget(file);
|
const target = getPluginTarget(file);
|
||||||
if (target && !IS_REPORTER) {
|
if (target) {
|
||||||
if (target === "dev" && !watch) continue;
|
if (target === "dev" && !watch) continue;
|
||||||
if (target === "web" && kind === "discordDesktop") continue;
|
if (target === "web" && kind === "discordDesktop") continue;
|
||||||
if (target === "desktop" && kind === "web") continue;
|
if (target === "desktop" && kind === "web") continue;
|
||||||
|
@ -165,60 +160,21 @@ export const gitRemotePlugin = {
|
||||||
/**
|
/**
|
||||||
* @type {import("esbuild").Plugin}
|
* @type {import("esbuild").Plugin}
|
||||||
*/
|
*/
|
||||||
export const fileUrlPlugin = {
|
export const fileIncludePlugin = {
|
||||||
name: "file-uri-plugin",
|
name: "file-include-plugin",
|
||||||
setup: build => {
|
setup: build => {
|
||||||
const filter = /^file:\/\/.+$/;
|
const filter = /^~fileContent\/.+$/;
|
||||||
build.onResolve({ filter }, args => ({
|
build.onResolve({ filter }, args => ({
|
||||||
namespace: "file-uri",
|
namespace: "include-file",
|
||||||
path: args.path,
|
path: args.path,
|
||||||
pluginData: {
|
pluginData: {
|
||||||
uri: args.path,
|
path: join(args.resolveDir, args.path.slice("include-file/".length))
|
||||||
path: join(args.resolveDir, args.path.slice("file://".length).split("?")[0])
|
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
build.onLoad({ filter, namespace: "file-uri" }, async ({ pluginData: { path, uri } }) => {
|
build.onLoad({ filter, namespace: "include-file" }, async ({ pluginData: { path } }) => {
|
||||||
const { searchParams } = new URL(uri);
|
const [name, format] = path.split(";");
|
||||||
const base64 = searchParams.has("base64");
|
|
||||||
const minify = IS_STANDALONE === true && searchParams.has("minify");
|
|
||||||
const noTrim = searchParams.get("trim") === "false";
|
|
||||||
|
|
||||||
const encoding = base64 ? "base64" : "utf-8";
|
|
||||||
|
|
||||||
let content;
|
|
||||||
if (!minify) {
|
|
||||||
content = await readFile(path, encoding);
|
|
||||||
if (!noTrim) content = content.trimEnd();
|
|
||||||
} else {
|
|
||||||
if (path.endsWith(".html")) {
|
|
||||||
content = await minifyHtml(await readFile(path, "utf-8"), {
|
|
||||||
collapseWhitespace: true,
|
|
||||||
removeComments: true,
|
|
||||||
minifyCSS: true,
|
|
||||||
minifyJS: true,
|
|
||||||
removeEmptyAttributes: true,
|
|
||||||
removeRedundantAttributes: true,
|
|
||||||
removeScriptTypeAttributes: true,
|
|
||||||
removeStyleLinkTypeAttributes: true,
|
|
||||||
useShortDoctype: true
|
|
||||||
});
|
|
||||||
} else if (/[mc]?[jt]sx?$/.test(path)) {
|
|
||||||
const res = await esbuild.build({
|
|
||||||
entryPoints: [path],
|
|
||||||
write: false,
|
|
||||||
minify: true
|
|
||||||
});
|
|
||||||
content = res.outputFiles[0].text;
|
|
||||||
} else {
|
|
||||||
throw new Error(`Don't know how to minify file type: ${path}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (base64)
|
|
||||||
content = Buffer.from(content).toString("base64");
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
contents: `export default ${JSON.stringify(content)}`
|
contents: `export default ${JSON.stringify(await readFile(name, format ?? "utf-8"))}`
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -260,7 +216,7 @@ export const commonOpts = {
|
||||||
sourcemap: watch ? "inline" : "",
|
sourcemap: watch ? "inline" : "",
|
||||||
legalComments: "linked",
|
legalComments: "linked",
|
||||||
banner,
|
banner,
|
||||||
plugins: [fileUrlPlugin, gitHashPlugin, gitRemotePlugin, stylePlugin],
|
plugins: [fileIncludePlugin, gitHashPlugin, gitRemotePlugin, stylePlugin],
|
||||||
external: ["~plugins", "~git-hash", "~git-remote", "/assets/*"],
|
external: ["~plugins", "~git-hash", "~git-remote", "/assets/*"],
|
||||||
inject: ["./scripts/build/inject/react.mjs"],
|
inject: ["./scripts/build/inject/react.mjs"],
|
||||||
jsxFactory: "VencordCreateElement",
|
jsxFactory: "VencordCreateElement",
|
||||||
|
|
|
@ -16,8 +16,6 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* eslint-disable no-fallthrough */
|
|
||||||
|
|
||||||
// eslint-disable-next-line spaced-comment
|
// eslint-disable-next-line spaced-comment
|
||||||
/// <reference types="../src/globals" />
|
/// <reference types="../src/globals" />
|
||||||
// eslint-disable-next-line spaced-comment
|
// eslint-disable-next-line spaced-comment
|
||||||
|
@ -42,11 +40,10 @@ const browser = await pup.launch({
|
||||||
|
|
||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
await page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36");
|
await page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36");
|
||||||
await page.setBypassCSP(true);
|
|
||||||
|
|
||||||
async function maybeGetError(handle: JSHandle): Promise<string | undefined> {
|
function maybeGetError(handle: JSHandle) {
|
||||||
return await (handle as JSHandle<Error>)?.getProperty("message")
|
return (handle as JSHandle<Error>)?.getProperty("message")
|
||||||
.then(m => m?.jsonValue());
|
.then(m => m.jsonValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
const report = {
|
const report = {
|
||||||
|
@ -62,7 +59,6 @@ const report = {
|
||||||
error: string;
|
error: string;
|
||||||
}[],
|
}[],
|
||||||
otherErrors: [] as string[],
|
otherErrors: [] as string[],
|
||||||
ignoredErrors: [] as string[],
|
|
||||||
badWebpackFinds: [] as string[]
|
badWebpackFinds: [] as string[]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -71,8 +67,7 @@ const IGNORED_DISCORD_ERRORS = [
|
||||||
"Unable to process domain list delta: Client revision number is null",
|
"Unable to process domain list delta: Client revision number is null",
|
||||||
"Downloading the full bad domains file",
|
"Downloading the full bad domains file",
|
||||||
/\[GatewaySocket\].{0,110}Cannot access '/,
|
/\[GatewaySocket\].{0,110}Cannot access '/,
|
||||||
"search for 'name' in undefined",
|
"search for 'name' in undefined"
|
||||||
"Attempting to set fast connect zstd when unsupported"
|
|
||||||
] as Array<string | RegExp>;
|
] as Array<string | RegExp>;
|
||||||
|
|
||||||
function toCodeBlock(s: string) {
|
function toCodeBlock(s: string) {
|
||||||
|
@ -110,6 +105,15 @@ async function printReport() {
|
||||||
|
|
||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
|
const ignoredErrors = [] as string[];
|
||||||
|
report.otherErrors = report.otherErrors.filter(e => {
|
||||||
|
if (IGNORED_DISCORD_ERRORS.some(regex => e.match(regex))) {
|
||||||
|
ignoredErrors.push(e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
console.log("## Discord Errors");
|
console.log("## Discord Errors");
|
||||||
report.otherErrors.forEach(e => {
|
report.otherErrors.forEach(e => {
|
||||||
console.log(`- ${toCodeBlock(e)}`);
|
console.log(`- ${toCodeBlock(e)}`);
|
||||||
|
@ -118,7 +122,7 @@ async function printReport() {
|
||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
console.log("## Ignored Discord Errors");
|
console.log("## Ignored Discord Errors");
|
||||||
report.ignoredErrors.forEach(e => {
|
ignoredErrors.forEach(e => {
|
||||||
console.log(`- ${toCodeBlock(e)}`);
|
console.log(`- ${toCodeBlock(e)}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -183,39 +187,33 @@ page.on("console", async e => {
|
||||||
const level = e.type();
|
const level = e.type();
|
||||||
const rawArgs = e.args();
|
const rawArgs = e.args();
|
||||||
|
|
||||||
async function getText() {
|
|
||||||
try {
|
|
||||||
return await Promise.all(
|
|
||||||
e.args().map(async a => {
|
|
||||||
return await maybeGetError(a) || await a.jsonValue();
|
|
||||||
})
|
|
||||||
).then(a => a.join(" ").trim());
|
|
||||||
} catch {
|
|
||||||
return e.text();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const firstArg = await rawArgs[0]?.jsonValue();
|
const firstArg = await rawArgs[0]?.jsonValue();
|
||||||
|
if (firstArg === "[PUPPETEER_TEST_DONE_SIGNAL]") {
|
||||||
|
await browser.close();
|
||||||
|
await printReport();
|
||||||
|
process.exit();
|
||||||
|
}
|
||||||
|
|
||||||
const isVencord = firstArg === "[Vencord]";
|
const isVencord = firstArg === "[Vencord]";
|
||||||
const isDebug = firstArg === "[PUP_DEBUG]";
|
const isDebug = firstArg === "[PUP_DEBUG]";
|
||||||
|
const isWebpackFindFail = firstArg === "[PUP_WEBPACK_FIND_FAIL]";
|
||||||
|
|
||||||
|
if (isWebpackFindFail) {
|
||||||
|
process.exitCode = 1;
|
||||||
|
report.badWebpackFinds.push(await rawArgs[1].jsonValue() as string);
|
||||||
|
}
|
||||||
|
|
||||||
outer:
|
|
||||||
if (isVencord) {
|
if (isVencord) {
|
||||||
try {
|
const args = await Promise.all(e.args().map(a => a.jsonValue()));
|
||||||
var args = await Promise.all(e.args().map(a => a.jsonValue()));
|
|
||||||
} catch {
|
|
||||||
break outer;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [, tag, message, otherMessage] = args as Array<string>;
|
const [, tag, message] = args as Array<string>;
|
||||||
|
const cause = await maybeGetError(e.args()[3]);
|
||||||
|
|
||||||
switch (tag) {
|
switch (tag) {
|
||||||
case "WebpackInterceptor:":
|
case "WebpackInterceptor:":
|
||||||
const patchFailMatch = message.match(/Patch by (.+?) (had no effect|errored|found no module) \(Module id is (.+?)\): (.+)/)!;
|
const patchFailMatch = message.match(/Patch by (.+?) (had no effect|errored|found no module) \(Module id is (.+?)\): (.+)/)!;
|
||||||
if (!patchFailMatch) break;
|
if (!patchFailMatch) break;
|
||||||
|
|
||||||
console.error(await getText());
|
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
|
|
||||||
const [, plugin, type, id, regex] = patchFailMatch;
|
const [, plugin, type, id, regex] = patchFailMatch;
|
||||||
|
@ -224,7 +222,7 @@ page.on("console", async e => {
|
||||||
type,
|
type,
|
||||||
id,
|
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])
|
error: cause
|
||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
@ -232,77 +230,249 @@ page.on("console", async e => {
|
||||||
const failedToStartMatch = message.match(/Failed to start (.+)/);
|
const failedToStartMatch = message.match(/Failed to start (.+)/);
|
||||||
if (!failedToStartMatch) break;
|
if (!failedToStartMatch) break;
|
||||||
|
|
||||||
console.error(await getText());
|
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
|
|
||||||
const [, name] = failedToStartMatch;
|
const [, name] = failedToStartMatch;
|
||||||
report.badStarts.push({
|
report.badStarts.push({
|
||||||
plugin: name,
|
plugin: name,
|
||||||
error: await maybeGetError(e.args()[3]) ?? "Unknown error"
|
error: cause
|
||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case "LazyChunkLoader:":
|
|
||||||
console.error(await getText());
|
|
||||||
|
|
||||||
switch (message) {
|
|
||||||
case "A fatal error occurred:":
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
case "Reporter:":
|
|
||||||
console.error(await getText());
|
|
||||||
|
|
||||||
switch (message) {
|
|
||||||
case "A fatal error occurred:":
|
|
||||||
process.exit(1);
|
|
||||||
case "Webpack Find Fail:":
|
|
||||||
process.exitCode = 1;
|
|
||||||
report.badWebpackFinds.push(otherMessage);
|
|
||||||
break;
|
|
||||||
case "Finished test":
|
|
||||||
await browser.close();
|
|
||||||
await printReport();
|
|
||||||
process.exit();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDebug) {
|
if (isDebug) {
|
||||||
console.error(await getText());
|
console.error(e.text());
|
||||||
} else if (level === "error") {
|
} else if (level === "error") {
|
||||||
const text = await getText();
|
const text = await Promise.all(
|
||||||
|
e.args().map(async a => {
|
||||||
|
try {
|
||||||
|
return await maybeGetError(a) || await a.jsonValue();
|
||||||
|
} catch (e) {
|
||||||
|
return a.toString();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).then(a => a.join(" ").trim());
|
||||||
|
|
||||||
|
|
||||||
if (text.length && !text.startsWith("Failed to load resource: the server responded with a status of") && !text.includes("Webpack")) {
|
if (text.length && !text.startsWith("Failed to load resource: the server responded with a status of") && !text.includes("Webpack")) {
|
||||||
if (IGNORED_DISCORD_ERRORS.some(regex => text.match(regex))) {
|
console.error("[Unexpected Error]", text);
|
||||||
report.ignoredErrors.push(text);
|
report.otherErrors.push(text);
|
||||||
} else {
|
|
||||||
console.error("[Unexpected Error]", text);
|
|
||||||
report.otherErrors.push(text);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
page.on("error", e => console.error("[Error]", e.message));
|
page.on("error", e => console.error("[Error]", e));
|
||||||
page.on("pageerror", e => console.error("[Page Error]", e.message));
|
page.on("pageerror", e => console.error("[Page Error]", e));
|
||||||
|
|
||||||
async function reporterRuntime(token: string) {
|
await page.setBypassCSP(true);
|
||||||
Vencord.Webpack.waitFor(
|
|
||||||
"loginToken",
|
function runTime(token: string) {
|
||||||
m => {
|
console.log("[PUP_DEBUG]", "Starting test...");
|
||||||
console.log("[PUP_DEBUG]", "Logging in with token...");
|
|
||||||
m.loginToken(token);
|
try {
|
||||||
}
|
// Spoof languages to not be suspicious
|
||||||
);
|
Object.defineProperty(navigator, "languages", {
|
||||||
|
get: function () {
|
||||||
|
return ["en-US", "en"];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Monkey patch Logger to not log with custom css
|
||||||
|
// @ts-ignore
|
||||||
|
Vencord.Util.Logger.prototype._log = function (level, levelColor, args) {
|
||||||
|
if (level === "warn" || level === "error")
|
||||||
|
console[level]("[Vencord]", this.name + ":", ...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Force enable all plugins and patches
|
||||||
|
Vencord.Plugins.patches.length = 0;
|
||||||
|
Object.values(Vencord.Plugins.plugins).forEach(p => {
|
||||||
|
// Needs native server to run
|
||||||
|
if (p.name === "WebRichPresence (arRPC)") return;
|
||||||
|
|
||||||
|
Vencord.Settings.plugins[p.name].enabled = true;
|
||||||
|
p.patches?.forEach(patch => {
|
||||||
|
patch.plugin = p.name;
|
||||||
|
delete patch.predicate;
|
||||||
|
delete patch.group;
|
||||||
|
|
||||||
|
if (!Array.isArray(patch.replacement))
|
||||||
|
patch.replacement = [patch.replacement];
|
||||||
|
|
||||||
|
patch.replacement.forEach(r => {
|
||||||
|
delete r.predicate;
|
||||||
|
});
|
||||||
|
|
||||||
|
Vencord.Plugins.patches.push(patch);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Vencord.Webpack.waitFor(
|
||||||
|
"loginToken",
|
||||||
|
m => {
|
||||||
|
console.log("[PUP_DEBUG]", "Logging in with token...");
|
||||||
|
m.loginToken(token);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Force load all chunks
|
||||||
|
Vencord.Webpack.onceReady.then(() => setTimeout(async () => {
|
||||||
|
console.log("[PUP_DEBUG]", "Webpack is ready!");
|
||||||
|
|
||||||
|
const { wreq } = Vencord.Webpack;
|
||||||
|
|
||||||
|
console.log("[PUP_DEBUG]", "Loading all chunks...");
|
||||||
|
|
||||||
|
let chunks = null as Record<number, string[]> | null;
|
||||||
|
const sym = Symbol("Vencord.chunksExtract");
|
||||||
|
|
||||||
|
Object.defineProperty(Object.prototype, sym, {
|
||||||
|
get() {
|
||||||
|
chunks = this;
|
||||||
|
},
|
||||||
|
set() { },
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await (wreq as any).el(sym);
|
||||||
|
delete Object.prototype[sym];
|
||||||
|
|
||||||
|
const validChunksEntryPoints = new Set<string>();
|
||||||
|
const validChunks = new Set<string>();
|
||||||
|
const invalidChunks = new Set<string>();
|
||||||
|
|
||||||
|
if (!chunks) throw new Error("Failed to get chunks");
|
||||||
|
|
||||||
|
for (const entryPoint in chunks) {
|
||||||
|
const chunkIds = chunks[entryPoint];
|
||||||
|
let invalidEntryPoint = false;
|
||||||
|
|
||||||
|
for (const id of chunkIds) {
|
||||||
|
if (wreq.u(id) == null || wreq.u(id) === "undefined.js") continue;
|
||||||
|
|
||||||
|
const isWasm = await fetch(wreq.p + wreq.u(id))
|
||||||
|
.then(r => r.text())
|
||||||
|
.then(t => t.includes(".module.wasm") || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push"));
|
||||||
|
|
||||||
|
if (isWasm) {
|
||||||
|
invalidChunks.add(id);
|
||||||
|
invalidEntryPoint = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
validChunks.add(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!invalidEntryPoint)
|
||||||
|
validChunksEntryPoints.add(entryPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entryPoint of validChunksEntryPoints) {
|
||||||
|
try {
|
||||||
|
// Loads all chunks required for an entry point
|
||||||
|
await (wreq as any).el(entryPoint);
|
||||||
|
} catch (err) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matches "id" or id:
|
||||||
|
const chunkIdRegex = /(?:"(\d+?)")|(?:(\d+?):)/g;
|
||||||
|
const wreqU = wreq.u.toString();
|
||||||
|
|
||||||
|
const allChunks = [] as string[];
|
||||||
|
let currentMatch: RegExpExecArray | null;
|
||||||
|
|
||||||
|
while ((currentMatch = chunkIdRegex.exec(wreqU)) != null) {
|
||||||
|
const id = currentMatch[1] ?? currentMatch[2];
|
||||||
|
if (id == null) continue;
|
||||||
|
|
||||||
|
allChunks.push(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allChunks.length === 0) throw new Error("Failed to get all chunks");
|
||||||
|
const chunksLeft = allChunks.filter(id => {
|
||||||
|
return !(validChunks.has(id) || invalidChunks.has(id));
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const id of chunksLeft) {
|
||||||
|
const isWasm = await fetch(wreq.p + wreq.u(id))
|
||||||
|
.then(r => r.text())
|
||||||
|
.then(t => t.includes(".module.wasm") || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push"));
|
||||||
|
|
||||||
|
// Loads a chunk
|
||||||
|
if (!isWasm) await wreq.e(id as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure every chunk has finished loading
|
||||||
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
|
|
||||||
|
for (const entryPoint of validChunksEntryPoints) {
|
||||||
|
try {
|
||||||
|
if (wreq.m[entryPoint]) wreq(entryPoint as any);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[PUP_DEBUG]", "Finished loading all chunks!");
|
||||||
|
|
||||||
|
for (const patch of Vencord.Plugins.patches) {
|
||||||
|
if (!patch.all) {
|
||||||
|
new Vencord.Util.Logger("WebpackInterceptor").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [searchType, args] of Vencord.Webpack.lazyWebpackSearchHistory) {
|
||||||
|
let method = searchType;
|
||||||
|
|
||||||
|
if (searchType === "findComponent") method = "find";
|
||||||
|
if (searchType === "findExportedComponent") method = "findByProps";
|
||||||
|
if (searchType === "waitFor" || searchType === "waitForComponent") {
|
||||||
|
if (typeof args[0] === "string") method = "findByProps";
|
||||||
|
else method = "find";
|
||||||
|
}
|
||||||
|
if (searchType === "waitForStore") method = "findStore";
|
||||||
|
|
||||||
|
try {
|
||||||
|
let result: any;
|
||||||
|
|
||||||
|
if (method === "proxyLazyWebpack" || method === "LazyComponentWebpack") {
|
||||||
|
const [factory] = args;
|
||||||
|
result = factory();
|
||||||
|
} else if (method === "extractAndLoadChunks") {
|
||||||
|
const [code, matcher] = args;
|
||||||
|
|
||||||
|
const module = Vencord.Webpack.findModuleFactory(...code);
|
||||||
|
if (module) result = module.toString().match(Vencord.Util.canonicalizeMatch(matcher));
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
result = Vencord.Webpack[method](...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == null || ("$$vencordInternal" in result && result.$$vencordInternal() == null)) throw "a rock at ben shapiro";
|
||||||
|
} catch (e) {
|
||||||
|
let logMessage = searchType;
|
||||||
|
if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") logMessage += `(${args[0].toString().slice(0, 147)}...)`;
|
||||||
|
else if (method === "extractAndLoadChunks") logMessage += `([${args[0].map(arg => `"${arg}"`).join(", ")}], ${args[1].toString()})`;
|
||||||
|
else logMessage += `(${args.map(arg => `"${arg}"`).join(", ")})`;
|
||||||
|
|
||||||
|
console.log("[PUP_WEBPACK_FIND_FAIL]", logMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => console.log("[PUPPETEER_TEST_DONE_SIGNAL]"), 1000);
|
||||||
|
}, 1000));
|
||||||
|
} catch (e) {
|
||||||
|
console.log("[PUP_DEBUG]", "A fatal error occurred:", e);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.evaluateOnNewDocument(`
|
await page.evaluateOnNewDocument(`
|
||||||
if (location.host.endsWith("discord.com")) {
|
${readFileSync("./dist/browser.js", "utf-8")}
|
||||||
${readFileSync("./dist/browser.js", "utf-8")};
|
|
||||||
(${reporterRuntime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)});
|
;(${runTime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)});
|
||||||
}
|
|
||||||
`);
|
`);
|
||||||
|
|
||||||
await page.goto(CANARY ? "https://canary.discord.com/login" : "https://discord.com/login");
|
await page.goto(CANARY ? "https://canary.discord.com/login" : "https://discord.com/login");
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export * as Api from "./api";
|
export * as Api from "./api";
|
||||||
export * as Components from "./components";
|
|
||||||
export * as Plugins from "./plugins";
|
export * as Plugins from "./plugins";
|
||||||
export * as Util from "./utils";
|
export * as Util from "./utils";
|
||||||
export * as QuickCss from "./utils/quickCss";
|
export * as QuickCss from "./utils/quickCss";
|
||||||
|
@ -28,7 +27,6 @@ export { PlainSettings, Settings };
|
||||||
import "./utils/quickCss";
|
import "./utils/quickCss";
|
||||||
import "./webpack/patchWebpack";
|
import "./webpack/patchWebpack";
|
||||||
|
|
||||||
import { openUpdaterModal } from "@components/VencordSettings/UpdaterTab";
|
|
||||||
import { StartAt } from "@utils/types";
|
import { StartAt } from "@utils/types";
|
||||||
|
|
||||||
import { get as dsGet } from "./api/DataStore";
|
import { get as dsGet } from "./api/DataStore";
|
||||||
|
@ -42,10 +40,6 @@ import { checkForUpdates, update, UpdateLogger } from "./utils/updater";
|
||||||
import { onceReady } from "./webpack";
|
import { onceReady } from "./webpack";
|
||||||
import { SettingsRouter } from "./webpack/common";
|
import { SettingsRouter } from "./webpack/common";
|
||||||
|
|
||||||
if (IS_REPORTER) {
|
|
||||||
require("./debug/runReporter");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function syncSettings() {
|
async function syncSettings() {
|
||||||
// pre-check for local shared settings
|
// pre-check for local shared settings
|
||||||
if (
|
if (
|
||||||
|
@ -91,7 +85,7 @@ async function init() {
|
||||||
|
|
||||||
syncSettings();
|
syncSettings();
|
||||||
|
|
||||||
if (!IS_WEB && !IS_UPDATER_DISABLED) {
|
if (!IS_WEB) {
|
||||||
try {
|
try {
|
||||||
const isOutdated = await checkForUpdates();
|
const isOutdated = await checkForUpdates();
|
||||||
if (!isOutdated) return;
|
if (!isOutdated) return;
|
||||||
|
@ -109,13 +103,16 @@ async function init() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => showNotification({
|
if (Settings.notifyAboutUpdates)
|
||||||
title: "A Vencord update is available!",
|
setTimeout(() => showNotification({
|
||||||
body: "Click here to view the update",
|
title: "A Vencord update is available!",
|
||||||
permanent: true,
|
body: "Click here to view the update",
|
||||||
noPersist: true,
|
permanent: true,
|
||||||
onClick: openUpdaterModal!
|
noPersist: true,
|
||||||
}), 10_000);
|
onClick() {
|
||||||
|
SettingsRouter.open("VencordUpdater");
|
||||||
|
}
|
||||||
|
}), 10_000);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
UpdateLogger.error("Failed to check for updates", err);
|
UpdateLogger.error("Failed to check for updates", err);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,12 +4,11 @@
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { PluginIpcMappings } from "@main/ipcPlugins";
|
import { IpcEvents } from "@utils/IpcEvents";
|
||||||
import type { UserThemeHeader } from "@main/themes";
|
|
||||||
import { IpcEvents } from "@shared/IpcEvents";
|
|
||||||
import { IpcRes } from "@utils/types";
|
import { IpcRes } from "@utils/types";
|
||||||
import type { Settings } from "api/Settings";
|
|
||||||
import { ipcRenderer } from "electron";
|
import { ipcRenderer } from "electron";
|
||||||
|
import { PluginIpcMappings } from "main/ipcPlugins";
|
||||||
|
import type { UserThemeHeader } from "main/themes";
|
||||||
|
|
||||||
function invoke<T = any>(event: IpcEvents, ...args: any[]) {
|
function invoke<T = any>(event: IpcEvents, ...args: any[]) {
|
||||||
return ipcRenderer.invoke(event, ...args) as Promise<T>;
|
return ipcRenderer.invoke(event, ...args) as Promise<T>;
|
||||||
|
@ -47,8 +46,8 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
settings: {
|
settings: {
|
||||||
get: () => sendSync<Settings>(IpcEvents.GET_SETTINGS),
|
get: () => sendSync<string>(IpcEvents.GET_SETTINGS),
|
||||||
set: (settings: Settings, pathToNotify?: string) => invoke<void>(IpcEvents.SET_SETTINGS, settings, pathToNotify),
|
set: (settings: string) => invoke<void>(IpcEvents.SET_SETTINGS, settings),
|
||||||
getSettingsDir: () => invoke<string>(IpcEvents.GET_SETTINGS_DIR),
|
getSettingsDir: () => invoke<string>(IpcEvents.GET_SETTINGS_DIR),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ export interface ProfileBadge {
|
||||||
image?: string;
|
image?: string;
|
||||||
link?: string;
|
link?: string;
|
||||||
/** Action to perform when you click the badge */
|
/** Action to perform when you click the badge */
|
||||||
onClick?(event: React.MouseEvent<HTMLButtonElement, MouseEvent>, props: BadgeUserArgs): void;
|
onClick?(): void;
|
||||||
/** Should the user display this badge? */
|
/** Should the user display this badge? */
|
||||||
shouldShow?(userInfo: BadgeUserArgs): boolean;
|
shouldShow?(userInfo: BadgeUserArgs): boolean;
|
||||||
/** Optional props (e.g. style) for the badge, ignored for component badges */
|
/** Optional props (e.g. style) for the badge, ignored for component badges */
|
||||||
|
@ -87,7 +87,9 @@ export function _getBadges(args: BadgeUserArgs) {
|
||||||
|
|
||||||
export interface BadgeUserArgs {
|
export interface BadgeUserArgs {
|
||||||
user: User;
|
user: User;
|
||||||
guildId: string;
|
profile: Profile;
|
||||||
|
premiumSince: Date;
|
||||||
|
premiumGuildSince?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ConnectedAccount {
|
interface ConnectedAccount {
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { mergeDefaults } from "@utils/mergeDefaults";
|
import { mergeDefaults } from "@utils/misc";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
import { MessageActions, SnowflakeUtils } from "@webpack/common";
|
import { MessageActions, SnowflakeUtils } from "@webpack/common";
|
||||||
import { Message } from "discord-types/general";
|
import { Message } from "discord-types/general";
|
||||||
|
|
|
@ -49,7 +49,7 @@ let defaultGetStoreFunc: UseStore | undefined;
|
||||||
|
|
||||||
function defaultGetStore() {
|
function defaultGetStore() {
|
||||||
if (!defaultGetStoreFunc) {
|
if (!defaultGetStoreFunc) {
|
||||||
defaultGetStoreFunc = createStore(!IS_REPORTER ? "VencordData" : "VencordDataReporter", "VencordStore");
|
defaultGetStoreFunc = createStore("VencordData", "VencordStore");
|
||||||
}
|
}
|
||||||
return defaultGetStoreFunc;
|
return defaultGetStoreFunc;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
/*
|
|
||||||
* Vencord, a Discord client mod
|
|
||||||
* Copyright (c) 2024 Vendicated and contributors
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { MessageCache, MessageStore } from "@webpack/common";
|
|
||||||
import { FluxStore } from "@webpack/types";
|
|
||||||
import { Message } from "discord-types/general";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update and re-render a message
|
|
||||||
* @param channelId The channel id of the message
|
|
||||||
* @param messageId The message id
|
|
||||||
* @param fields The fields of the message to change. Leave empty if you just want to re-render
|
|
||||||
*/
|
|
||||||
export function updateMessage(channelId: string, messageId: string, fields?: Partial<Message>) {
|
|
||||||
const channelMessageCache = MessageCache.getOrCreate(channelId);
|
|
||||||
if (!channelMessageCache.has(messageId)) return;
|
|
||||||
|
|
||||||
// To cause a message to re-render, we basically need to create a new instance of the message and obtain a new reference
|
|
||||||
// If we have fields to modify we can use the merge method of the class, otherwise we just create a new instance with the old fields
|
|
||||||
const newChannelMessageCache = channelMessageCache.update(messageId, (oldMessage: any) => {
|
|
||||||
return fields ? oldMessage.merge(fields) : new oldMessage.constructor(oldMessage);
|
|
||||||
});
|
|
||||||
|
|
||||||
MessageCache.commit(newChannelMessageCache);
|
|
||||||
(MessageStore as unknown as FluxStore).emitChange();
|
|
||||||
}
|
|
|
@ -113,7 +113,7 @@ export default ErrorBoundary.wrap(function NotificationComponent({
|
||||||
{timeout !== 0 && !permanent && (
|
{timeout !== 0 && !permanent && (
|
||||||
<div
|
<div
|
||||||
className="vc-notification-progressbar"
|
className="vc-notification-progressbar"
|
||||||
style={{ width: `${(1 - timeoutProgress) * 100}%`, backgroundColor: color || "var(--brand-500)" }}
|
style={{ width: `${(1 - timeoutProgress) * 100}%`, backgroundColor: color || "var(--brand-experiment)" }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -100,7 +100,6 @@ export async function showNotification(data: NotificationData) {
|
||||||
const n = new Notification(title, {
|
const n = new Notification(title, {
|
||||||
body,
|
body,
|
||||||
icon,
|
icon,
|
||||||
// @ts-expect-error ts is drunk
|
|
||||||
image
|
image
|
||||||
});
|
});
|
||||||
n.onclick = onClick;
|
n.onclick = onClick;
|
||||||
|
|
|
@ -16,11 +16,10 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { debounce } from "@shared/debounce";
|
import { debounce } from "@utils/debounce";
|
||||||
import { SettingsStore as SettingsStoreClass } from "@shared/SettingsStore";
|
|
||||||
import { localStorage } from "@utils/localStorage";
|
import { localStorage } from "@utils/localStorage";
|
||||||
import { Logger } from "@utils/Logger";
|
import { Logger } from "@utils/Logger";
|
||||||
import { mergeDefaults } from "@utils/mergeDefaults";
|
import { mergeDefaults } from "@utils/misc";
|
||||||
import { putCloudSettings } from "@utils/settingsSync";
|
import { putCloudSettings } from "@utils/settingsSync";
|
||||||
import { DefinedSettings, OptionType, SettingsChecks, SettingsDefinition } from "@utils/types";
|
import { DefinedSettings, OptionType, SettingsChecks, SettingsDefinition } from "@utils/types";
|
||||||
import { React } from "@webpack/common";
|
import { React } from "@webpack/common";
|
||||||
|
@ -29,6 +28,7 @@ import plugins from "~plugins";
|
||||||
|
|
||||||
const logger = new Logger("Settings");
|
const logger = new Logger("Settings");
|
||||||
export interface Settings {
|
export interface Settings {
|
||||||
|
notifyAboutUpdates: boolean;
|
||||||
autoUpdate: boolean;
|
autoUpdate: boolean;
|
||||||
autoUpdateNotification: boolean,
|
autoUpdateNotification: boolean,
|
||||||
useQuickCss: boolean;
|
useQuickCss: boolean;
|
||||||
|
@ -52,6 +52,7 @@ export interface Settings {
|
||||||
| "under-page"
|
| "under-page"
|
||||||
| "window"
|
| "window"
|
||||||
| undefined;
|
| undefined;
|
||||||
|
macosTranslucency: boolean | undefined;
|
||||||
disableMinSize: boolean;
|
disableMinSize: boolean;
|
||||||
winNativeTitleBar: boolean;
|
winNativeTitleBar: boolean;
|
||||||
plugins: {
|
plugins: {
|
||||||
|
@ -77,7 +78,8 @@ export interface Settings {
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultSettings: Settings = {
|
const DefaultSettings: Settings = {
|
||||||
autoUpdate: true,
|
notifyAboutUpdates: true,
|
||||||
|
autoUpdate: false,
|
||||||
autoUpdateNotification: true,
|
autoUpdateNotification: true,
|
||||||
useQuickCss: true,
|
useQuickCss: true,
|
||||||
themeLinks: [],
|
themeLinks: [],
|
||||||
|
@ -86,6 +88,8 @@ const DefaultSettings: Settings = {
|
||||||
frameless: false,
|
frameless: false,
|
||||||
transparent: false,
|
transparent: false,
|
||||||
winCtrlQ: false,
|
winCtrlQ: false,
|
||||||
|
// Replaced by macosVibrancyStyle
|
||||||
|
macosTranslucency: undefined,
|
||||||
macosVibrancyStyle: undefined,
|
macosVibrancyStyle: undefined,
|
||||||
disableMinSize: false,
|
disableMinSize: false,
|
||||||
winNativeTitleBar: false,
|
winNativeTitleBar: false,
|
||||||
|
@ -106,8 +110,13 @@ const DefaultSettings: Settings = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const settings = !IS_REPORTER ? VencordNative.settings.get() : {} as Settings;
|
try {
|
||||||
mergeDefaults(settings, DefaultSettings);
|
var settings = JSON.parse(VencordNative.settings.get()) as Settings;
|
||||||
|
mergeDefaults(settings, DefaultSettings);
|
||||||
|
} catch (err) {
|
||||||
|
var settings = mergeDefaults({} as Settings, DefaultSettings);
|
||||||
|
logger.error("An error occurred while loading the settings. Corrupt settings file?\n", err);
|
||||||
|
}
|
||||||
|
|
||||||
const saveSettingsOnFrequentAction = debounce(async () => {
|
const saveSettingsOnFrequentAction = debounce(async () => {
|
||||||
if (Settings.cloud.settingsSync && Settings.cloud.authenticated) {
|
if (Settings.cloud.settingsSync && Settings.cloud.authenticated) {
|
||||||
|
@ -116,52 +125,74 @@ const saveSettingsOnFrequentAction = debounce(async () => {
|
||||||
}
|
}
|
||||||
}, 60_000);
|
}, 60_000);
|
||||||
|
|
||||||
|
type SubscriptionCallback = ((newValue: any, path: string) => void) & { _paths?: Array<string>; };
|
||||||
|
const subscriptions = new Set<SubscriptionCallback>();
|
||||||
|
|
||||||
export const SettingsStore = new SettingsStoreClass(settings, {
|
const proxyCache = {} as Record<string, any>;
|
||||||
readOnly: true,
|
|
||||||
getDefaultValue({
|
|
||||||
target,
|
|
||||||
key,
|
|
||||||
path
|
|
||||||
}) {
|
|
||||||
const v = target[key];
|
|
||||||
if (!plugins) return v; // plugins not initialised yet. this means this path was reached by being called on the top level
|
|
||||||
|
|
||||||
if (path === "plugins" && key in plugins)
|
// Wraps the passed settings object in a Proxy to nicely handle change listeners and default values
|
||||||
return target[key] = {
|
function makeProxy(settings: any, root = settings, path = ""): Settings {
|
||||||
enabled: IS_REPORTER ?? plugins[key].required ?? plugins[key].enabledByDefault ?? false
|
return proxyCache[path] ??= new Proxy(settings, {
|
||||||
};
|
get(target, p: string) {
|
||||||
|
const v = target[p];
|
||||||
|
|
||||||
// Since the property is not set, check if this is a plugin's setting and if so, try to resolve
|
// using "in" is important in the following cases to properly handle falsy or nullish values
|
||||||
// the default value.
|
if (!(p in target)) {
|
||||||
if (path.startsWith("plugins.")) {
|
// Return empty for plugins with no settings
|
||||||
const plugin = path.slice("plugins.".length);
|
if (path === "plugins" && p in plugins)
|
||||||
if (plugin in plugins) {
|
return target[p] = makeProxy({
|
||||||
const setting = plugins[plugin].options?.[key];
|
enabled: plugins[p].required ?? plugins[p].enabledByDefault ?? false
|
||||||
if (!setting) return v;
|
}, root, `plugins.${p}`);
|
||||||
|
|
||||||
if ("default" in setting)
|
// Since the property is not set, check if this is a plugin's setting and if so, try to resolve
|
||||||
// normal setting with a default value
|
// the default value.
|
||||||
return (target[key] = setting.default);
|
if (path.startsWith("plugins.")) {
|
||||||
|
const plugin = path.slice("plugins.".length);
|
||||||
|
if (plugin in plugins) {
|
||||||
|
const setting = plugins[plugin].options?.[p];
|
||||||
|
if (!setting) return v;
|
||||||
|
if ("default" in setting)
|
||||||
|
// normal setting with a default value
|
||||||
|
return (target[p] = setting.default);
|
||||||
|
if (setting.type === OptionType.SELECT) {
|
||||||
|
const def = setting.options.find(o => o.default);
|
||||||
|
if (def)
|
||||||
|
target[p] = def.value;
|
||||||
|
return def?.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
if (setting.type === OptionType.SELECT) {
|
// Recursively proxy Objects with the updated property path
|
||||||
const def = setting.options.find(o => o.default);
|
if (typeof v === "object" && !Array.isArray(v) && v !== null)
|
||||||
if (def)
|
return makeProxy(v, root, `${path}${path && "."}${p}`);
|
||||||
target[key] = def.value;
|
|
||||||
return def?.value;
|
// primitive or similar, no need to proxy further
|
||||||
|
return v;
|
||||||
|
},
|
||||||
|
|
||||||
|
set(target, p: string, v) {
|
||||||
|
// avoid unnecessary updates to React Components and other listeners
|
||||||
|
if (target[p] === v) return true;
|
||||||
|
|
||||||
|
target[p] = v;
|
||||||
|
// Call any listeners that are listening to a setting of this path
|
||||||
|
const setPath = `${path}${path && "."}${p}`;
|
||||||
|
delete proxyCache[setPath];
|
||||||
|
for (const subscription of subscriptions) {
|
||||||
|
if (!subscription._paths || subscription._paths.includes(setPath)) {
|
||||||
|
subscription(v, setPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// And don't forget to persist the settings!
|
||||||
|
PlainSettings.cloud.settingsSyncVersion = Date.now();
|
||||||
|
localStorage.Vencord_settingsDirty = true;
|
||||||
|
saveSettingsOnFrequentAction();
|
||||||
|
VencordNative.settings.set(JSON.stringify(root, null, 4));
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return v;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!IS_REPORTER) {
|
|
||||||
SettingsStore.addGlobalChangeListener((_, path) => {
|
|
||||||
SettingsStore.plain.cloud.settingsSyncVersion = Date.now();
|
|
||||||
localStorage.Vencord_settingsDirty = true;
|
|
||||||
saveSettingsOnFrequentAction();
|
|
||||||
VencordNative.settings.set(SettingsStore.plain, path);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,7 +210,7 @@ export const PlainSettings = settings;
|
||||||
* the updated settings to disk.
|
* the updated settings to disk.
|
||||||
* This recursively proxies objects. If you need the object non proxied, use {@link PlainSettings}
|
* This recursively proxies objects. If you need the object non proxied, use {@link PlainSettings}
|
||||||
*/
|
*/
|
||||||
export const Settings = SettingsStore.store;
|
export const Settings = makeProxy(settings);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Settings hook for React components. Returns a smart settings
|
* Settings hook for React components. Returns a smart settings
|
||||||
|
@ -192,21 +223,45 @@ export const Settings = SettingsStore.store;
|
||||||
export function useSettings(paths?: UseSettings<Settings>[]) {
|
export function useSettings(paths?: UseSettings<Settings>[]) {
|
||||||
const [, forceUpdate] = React.useReducer(() => ({}), {});
|
const [, forceUpdate] = React.useReducer(() => ({}), {});
|
||||||
|
|
||||||
|
if (paths) {
|
||||||
|
(forceUpdate as SubscriptionCallback)._paths = paths;
|
||||||
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (paths) {
|
subscriptions.add(forceUpdate);
|
||||||
paths.forEach(p => SettingsStore.addChangeListener(p, forceUpdate));
|
return () => void subscriptions.delete(forceUpdate);
|
||||||
return () => paths.forEach(p => SettingsStore.removeChangeListener(p, forceUpdate));
|
|
||||||
} else {
|
|
||||||
SettingsStore.addGlobalChangeListener(forceUpdate);
|
|
||||||
return () => SettingsStore.removeGlobalChangeListener(forceUpdate);
|
|
||||||
}
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return SettingsStore.store;
|
return Settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolves a possibly nested prop in the form of "some.nested.prop" to type of T.some.nested.prop
|
||||||
|
type ResolvePropDeep<T, P> = P extends "" ? T :
|
||||||
|
P extends `${infer Pre}.${infer Suf}` ?
|
||||||
|
Pre extends keyof T ? ResolvePropDeep<T[Pre], Suf> : never : P extends keyof T ? T[P] : never;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a settings listener that will be invoked whenever the desired setting is updated
|
||||||
|
* @param path Path to the setting that you want to watch, for example "plugins.Unindent.enabled" will fire your callback
|
||||||
|
* whenever Unindent is toggled. Pass an empty string to get notified for all changes
|
||||||
|
* @param onUpdate Callback function whenever a setting matching path is updated. It gets passed the new value and the path
|
||||||
|
* to the updated setting. This path will be the same as your path argument, unless it was an empty string.
|
||||||
|
*
|
||||||
|
* @example addSettingsListener("", (newValue, path) => console.log(`${path} is now ${newValue}`))
|
||||||
|
* addSettingsListener("plugins.Unindent.enabled", v => console.log("Unindent is now", v ? "enabled" : "disabled"))
|
||||||
|
*/
|
||||||
|
export function addSettingsListener<Path extends keyof Settings>(path: Path, onUpdate: (newValue: Settings[Path], path: Path) => void): void;
|
||||||
|
export function addSettingsListener<Path extends string>(path: Path, onUpdate: (newValue: Path extends "" ? any : ResolvePropDeep<Settings, Path>, path: Path extends "" ? string : Path) => void): void;
|
||||||
|
export function addSettingsListener(path: string, onUpdate: (newValue: any, path: string) => void) {
|
||||||
|
if (path) {
|
||||||
|
((onUpdate as SubscriptionCallback)._paths ??= []).push(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriptions.add(onUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function migratePluginSettings(name: string, ...oldNames: string[]) {
|
export function migratePluginSettings(name: string, ...oldNames: string[]) {
|
||||||
const { plugins } = SettingsStore.plain;
|
const { plugins } = settings;
|
||||||
if (name in plugins) return;
|
if (name in plugins) return;
|
||||||
|
|
||||||
for (const oldName of oldNames) {
|
for (const oldName of oldNames) {
|
||||||
|
@ -214,7 +269,7 @@ export function migratePluginSettings(name: string, ...oldNames: string[]) {
|
||||||
logger.info(`Migrating settings from old name ${oldName} to ${name}`);
|
logger.info(`Migrating settings from old name ${oldName} to ${name}`);
|
||||||
plugins[name] = plugins[oldName];
|
plugins[name] = plugins[oldName];
|
||||||
delete plugins[oldName];
|
delete plugins[oldName];
|
||||||
SettingsStore.markAsChanged();
|
VencordNative.settings.set(JSON.stringify(settings, null, 4));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,6 @@ import * as $MessageAccessories from "./MessageAccessories";
|
||||||
import * as $MessageDecorations from "./MessageDecorations";
|
import * as $MessageDecorations from "./MessageDecorations";
|
||||||
import * as $MessageEventsAPI from "./MessageEvents";
|
import * as $MessageEventsAPI from "./MessageEvents";
|
||||||
import * as $MessagePopover from "./MessagePopover";
|
import * as $MessagePopover from "./MessagePopover";
|
||||||
import * as $MessageUpdater from "./MessageUpdater";
|
|
||||||
import * as $Notices from "./Notices";
|
import * as $Notices from "./Notices";
|
||||||
import * as $Notifications from "./Notifications";
|
import * as $Notifications from "./Notifications";
|
||||||
import * as $ServerList from "./ServerList";
|
import * as $ServerList from "./ServerList";
|
||||||
|
@ -111,8 +110,3 @@ export const ContextMenu = $ContextMenu;
|
||||||
* An API allowing you to add buttons to the chat input
|
* An API allowing you to add buttons to the chat input
|
||||||
*/
|
*/
|
||||||
export const ChatButtons = $ChatButtons;
|
export const ChatButtons = $ChatButtons;
|
||||||
|
|
||||||
/**
|
|
||||||
* An API allowing you to update and re-render messages
|
|
||||||
*/
|
|
||||||
export const MessageUpdater = $MessageUpdater;
|
|
||||||
|
|
|
@ -16,12 +16,10 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import "./ExpandableHeader.css";
|
|
||||||
|
|
||||||
import { classNameFactory } from "@api/Styles";
|
import { classNameFactory } from "@api/Styles";
|
||||||
import { Text, Tooltip, useState } from "@webpack/common";
|
import { Text, Tooltip, useState } from "@webpack/common";
|
||||||
|
export const cl = classNameFactory("vc-expandableheader-");
|
||||||
const cl = classNameFactory("vc-expandableheader-");
|
import "./ExpandableHeader.css";
|
||||||
|
|
||||||
export interface ExpandableHeaderProps {
|
export interface ExpandableHeaderProps {
|
||||||
onMoreClick?: () => void;
|
onMoreClick?: () => void;
|
||||||
|
@ -33,7 +31,7 @@ export interface ExpandableHeaderProps {
|
||||||
buttons?: React.ReactNode[];
|
buttons?: React.ReactNode[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ExpandableHeader({ children, onMoreClick, buttons, moreTooltipText, defaultState = false, onDropDownClick, headerText }: ExpandableHeaderProps) {
|
export default function ExpandableHeader({ children, onMoreClick, buttons, moreTooltipText, defaultState = false, onDropDownClick, headerText }: ExpandableHeaderProps) {
|
||||||
const [showContent, setShowContent] = useState(defaultState);
|
const [showContent, setShowContent] = useState(defaultState);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -9,12 +9,10 @@ import "./contributorModal.css";
|
||||||
import { useSettings } from "@api/Settings";
|
import { useSettings } from "@api/Settings";
|
||||||
import { classNameFactory } from "@api/Styles";
|
import { classNameFactory } from "@api/Styles";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Link } from "@components/Link";
|
|
||||||
import { DevsById } from "@utils/constants";
|
import { DevsById } from "@utils/constants";
|
||||||
import { fetchUserProfile, getTheme, Theme } from "@utils/discord";
|
import { fetchUserProfile, getTheme, Theme } from "@utils/discord";
|
||||||
import { pluralise } from "@utils/misc";
|
|
||||||
import { ModalContent, ModalRoot, openModal } from "@utils/modal";
|
import { ModalContent, ModalRoot, openModal } from "@utils/modal";
|
||||||
import { Forms, MaskedLink, showToast, Tooltip, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common";
|
import { Forms, MaskedLink, showToast, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common";
|
||||||
import { User } from "discord-types/general";
|
import { User } from "discord-types/general";
|
||||||
|
|
||||||
import Plugins from "~plugins";
|
import Plugins from "~plugins";
|
||||||
|
@ -74,8 +72,6 @@ function ContributorModal({ user }: { user: User; }) {
|
||||||
.sort((a, b) => Number(a.required ?? false) - Number(b.required ?? false));
|
.sort((a, b) => Number(a.required ?? false) - Number(b.required ?? false));
|
||||||
}, [user.id, user.username]);
|
}, [user.id, user.username]);
|
||||||
|
|
||||||
const ContributedHyperLink = <Link href="https://vencord.dev/source">contributed</Link>;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={cl("header")}>
|
<div className={cl("header")}>
|
||||||
|
@ -88,48 +84,30 @@ function ContributorModal({ user }: { user: User; }) {
|
||||||
|
|
||||||
<div className={cl("links")}>
|
<div className={cl("links")}>
|
||||||
{website && (
|
{website && (
|
||||||
<Tooltip text={website}>
|
<MaskedLink
|
||||||
{props => (
|
href={"https://" + website}
|
||||||
<MaskedLink {...props} href={"https://" + website}>
|
>
|
||||||
<WebsiteIcon />
|
<WebsiteIcon />
|
||||||
</MaskedLink>
|
</MaskedLink>
|
||||||
)}
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
)}
|
||||||
{githubName && (
|
{githubName && (
|
||||||
<Tooltip text={githubName}>
|
<MaskedLink href={`https://github.com/${githubName}`}>
|
||||||
{props => (
|
<GithubIcon />
|
||||||
<MaskedLink {...props} href={`https://github.com/${githubName}`}>
|
</MaskedLink>
|
||||||
<GithubIcon />
|
|
||||||
</MaskedLink>
|
|
||||||
)}
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{plugins.length ? (
|
<div className={cl("plugins")}>
|
||||||
<Forms.FormText>
|
{plugins.map(p =>
|
||||||
This person has {ContributedHyperLink} to {pluralise(plugins.length, "plugin")}!
|
<PluginCard
|
||||||
</Forms.FormText>
|
key={p.name}
|
||||||
) : (
|
plugin={p}
|
||||||
<Forms.FormText>
|
disabled={p.required ?? false}
|
||||||
This person has not made any plugins. They likely {ContributedHyperLink} to Vencord in other ways!
|
onRestartNeeded={() => showToast("Restart to apply changes!")}
|
||||||
</Forms.FormText>
|
/>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
{!!plugins.length && (
|
|
||||||
<div className={cl("plugins")}>
|
|
||||||
{plugins.map(p =>
|
|
||||||
<PluginCard
|
|
||||||
key={p.name}
|
|
||||||
plugin={p}
|
|
||||||
disabled={p.required ?? false}
|
|
||||||
onRestartNeeded={() => showToast("Restart to apply changes!")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,13 +25,11 @@
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 32px;
|
width: 16px;
|
||||||
background: var(--background-tertiary);
|
background: var(--background-tertiary);
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
left: -32px;
|
left: -16px;
|
||||||
top: 0;
|
top: 0;
|
||||||
border-top-left-radius: 9999px;
|
|
||||||
border-bottom-left-radius: 9999px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-author-modal-avatar {
|
.vc-author-modal-avatar {
|
||||||
|
@ -57,5 +55,4 @@
|
||||||
.vc-author-modal-plugins {
|
.vc-author-modal-plugins {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
margin-top: 0.75em;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,6 @@ import PluginModal from "@components/PluginSettings/PluginModal";
|
||||||
import { AddonCard } from "@components/VencordSettings/AddonCard";
|
import { AddonCard } from "@components/VencordSettings/AddonCard";
|
||||||
import { SettingsTab } from "@components/VencordSettings/shared";
|
import { SettingsTab } from "@components/VencordSettings/shared";
|
||||||
import { ChangeList } from "@utils/ChangeList";
|
import { ChangeList } from "@utils/ChangeList";
|
||||||
import { proxyLazy } from "@utils/lazy";
|
|
||||||
import { Logger } from "@utils/Logger";
|
import { Logger } from "@utils/Logger";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { classes, isObjectEmpty } from "@utils/misc";
|
import { classes, isObjectEmpty } from "@utils/misc";
|
||||||
|
@ -39,8 +38,8 @@ import { Alerts, Button, Card, Forms, lodash, Parser, React, Select, Text, TextI
|
||||||
|
|
||||||
import Plugins from "~plugins";
|
import Plugins from "~plugins";
|
||||||
|
|
||||||
// Avoid circular dependency
|
import { startDependenciesRecursive, startPlugin, stopPlugin } from "../../plugins";
|
||||||
const { startDependenciesRecursive, startPlugin, stopPlugin } = proxyLazy(() => require("../../plugins"));
|
|
||||||
|
|
||||||
const cl = classNameFactory("vc-plugins-");
|
const cl = classNameFactory("vc-plugins-");
|
||||||
const logger = new Logger("PluginSettings", "#a6d189");
|
const logger = new Logger("PluginSettings", "#a6d189");
|
||||||
|
@ -261,9 +260,8 @@ export default function PluginSettings() {
|
||||||
plugins = [];
|
plugins = [];
|
||||||
requiredPlugins = [];
|
requiredPlugins = [];
|
||||||
|
|
||||||
const showApi = searchValue.value === "API";
|
|
||||||
for (const p of sortedPlugins) {
|
for (const p of sortedPlugins) {
|
||||||
if (p.hidden || (!p.options && p.name.endsWith("API") && !showApi))
|
if (!p.options && p.name.endsWith("API") && searchValue.value !== "API")
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (!pluginFilter(p)) continue;
|
if (!pluginFilter(p)) continue;
|
||||||
|
@ -317,6 +315,7 @@ export default function PluginSettings() {
|
||||||
<TextInput autoFocus value={searchValue.value} placeholder="Search for a plugin..." onChange={onSearch} className={Margins.bottom20} />
|
<TextInput autoFocus value={searchValue.value} placeholder="Search for a plugin..." onChange={onSearch} className={Margins.bottom20} />
|
||||||
<div className={InputStyles.inputWrapper}>
|
<div className={InputStyles.inputWrapper}>
|
||||||
<Select
|
<Select
|
||||||
|
className={InputStyles.inputDefault}
|
||||||
options={[
|
options={[
|
||||||
{ label: "Show All", value: SearchStatus.ALL, default: true },
|
{ label: "Show All", value: SearchStatus.ALL, default: true },
|
||||||
{ label: "Show Enabled", value: SearchStatus.ENABLED },
|
{ label: "Show Enabled", value: SearchStatus.ENABLED },
|
||||||
|
|
|
@ -21,7 +21,7 @@ import "./addonCard.css";
|
||||||
import { classNameFactory } from "@api/Styles";
|
import { classNameFactory } from "@api/Styles";
|
||||||
import { Badge } from "@components/Badge";
|
import { Badge } from "@components/Badge";
|
||||||
import { Switch } from "@components/Switch";
|
import { Switch } from "@components/Switch";
|
||||||
import { Text, useRef } from "@webpack/common";
|
import { Text } from "@webpack/common";
|
||||||
import type { MouseEventHandler, ReactNode } from "react";
|
import type { MouseEventHandler, ReactNode } from "react";
|
||||||
|
|
||||||
const cl = classNameFactory("vc-addon-");
|
const cl = classNameFactory("vc-addon-");
|
||||||
|
@ -42,8 +42,6 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AddonCard({ disabled, isNew, name, infoButton, footer, author, enabled, setEnabled, description, onMouseEnter, onMouseLeave }: Props) {
|
export function AddonCard({ disabled, isNew, name, infoButton, footer, author, enabled, setEnabled, description, onMouseEnter, onMouseLeave }: Props) {
|
||||||
const titleRef = useRef<HTMLDivElement>(null);
|
|
||||||
const titleContainerRef = useRef<HTMLDivElement>(null);
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cl("card", { "card-disabled": disabled })}
|
className={cl("card", { "card-disabled": disabled })}
|
||||||
|
@ -53,21 +51,7 @@ export function AddonCard({ disabled, isNew, name, infoButton, footer, author, e
|
||||||
<div className={cl("header")}>
|
<div className={cl("header")}>
|
||||||
<div className={cl("name-author")}>
|
<div className={cl("name-author")}>
|
||||||
<Text variant="text-md/bold" className={cl("name")}>
|
<Text variant="text-md/bold" className={cl("name")}>
|
||||||
<div ref={titleContainerRef} className={cl("title-container")}>
|
{name}{isNew && <Badge text="NEW" color="#ED4245" />}
|
||||||
<div
|
|
||||||
ref={titleRef}
|
|
||||||
className={cl("title")}
|
|
||||||
onMouseOver={() => {
|
|
||||||
const title = titleRef.current!;
|
|
||||||
const titleContainer = titleContainerRef.current!;
|
|
||||||
|
|
||||||
title.style.setProperty("--offset", `${titleContainer.clientWidth - title.scrollWidth}px`);
|
|
||||||
title.style.setProperty("--duration", `${Math.max(0.5, (title.scrollWidth - titleContainer.clientWidth) / 7)}s`);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{name}
|
|
||||||
</div>
|
|
||||||
</div>{isNew && <Badge text="NEW" color="#ED4245" />}
|
|
||||||
</Text>
|
</Text>
|
||||||
{!!author && (
|
{!!author && (
|
||||||
<Text variant="text-md/normal" className={cl("author")}>
|
<Text variant="text-md/normal" className={cl("author")}>
|
||||||
|
|
|
@ -16,14 +16,15 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { CheckedTextInput } from "@components/CheckedTextInput";
|
||||||
import { CodeBlock } from "@components/CodeBlock";
|
import { CodeBlock } from "@components/CodeBlock";
|
||||||
import { debounce } from "@shared/debounce";
|
import { debounce } from "@utils/debounce";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
|
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
|
||||||
import { makeCodeblock } from "@utils/text";
|
import { makeCodeblock } from "@utils/text";
|
||||||
import { Patch, ReplaceFn } from "@utils/types";
|
import { ReplaceFn } from "@utils/types";
|
||||||
import { search } from "@webpack";
|
import { search } from "@webpack";
|
||||||
import { Button, Clipboard, Forms, Parser, React, Switch, TextArea, TextInput } from "@webpack/common";
|
import { Button, Clipboard, Forms, Parser, React, Switch, TextInput } from "@webpack/common";
|
||||||
|
|
||||||
import { SettingsTab, wrapTab } from "./shared";
|
import { SettingsTab, wrapTab } from "./shared";
|
||||||
|
|
||||||
|
@ -46,7 +47,7 @@ const findCandidates = debounce(function ({ find, setModule, setError }) {
|
||||||
|
|
||||||
interface ReplacementComponentProps {
|
interface ReplacementComponentProps {
|
||||||
module: [id: number, factory: Function];
|
module: [id: number, factory: Function];
|
||||||
match: string;
|
match: string | RegExp;
|
||||||
replacement: string | ReplaceFn;
|
replacement: string | ReplaceFn;
|
||||||
setReplacementError(error: any): void;
|
setReplacementError(error: any): void;
|
||||||
}
|
}
|
||||||
|
@ -57,13 +58,7 @@ function ReplacementComponent({ module, match, replacement, setReplacementError
|
||||||
|
|
||||||
const [patchedCode, matchResult, diff] = React.useMemo(() => {
|
const [patchedCode, matchResult, diff] = React.useMemo(() => {
|
||||||
const src: string = fact.toString().replaceAll("\n", "");
|
const src: string = fact.toString().replaceAll("\n", "");
|
||||||
|
const canonicalMatch = canonicalizeMatch(match);
|
||||||
try {
|
|
||||||
new RegExp(match);
|
|
||||||
} catch (e) {
|
|
||||||
return ["", [], []];
|
|
||||||
}
|
|
||||||
const canonicalMatch = canonicalizeMatch(new RegExp(match));
|
|
||||||
try {
|
try {
|
||||||
const canonicalReplace = canonicalizeReplace(replacement, "YourPlugin");
|
const canonicalReplace = canonicalizeReplace(replacement, "YourPlugin");
|
||||||
var patched = src.replace(canonicalMatch, canonicalReplace as string);
|
var patched = src.replace(canonicalMatch, canonicalReplace as string);
|
||||||
|
@ -185,8 +180,7 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* FormTitle adds a class if className is not set, so we set it to an empty string to prevent that */}
|
<Forms.FormTitle>replacement</Forms.FormTitle>
|
||||||
<Forms.FormTitle className="">replacement</Forms.FormTitle>
|
|
||||||
<TextInput
|
<TextInput
|
||||||
value={replacement?.toString()}
|
value={replacement?.toString()}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
@ -194,7 +188,7 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
|
||||||
/>
|
/>
|
||||||
{!isFunc && (
|
{!isFunc && (
|
||||||
<div className="vc-text-selectable">
|
<div className="vc-text-selectable">
|
||||||
<Forms.FormTitle className={Margins.top8}>Cheat Sheet</Forms.FormTitle>
|
<Forms.FormTitle>Cheat Sheet</Forms.FormTitle>
|
||||||
{Object.entries({
|
{Object.entries({
|
||||||
"\\i": "Special regex escape sequence that matches identifiers (varnames, classnames, etc.)",
|
"\\i": "Special regex escape sequence that matches identifiers (varnames, classnames, etc.)",
|
||||||
"$$": "Insert a $",
|
"$$": "Insert a $",
|
||||||
|
@ -224,66 +218,8 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FullPatchInputProps {
|
|
||||||
setFind(v: string): void;
|
|
||||||
setParsedFind(v: string | RegExp): void;
|
|
||||||
setMatch(v: string): void;
|
|
||||||
setReplacement(v: string | ReplaceFn): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function FullPatchInput({ setFind, setParsedFind, setMatch, setReplacement }: FullPatchInputProps) {
|
|
||||||
const [fullPatch, setFullPatch] = React.useState<string>("");
|
|
||||||
const [fullPatchError, setFullPatchError] = React.useState<string>("");
|
|
||||||
|
|
||||||
function update() {
|
|
||||||
if (fullPatch === "") {
|
|
||||||
setFullPatchError("");
|
|
||||||
|
|
||||||
setFind("");
|
|
||||||
setParsedFind("");
|
|
||||||
setMatch("");
|
|
||||||
setReplacement("");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
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");
|
|
||||||
|
|
||||||
if (parsed.replacement instanceof Array) {
|
|
||||||
if (parsed.replacement.length === 0) throw new Error("Invalid replacement");
|
|
||||||
|
|
||||||
parsed.replacement = {
|
|
||||||
match: parsed.replacement[0].match,
|
|
||||||
replace: parsed.replacement[0].replace
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!parsed.replacement.match) throw new Error("No 'replacement.match' field");
|
|
||||||
if (!parsed.replacement.replace) throw new Error("No 'replacement.replace' field");
|
|
||||||
|
|
||||||
setFind(parsed.find instanceof RegExp ? parsed.find.toString() : parsed.find);
|
|
||||||
setParsedFind(parsed.find);
|
|
||||||
setMatch(parsed.replacement.match instanceof RegExp ? parsed.replacement.match.source : parsed.replacement.match);
|
|
||||||
setReplacement(parsed.replacement.replace);
|
|
||||||
setFullPatchError("");
|
|
||||||
} catch (e) {
|
|
||||||
setFullPatchError((e as Error).message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return <>
|
|
||||||
<Forms.FormText className={Margins.bottom8}>Paste your full JSON patch here to fill out the fields</Forms.FormText>
|
|
||||||
<TextArea value={fullPatch} onChange={setFullPatch} onBlur={update} />
|
|
||||||
{fullPatchError !== "" && <Forms.FormText style={{ color: "var(--text-danger)" }}>{fullPatchError}</Forms.FormText>}
|
|
||||||
</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function PatchHelper() {
|
function PatchHelper() {
|
||||||
const [find, setFind] = React.useState<string>("");
|
const [find, setFind] = React.useState<string>("");
|
||||||
const [parsedFind, setParsedFind] = React.useState<string | RegExp>("");
|
|
||||||
const [match, setMatch] = React.useState<string>("");
|
const [match, setMatch] = React.useState<string>("");
|
||||||
const [replacement, setReplacement] = React.useState<string | ReplaceFn>("");
|
const [replacement, setReplacement] = React.useState<string | ReplaceFn>("");
|
||||||
|
|
||||||
|
@ -291,60 +227,40 @@ function PatchHelper() {
|
||||||
|
|
||||||
const [module, setModule] = React.useState<[number, Function]>();
|
const [module, setModule] = React.useState<[number, Function]>();
|
||||||
const [findError, setFindError] = React.useState<string>();
|
const [findError, setFindError] = React.useState<string>();
|
||||||
const [matchError, setMatchError] = React.useState<string>();
|
|
||||||
|
|
||||||
const code = React.useMemo(() => {
|
const code = React.useMemo(() => {
|
||||||
return `
|
return `
|
||||||
{
|
{
|
||||||
find: ${parsedFind instanceof RegExp ? parsedFind.toString() : JSON.stringify(parsedFind)},
|
find: ${JSON.stringify(find)},
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /${match.replace(/(?<!\\)\//g, "\\/")}/,
|
match: /${match.replace(/(?<!\\)\//g, "\\/")}/,
|
||||||
replace: ${typeof replacement === "function" ? replacement.toString() : JSON.stringify(replacement)}
|
replace: ${typeof replacement === "function" ? replacement.toString() : JSON.stringify(replacement)}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`.trim();
|
`.trim();
|
||||||
}, [parsedFind, match, replacement]);
|
}, [find, match, replacement]);
|
||||||
|
|
||||||
function onFindChange(v: string) {
|
function onFindChange(v: string) {
|
||||||
|
setFindError(void 0);
|
||||||
setFind(v);
|
setFind(v);
|
||||||
|
if (v.length) {
|
||||||
|
findCandidates({ find: v, setModule, setError: setFindError });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMatchChange(v: string) {
|
||||||
try {
|
try {
|
||||||
let parsedFind = v as string | RegExp;
|
new RegExp(v);
|
||||||
if (/^\/.+?\/$/.test(v)) parsedFind = new RegExp(v.slice(1, -1));
|
|
||||||
|
|
||||||
setFindError(void 0);
|
setFindError(void 0);
|
||||||
setParsedFind(parsedFind);
|
setMatch(v);
|
||||||
|
|
||||||
if (v.length) {
|
|
||||||
findCandidates({ find: parsedFind, setModule, setError: setFindError });
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setFindError((e as Error).message);
|
setFindError((e as Error).message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMatchChange(v: string) {
|
|
||||||
setMatch(v);
|
|
||||||
|
|
||||||
try {
|
|
||||||
new RegExp(v);
|
|
||||||
setMatchError(void 0);
|
|
||||||
} catch (e: any) {
|
|
||||||
setMatchError((e as Error).message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsTab title="Patch Helper">
|
<SettingsTab title="Patch Helper">
|
||||||
<Forms.FormTitle>full patch</Forms.FormTitle>
|
<Forms.FormTitle>find</Forms.FormTitle>
|
||||||
<FullPatchInput
|
|
||||||
setFind={onFindChange}
|
|
||||||
setParsedFind={setParsedFind}
|
|
||||||
setMatch={onMatchChange}
|
|
||||||
setReplacement={setReplacement}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Forms.FormTitle className={Margins.top8}>find</Forms.FormTitle>
|
|
||||||
<TextInput
|
<TextInput
|
||||||
type="text"
|
type="text"
|
||||||
value={find}
|
value={find}
|
||||||
|
@ -352,15 +268,19 @@ function PatchHelper() {
|
||||||
error={findError}
|
error={findError}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Forms.FormTitle className={Margins.top8}>match</Forms.FormTitle>
|
<Forms.FormTitle>match</Forms.FormTitle>
|
||||||
<TextInput
|
<CheckedTextInput
|
||||||
type="text"
|
|
||||||
value={match}
|
value={match}
|
||||||
onChange={onMatchChange}
|
onChange={onMatchChange}
|
||||||
error={matchError}
|
validate={v => {
|
||||||
|
try {
|
||||||
|
return (new RegExp(v), true);
|
||||||
|
} catch (e) {
|
||||||
|
return (e as Error).message;
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={Margins.top8} />
|
|
||||||
<ReplacementInput
|
<ReplacementInput
|
||||||
replacement={replacement}
|
replacement={replacement}
|
||||||
setReplacement={setReplacement}
|
setReplacement={setReplacement}
|
||||||
|
@ -371,7 +291,7 @@ function PatchHelper() {
|
||||||
{module && (
|
{module && (
|
||||||
<ReplacementComponent
|
<ReplacementComponent
|
||||||
module={module}
|
module={module}
|
||||||
match={match}
|
match={new RegExp(match)}
|
||||||
replacement={replacement}
|
replacement={replacement}
|
||||||
setReplacementError={setReplacementError}
|
setReplacementError={setReplacementError}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -22,7 +22,6 @@ import { Flex } from "@components/Flex";
|
||||||
import { DeleteIcon } from "@components/Icons";
|
import { DeleteIcon } from "@components/Icons";
|
||||||
import { Link } from "@components/Link";
|
import { Link } from "@components/Link";
|
||||||
import PluginModal from "@components/PluginSettings/PluginModal";
|
import PluginModal from "@components/PluginSettings/PluginModal";
|
||||||
import type { UserThemeHeader } from "@main/themes";
|
|
||||||
import { openInviteModal } from "@utils/discord";
|
import { openInviteModal } from "@utils/discord";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { classes } from "@utils/misc";
|
import { classes } from "@utils/misc";
|
||||||
|
@ -31,6 +30,7 @@ import { showItemInFolder } from "@utils/native";
|
||||||
import { useAwaiter } from "@utils/react";
|
import { useAwaiter } from "@utils/react";
|
||||||
import { findByPropsLazy, findLazy } from "@webpack";
|
import { findByPropsLazy, findLazy } from "@webpack";
|
||||||
import { Button, Card, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common";
|
import { Button, Card, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common";
|
||||||
|
import { UserThemeHeader } from "main/themes";
|
||||||
import type { ComponentType, Ref, SyntheticEvent } from "react";
|
import type { ComponentType, Ref, SyntheticEvent } from "react";
|
||||||
|
|
||||||
import { AddonCard } from "./AddonCard";
|
import { AddonCard } from "./AddonCard";
|
||||||
|
|
|
@ -22,7 +22,6 @@ import { Flex } from "@components/Flex";
|
||||||
import { Link } from "@components/Link";
|
import { Link } from "@components/Link";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { classes } from "@utils/misc";
|
import { classes } from "@utils/misc";
|
||||||
import { ModalCloseButton, ModalContent, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
|
||||||
import { relaunch } from "@utils/native";
|
import { relaunch } from "@utils/native";
|
||||||
import { useAwaiter } from "@utils/react";
|
import { useAwaiter } from "@utils/react";
|
||||||
import { changes, checkForUpdates, getRepo, isNewer, update, updateError, UpdateLogger } from "@utils/updater";
|
import { changes, checkForUpdates, getRepo, isNewer, update, updateError, UpdateLogger } from "@utils/updater";
|
||||||
|
@ -30,7 +29,7 @@ import { Alerts, Button, Card, Forms, Parser, React, Switch, Toasts } from "@web
|
||||||
|
|
||||||
import gitHash from "~git-hash";
|
import gitHash from "~git-hash";
|
||||||
|
|
||||||
import { handleSettingsTabError, SettingsTab, wrapTab } from "./shared";
|
import { SettingsTab, wrapTab } from "./shared";
|
||||||
|
|
||||||
function withDispatcher(dispatcher: React.Dispatch<React.SetStateAction<boolean>>, action: () => any) {
|
function withDispatcher(dispatcher: React.Dispatch<React.SetStateAction<boolean>>, action: () => any) {
|
||||||
return async () => {
|
return async () => {
|
||||||
|
@ -39,24 +38,21 @@ function withDispatcher(dispatcher: React.Dispatch<React.SetStateAction<boolean>
|
||||||
await action();
|
await action();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
UpdateLogger.error("Failed to update", e);
|
UpdateLogger.error("Failed to update", e);
|
||||||
|
|
||||||
let err: string;
|
|
||||||
if (!e) {
|
if (!e) {
|
||||||
err = "An unknown error occurred (error is undefined).\nPlease try again.";
|
var err = "An unknown error occurred (error is undefined).\nPlease try again.";
|
||||||
} else if (e.code && e.cmd) {
|
} else if (e.code && e.cmd) {
|
||||||
const { code, path, cmd, stderr } = e;
|
const { code, path, cmd, stderr } = e;
|
||||||
|
|
||||||
if (code === "ENOENT")
|
if (code === "ENOENT")
|
||||||
err = `Command \`${path}\` not found.\nPlease install it and try again`;
|
var err = `Command \`${path}\` not found.\nPlease install it and try again`;
|
||||||
else {
|
else {
|
||||||
err = `An error occurred while running \`${cmd}\`:\n`;
|
var err = `An error occurred while running \`${cmd}\`:\n`;
|
||||||
err += stderr || `Code \`${code}\`. See the console for more info`;
|
err += stderr || `Code \`${code}\`. See the console for more info`;
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
err = "An unknown error occurred. See the console for more info.";
|
var err = "An unknown error occurred. See the console for more info.";
|
||||||
}
|
}
|
||||||
|
|
||||||
Alerts.show({
|
Alerts.show({
|
||||||
title: "Oops!",
|
title: "Oops!",
|
||||||
body: (
|
body: (
|
||||||
|
@ -190,7 +186,7 @@ function Newer(props: CommonProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function Updater() {
|
function Updater() {
|
||||||
const settings = useSettings(["autoUpdate", "autoUpdateNotification"]);
|
const settings = useSettings(["notifyAboutUpdates", "autoUpdate", "autoUpdateNotification"]);
|
||||||
|
|
||||||
const [repo, err, repoPending] = useAwaiter(getRepo, { fallbackValue: "Loading..." });
|
const [repo, err, repoPending] = useAwaiter(getRepo, { fallbackValue: "Loading..." });
|
||||||
|
|
||||||
|
@ -207,6 +203,14 @@ function Updater() {
|
||||||
return (
|
return (
|
||||||
<SettingsTab title="Vencord Updater">
|
<SettingsTab title="Vencord Updater">
|
||||||
<Forms.FormTitle tag="h5">Updater Settings</Forms.FormTitle>
|
<Forms.FormTitle tag="h5">Updater Settings</Forms.FormTitle>
|
||||||
|
<Switch
|
||||||
|
value={settings.notifyAboutUpdates}
|
||||||
|
onChange={(v: boolean) => settings.notifyAboutUpdates = v}
|
||||||
|
note="Shows a notification on startup"
|
||||||
|
disabled={settings.autoUpdate}
|
||||||
|
>
|
||||||
|
Get notified about new updates
|
||||||
|
</Switch>
|
||||||
<Switch
|
<Switch
|
||||||
value={settings.autoUpdate}
|
value={settings.autoUpdate}
|
||||||
onChange={(v: boolean) => settings.autoUpdate = v}
|
onChange={(v: boolean) => settings.autoUpdate = v}
|
||||||
|
@ -249,20 +253,3 @@ function Updater() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default IS_UPDATER_DISABLED ? null : wrapTab(Updater, "Updater");
|
export default IS_UPDATER_DISABLED ? null : wrapTab(Updater, "Updater");
|
||||||
|
|
||||||
export const openUpdaterModal = IS_UPDATER_DISABLED ? null : function () {
|
|
||||||
const UpdaterTab = wrapTab(Updater, "Updater");
|
|
||||||
|
|
||||||
try {
|
|
||||||
openModal(wrapTab((modalProps: ModalProps) => (
|
|
||||||
<ModalRoot {...modalProps} size={ModalSize.MEDIUM}>
|
|
||||||
<ModalContent className="vc-updater-modal">
|
|
||||||
<ModalCloseButton onClick={modalProps.onClose} className="vc-updater-modal-close-button" />
|
|
||||||
<UpdaterTab />
|
|
||||||
</ModalContent>
|
|
||||||
</ModalRoot>
|
|
||||||
), "UpdaterModal"));
|
|
||||||
} catch {
|
|
||||||
handleSettingsTabError();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
|
@ -50,6 +50,14 @@ function VencordSettings() {
|
||||||
const isMac = navigator.platform.toLowerCase().startsWith("mac");
|
const isMac = navigator.platform.toLowerCase().startsWith("mac");
|
||||||
const needsVibrancySettings = IS_DISCORD_DESKTOP && isMac;
|
const needsVibrancySettings = IS_DISCORD_DESKTOP && isMac;
|
||||||
|
|
||||||
|
// One-time migration of the old setting to the new one if necessary.
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (settings.macosTranslucency === true && !settings.macosVibrancyStyle) {
|
||||||
|
settings.macosVibrancyStyle = "sidebar";
|
||||||
|
settings.macosTranslucency = undefined;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const Switches: Array<false | {
|
const Switches: Array<false | {
|
||||||
key: KeysOfType<typeof settings, boolean>;
|
key: KeysOfType<typeof settings, boolean>;
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -156,7 +164,7 @@ function VencordSettings() {
|
||||||
options={[
|
options={[
|
||||||
// Sorted from most opaque to most transparent
|
// Sorted from most opaque to most transparent
|
||||||
{
|
{
|
||||||
label: "No vibrancy", value: undefined
|
label: "No vibrancy", default: !settings.macosTranslucency, value: undefined
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Under Page (window tinting)",
|
label: "Under Page (window tinting)",
|
||||||
|
@ -183,8 +191,9 @@ function VencordSettings() {
|
||||||
value: "header"
|
value: "header"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Sidebar",
|
label: "Sidebar (old value for transparent windows)",
|
||||||
value: "sidebar"
|
value: "sidebar",
|
||||||
|
default: settings.macosTranslucency
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Tooltip",
|
label: "Tooltip",
|
||||||
|
|
|
@ -62,36 +62,3 @@
|
||||||
.vc-addon-author::before {
|
.vc-addon-author::before {
|
||||||
content: "by ";
|
content: "by ";
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-addon-title-container {
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
height: 1.25em;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-addon-title {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes vc-addon-title {
|
|
||||||
0% {
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
50% {
|
|
||||||
transform: translateX(var(--offset));
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-addon-title:hover {
|
|
||||||
overflow: visible;
|
|
||||||
animation: vc-addon-title var(--duration) linear infinite;
|
|
||||||
}
|
|
||||||
|
|
|
@ -65,11 +65,3 @@
|
||||||
/* discord also sets cursor: default which prevents the cursor from showing as text */
|
/* discord also sets cursor: default which prevents the cursor from showing as text */
|
||||||
cursor: initial;
|
cursor: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-updater-modal {
|
|
||||||
padding: 1.5em !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-updater-modal-close-button {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
|
@ -42,11 +42,11 @@ export function SettingsTab({ title, children }: PropsWithChildren<{ title: stri
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const handleSettingsTabError = onlyOnce(handleComponentFailed);
|
const onError = onlyOnce(handleComponentFailed);
|
||||||
|
|
||||||
export function wrapTab(component: ComponentType<any>, tab: string) {
|
export function wrapTab(component: ComponentType, tab: string) {
|
||||||
return ErrorBoundary.wrap(component, {
|
return ErrorBoundary.wrap(component, {
|
||||||
message: `Failed to render the ${tab} tab. If this issue persists, try using the installer to reinstall!`,
|
message: `Failed to render the ${tab} tab. If this issue persists, try using the installer to reinstall!`,
|
||||||
onError: handleSettingsTabError,
|
onError,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
/*
|
|
||||||
* Vencord, a Discord client mod
|
|
||||||
* Copyright (c) 2024 Vendicated and contributors
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
export * from "./Badge";
|
|
||||||
export * from "./CheckedTextInput";
|
|
||||||
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";
|
|
||||||
export * from "./Link";
|
|
||||||
export * from "./Switch";
|
|
|
@ -18,14 +18,14 @@
|
||||||
|
|
||||||
import { Logger } from "@utils/Logger";
|
import { Logger } from "@utils/Logger";
|
||||||
|
|
||||||
if (IS_DEV || IS_REPORTER) {
|
if (IS_DEV) {
|
||||||
var traces = {} as Record<string, [number, any[]]>;
|
var traces = {} as Record<string, [number, any[]]>;
|
||||||
var logger = new Logger("Tracer", "#FFD166");
|
var logger = new Logger("Tracer", "#FFD166");
|
||||||
}
|
}
|
||||||
|
|
||||||
const noop = function () { };
|
const noop = function () { };
|
||||||
|
|
||||||
export const beginTrace = !(IS_DEV || IS_REPORTER) ? noop :
|
export const beginTrace = !IS_DEV ? noop :
|
||||||
function beginTrace(name: string, ...args: any[]) {
|
function beginTrace(name: string, ...args: any[]) {
|
||||||
if (name in traces)
|
if (name in traces)
|
||||||
throw new Error(`Trace ${name} already exists!`);
|
throw new Error(`Trace ${name} already exists!`);
|
||||||
|
@ -33,7 +33,7 @@ export const beginTrace = !(IS_DEV || IS_REPORTER) ? noop :
|
||||||
traces[name] = [performance.now(), args];
|
traces[name] = [performance.now(), args];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const finishTrace = !(IS_DEV || IS_REPORTER) ? noop : function finishTrace(name: string) {
|
export const finishTrace = !IS_DEV ? noop : function finishTrace(name: string) {
|
||||||
const end = performance.now();
|
const end = performance.now();
|
||||||
|
|
||||||
const [start, args] = traces[name];
|
const [start, args] = traces[name];
|
||||||
|
@ -48,7 +48,7 @@ type TraceNameMapper<F extends Func> = (...args: Parameters<F>) => string;
|
||||||
const noopTracer =
|
const noopTracer =
|
||||||
<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>) => f;
|
<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>) => f;
|
||||||
|
|
||||||
export const traceFunction = !(IS_DEV || IS_REPORTER)
|
export const traceFunction = !IS_DEV
|
||||||
? noopTracer
|
? noopTracer
|
||||||
: function traceFunction<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>): F {
|
: function traceFunction<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>): F {
|
||||||
return function (this: any, ...args: Parameters<F>) {
|
return function (this: any, ...args: Parameters<F>) {
|
||||||
|
|
|
@ -1,167 +0,0 @@
|
||||||
/*
|
|
||||||
* Vencord, a Discord client mod
|
|
||||||
* Copyright (c) 2024 Vendicated and contributors
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Logger } from "@utils/Logger";
|
|
||||||
import { canonicalizeMatch } from "@utils/patches";
|
|
||||||
import * as Webpack from "@webpack";
|
|
||||||
import { wreq } from "@webpack";
|
|
||||||
|
|
||||||
const LazyChunkLoaderLogger = new Logger("LazyChunkLoader");
|
|
||||||
|
|
||||||
export async function loadLazyChunks() {
|
|
||||||
try {
|
|
||||||
LazyChunkLoaderLogger.log("Loading all chunks...");
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
// True if resolved, false otherwise
|
|
||||||
const chunksSearchPromises = [] as Array<() => boolean>;
|
|
||||||
|
|
||||||
const LazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?(\i\.e\("[^)]+?"\)[^\]]*?)(?:\]\))?)\.then\(\i\.bind\(\i,"([^)]+?)"\)\)/g);
|
|
||||||
|
|
||||||
async function searchAndLoadLazyChunks(factoryCode: string) {
|
|
||||||
const lazyChunks = factoryCode.matchAll(LazyChunkRegex);
|
|
||||||
const validChunkGroups = new Set<[chunkIds: string[], entryPoint: string]>();
|
|
||||||
|
|
||||||
// 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 => m[1]) : [];
|
|
||||||
|
|
||||||
if (chunkIds.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let invalidChunkGroup = false;
|
|
||||||
|
|
||||||
for (const id of chunkIds) {
|
|
||||||
if (wreq.u(id) == null || wreq.u(id) === "undefined.js") continue;
|
|
||||||
|
|
||||||
const isWasm = await fetch(wreq.p + wreq.u(id))
|
|
||||||
.then(r => r.text())
|
|
||||||
.then(t => (IS_WEB && t.includes(".module.wasm")) || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push"));
|
|
||||||
|
|
||||||
if (isWasm && IS_WEB) {
|
|
||||||
invalidChunks.add(id);
|
|
||||||
invalidChunkGroup = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
validChunks.add(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!invalidChunkGroup) {
|
|
||||||
validChunkGroups.add([chunkIds, entryPoint]);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Loads all found valid chunk groups
|
|
||||||
await Promise.all(
|
|
||||||
Array.from(validChunkGroups)
|
|
||||||
.map(([chunkIds]) =>
|
|
||||||
Promise.all(chunkIds.map(id => wreq.e(id as any).catch(() => { })))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Requires the entry points for all valid chunk groups
|
|
||||||
for (const [, entryPoint] of validChunkGroups) {
|
|
||||||
try {
|
|
||||||
if (shouldForceDefer) {
|
|
||||||
deferredRequires.add(entryPoint);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wreq.m[entryPoint]) wreq(entryPoint as any);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// setImmediate to only check if all chunks were loaded after this function resolves
|
|
||||||
// We check if all chunks were loaded every time a factory is loaded
|
|
||||||
// If we are still looking for chunks in the other factories, the array will have that factory's chunk search promise not resolved
|
|
||||||
// But, if all chunk search promises are resolved, this means we found every lazy chunk loaded by Discord code and manually loaded them
|
|
||||||
setTimeout(() => {
|
|
||||||
let allResolved = true;
|
|
||||||
|
|
||||||
for (let i = 0; i < chunksSearchPromises.length; i++) {
|
|
||||||
const isResolved = chunksSearchPromises[i]();
|
|
||||||
|
|
||||||
if (isResolved) {
|
|
||||||
// Remove finished promises to avoid having to iterate through a huge array everytime
|
|
||||||
chunksSearchPromises.splice(i--, 1);
|
|
||||||
} else {
|
|
||||||
allResolved = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allResolved) chunksSearchingResolve();
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
Webpack.factoryListeners.add(factory => {
|
|
||||||
let isResolved = false;
|
|
||||||
searchAndLoadLazyChunks(factory.toString()).then(() => isResolved = true);
|
|
||||||
|
|
||||||
chunksSearchPromises.push(() => isResolved);
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const factoryId in wreq.m) {
|
|
||||||
let isResolved = false;
|
|
||||||
searchAndLoadLazyChunks(wreq.m[factoryId].toString()).then(() => isResolved = true);
|
|
||||||
|
|
||||||
chunksSearchPromises.push(() => isResolved);
|
|
||||||
}
|
|
||||||
|
|
||||||
await chunksSearchingDone;
|
|
||||||
|
|
||||||
// Require deferred entry points
|
|
||||||
for (const deferredRequire of deferredRequires) {
|
|
||||||
wreq!(deferredRequire as any);
|
|
||||||
}
|
|
||||||
|
|
||||||
// All chunks Discord has mapped to asset files, even if they are not used anymore
|
|
||||||
const allChunks = [] as string[];
|
|
||||||
|
|
||||||
// Matches "id" or id:
|
|
||||||
for (const currentMatch of wreq!.u.toString().matchAll(/(?:"(\d+?)")|(?:(\d+?):)/g)) {
|
|
||||||
const id = currentMatch[1] ?? currentMatch[2];
|
|
||||||
if (id == null) continue;
|
|
||||||
|
|
||||||
allChunks.push(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allChunks.length === 0) throw new Error("Failed to get all chunks");
|
|
||||||
|
|
||||||
// Chunks that are not loaded (not used) by Discord code anymore
|
|
||||||
const chunksLeft = allChunks.filter(id => {
|
|
||||||
return !(validChunks.has(id) || invalidChunks.has(id));
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all(chunksLeft.map(async id => {
|
|
||||||
const isWasm = await fetch(wreq.p + wreq.u(id))
|
|
||||||
.then(r => r.text())
|
|
||||||
.then(t => (IS_WEB && t.includes(".module.wasm")) || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push"));
|
|
||||||
|
|
||||||
// Loads and requires a chunk
|
|
||||||
if (!isWasm) {
|
|
||||||
await wreq.e(id as any);
|
|
||||||
if (wreq.m[id]) wreq(id as any);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
LazyChunkLoaderLogger.log("Finished loading all chunks!");
|
|
||||||
} catch (e) {
|
|
||||||
LazyChunkLoaderLogger.log("A fatal error occurred:", e);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,75 +0,0 @@
|
||||||
/*
|
|
||||||
* Vencord, a Discord client mod
|
|
||||||
* Copyright (c) 2024 Vendicated and contributors
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Logger } from "@utils/Logger";
|
|
||||||
import * as Webpack from "@webpack";
|
|
||||||
import { patches } from "plugins";
|
|
||||||
|
|
||||||
import { loadLazyChunks } from "./loadLazyChunks";
|
|
||||||
|
|
||||||
const ReporterLogger = new Logger("Reporter");
|
|
||||||
|
|
||||||
async function runReporter() {
|
|
||||||
try {
|
|
||||||
ReporterLogger.log("Starting test...");
|
|
||||||
|
|
||||||
let loadLazyChunksResolve: (value: void | PromiseLike<void>) => void;
|
|
||||||
const loadLazyChunksDone = new Promise<void>(r => loadLazyChunksResolve = r);
|
|
||||||
|
|
||||||
Webpack.beforeInitListeners.add(() => loadLazyChunks().then((loadLazyChunksResolve)));
|
|
||||||
await loadLazyChunksDone;
|
|
||||||
|
|
||||||
for (const patch of patches) {
|
|
||||||
if (!patch.all) {
|
|
||||||
new Logger("WebpackInterceptor").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [searchType, args] of Webpack.lazyWebpackSearchHistory) {
|
|
||||||
let method = searchType;
|
|
||||||
|
|
||||||
if (searchType === "findComponent") method = "find";
|
|
||||||
if (searchType === "findExportedComponent") method = "findByProps";
|
|
||||||
if (searchType === "waitFor" || searchType === "waitForComponent") {
|
|
||||||
if (typeof args[0] === "string") method = "findByProps";
|
|
||||||
else method = "find";
|
|
||||||
}
|
|
||||||
if (searchType === "waitForStore") method = "findStore";
|
|
||||||
|
|
||||||
try {
|
|
||||||
let result: any;
|
|
||||||
|
|
||||||
if (method === "proxyLazyWebpack" || method === "LazyComponentWebpack") {
|
|
||||||
const [factory] = args;
|
|
||||||
result = factory();
|
|
||||||
} else if (method === "extractAndLoadChunks") {
|
|
||||||
const [code, matcher] = args;
|
|
||||||
|
|
||||||
result = await Webpack.extractAndLoadChunks(code, matcher);
|
|
||||||
if (result === false) result = null;
|
|
||||||
} else {
|
|
||||||
// @ts-ignore
|
|
||||||
result = Webpack[method](...args);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result == null || (result.$$vencordInternal != null && result.$$vencordInternal() == null)) throw "a rock at ben shapiro";
|
|
||||||
} catch (e) {
|
|
||||||
let logMessage = searchType;
|
|
||||||
if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") logMessage += `(${args[0].toString().slice(0, 147)}...)`;
|
|
||||||
else if (method === "extractAndLoadChunks") logMessage += `([${args[0].map(arg => `"${arg}"`).join(", ")}], ${args[1].toString()})`;
|
|
||||||
else logMessage += `(${args.map(arg => `"${arg}"`).join(", ")})`;
|
|
||||||
|
|
||||||
ReporterLogger.log("Webpack Find Fail:", logMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ReporterLogger.log("Finished test");
|
|
||||||
} catch (e) {
|
|
||||||
ReporterLogger.log("A fatal error occurred:", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
runReporter();
|
|
3
src/globals.d.ts
vendored
3
src/globals.d.ts
vendored
|
@ -34,10 +34,9 @@ declare global {
|
||||||
*/
|
*/
|
||||||
export var IS_WEB: boolean;
|
export var IS_WEB: boolean;
|
||||||
export var IS_EXTENSION: boolean;
|
export var IS_EXTENSION: boolean;
|
||||||
|
export var IS_DEV: boolean;
|
||||||
export var IS_STANDALONE: boolean;
|
export var IS_STANDALONE: boolean;
|
||||||
export var IS_UPDATER_DISABLED: boolean;
|
export var IS_UPDATER_DISABLED: boolean;
|
||||||
export var IS_DEV: boolean;
|
|
||||||
export var IS_REPORTER: boolean;
|
|
||||||
export var IS_DISCORD_DESKTOP: boolean;
|
export var IS_DISCORD_DESKTOP: boolean;
|
||||||
export var IS_VESKTOP: boolean;
|
export var IS_VESKTOP: boolean;
|
||||||
export var VERSION: string;
|
export var VERSION: string;
|
||||||
|
|
|
@ -19,8 +19,7 @@
|
||||||
import { app, protocol, session } from "electron";
|
import { app, protocol, session } from "electron";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
|
|
||||||
import { ensureSafePath } from "./ipcMain";
|
import { ensureSafePath, getSettings } from "./ipcMain";
|
||||||
import { RendererSettings } from "./settings";
|
|
||||||
import { IS_VANILLA, THEMES_DIR } from "./utils/constants";
|
import { IS_VANILLA, THEMES_DIR } from "./utils/constants";
|
||||||
import { installExt } from "./utils/extensions";
|
import { installExt } from "./utils/extensions";
|
||||||
|
|
||||||
|
@ -56,7 +55,7 @@ if (IS_VESKTOP || !IS_VANILLA) {
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (RendererSettings.store.enableReactDevtools)
|
if (getSettings().enableReactDevtools)
|
||||||
installExt("fmkadmapgofadopljbjfkapdkoienihi")
|
installExt("fmkadmapgofadopljbjfkapdkoienihi")
|
||||||
.then(() => console.info("[Vencord] Installed React Developer Tools"))
|
.then(() => console.info("[Vencord] Installed React Developer Tools"))
|
||||||
.catch(err => console.error("[Vencord] Failed to install React Developer Tools", err));
|
.catch(err => console.error("[Vencord] Failed to install React Developer Tools", err));
|
||||||
|
|
|
@ -18,20 +18,22 @@
|
||||||
|
|
||||||
import "./updater";
|
import "./updater";
|
||||||
import "./ipcPlugins";
|
import "./ipcPlugins";
|
||||||
import "./settings";
|
|
||||||
|
|
||||||
import { debounce } from "@shared/debounce";
|
import { debounce } from "@utils/debounce";
|
||||||
import { IpcEvents } from "@shared/IpcEvents";
|
import { IpcEvents } from "@utils/IpcEvents";
|
||||||
|
import { Queue } from "@utils/Queue";
|
||||||
import { BrowserWindow, ipcMain, shell, systemPreferences } from "electron";
|
import { BrowserWindow, ipcMain, shell, systemPreferences } from "electron";
|
||||||
import monacoHtml from "file://monacoWin.html?minify&base64";
|
import { FSWatcher, mkdirSync, readFileSync, watch } from "fs";
|
||||||
import { FSWatcher, mkdirSync, watch, writeFileSync } from "fs";
|
import { open, readdir, readFile, writeFile } from "fs/promises";
|
||||||
import { open, readdir, readFile } from "fs/promises";
|
|
||||||
import { join, normalize } from "path";
|
import { join, normalize } from "path";
|
||||||
|
|
||||||
|
import monacoHtml from "~fileContent/monacoWin.html;base64";
|
||||||
|
|
||||||
import { getThemeInfo, stripBOM, UserThemeHeader } from "./themes";
|
import { getThemeInfo, stripBOM, UserThemeHeader } from "./themes";
|
||||||
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, THEMES_DIR } from "./utils/constants";
|
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, SETTINGS_FILE, THEMES_DIR } from "./utils/constants";
|
||||||
import { makeLinksOpenExternally } from "./utils/externalLinks";
|
import { makeLinksOpenExternally } from "./utils/externalLinks";
|
||||||
|
|
||||||
|
mkdirSync(SETTINGS_DIR, { recursive: true });
|
||||||
mkdirSync(THEMES_DIR, { recursive: true });
|
mkdirSync(THEMES_DIR, { recursive: true });
|
||||||
|
|
||||||
export function ensureSafePath(basePath: string, path: string) {
|
export function ensureSafePath(basePath: string, path: string) {
|
||||||
|
@ -69,6 +71,22 @@ function getThemeData(fileName: string) {
|
||||||
return readFile(safePath, "utf-8");
|
return readFile(safePath, "utf-8");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function readSettings() {
|
||||||
|
try {
|
||||||
|
return readFileSync(SETTINGS_FILE, "utf-8");
|
||||||
|
} catch {
|
||||||
|
return "{}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSettings(): typeof import("@api/Settings").Settings {
|
||||||
|
try {
|
||||||
|
return JSON.parse(readSettings());
|
||||||
|
} catch {
|
||||||
|
return {} as any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ipcMain.handle(IpcEvents.OPEN_QUICKCSS, () => shell.openPath(QUICKCSS_PATH));
|
ipcMain.handle(IpcEvents.OPEN_QUICKCSS, () => shell.openPath(QUICKCSS_PATH));
|
||||||
|
|
||||||
ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => {
|
ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => {
|
||||||
|
@ -83,10 +101,12 @@ ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => {
|
||||||
shell.openExternal(url);
|
shell.openExternal(url);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const cssWriteQueue = new Queue();
|
||||||
|
const settingsWriteQueue = new Queue();
|
||||||
|
|
||||||
ipcMain.handle(IpcEvents.GET_QUICK_CSS, () => readCss());
|
ipcMain.handle(IpcEvents.GET_QUICK_CSS, () => readCss());
|
||||||
ipcMain.handle(IpcEvents.SET_QUICK_CSS, (_, css) =>
|
ipcMain.handle(IpcEvents.SET_QUICK_CSS, (_, css) =>
|
||||||
writeFileSync(QUICKCSS_PATH, css)
|
cssWriteQueue.push(() => writeFile(QUICKCSS_PATH, css))
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle(IpcEvents.GET_THEMES_DIR, () => THEMES_DIR);
|
ipcMain.handle(IpcEvents.GET_THEMES_DIR, () => THEMES_DIR);
|
||||||
|
@ -97,6 +117,13 @@ ipcMain.handle(IpcEvents.GET_THEME_SYSTEM_VALUES, () => ({
|
||||||
"os-accent-color": `#${systemPreferences.getAccentColor?.() || ""}`
|
"os-accent-color": `#${systemPreferences.getAccentColor?.() || ""}`
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
ipcMain.handle(IpcEvents.GET_SETTINGS_DIR, () => SETTINGS_DIR);
|
||||||
|
ipcMain.on(IpcEvents.GET_SETTINGS, e => e.returnValue = readSettings());
|
||||||
|
|
||||||
|
ipcMain.handle(IpcEvents.SET_SETTINGS, (_, s) => {
|
||||||
|
settingsWriteQueue.push(() => writeFile(SETTINGS_FILE, s));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
export function initIpc(mainWindow: BrowserWindow) {
|
export function initIpc(mainWindow: BrowserWindow) {
|
||||||
let quickCssWatcher: FSWatcher | undefined;
|
let quickCssWatcher: FSWatcher | undefined;
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IpcEvents } from "@shared/IpcEvents";
|
import { IpcEvents } from "@utils/IpcEvents";
|
||||||
import { ipcMain } from "electron";
|
import { ipcMain } from "electron";
|
||||||
|
|
||||||
import PluginNatives from "~pluginNatives";
|
import PluginNatives from "~pluginNatives";
|
||||||
|
|
|
@ -16,12 +16,11 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { onceDefined } from "@shared/onceDefined";
|
import { onceDefined } from "@utils/onceDefined";
|
||||||
import electron, { app, BrowserWindowConstructorOptions, Menu } from "electron";
|
import electron, { app, BrowserWindowConstructorOptions, Menu } from "electron";
|
||||||
import { dirname, join } from "path";
|
import { dirname, join } from "path";
|
||||||
|
|
||||||
import { initIpc } from "./ipcMain";
|
import { getSettings, initIpc } from "./ipcMain";
|
||||||
import { RendererSettings } from "./settings";
|
|
||||||
import { IS_VANILLA } from "./utils/constants";
|
import { IS_VANILLA } from "./utils/constants";
|
||||||
|
|
||||||
console.log("[Vencord] Starting up...");
|
console.log("[Vencord] Starting up...");
|
||||||
|
@ -42,7 +41,8 @@ require.main!.filename = join(asarPath, discordPkg.main);
|
||||||
app.setAppPath(asarPath);
|
app.setAppPath(asarPath);
|
||||||
|
|
||||||
if (!IS_VANILLA) {
|
if (!IS_VANILLA) {
|
||||||
const settings = RendererSettings.store;
|
const settings = getSettings();
|
||||||
|
|
||||||
// Repatch after host updates on Windows
|
// Repatch after host updates on Windows
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
require("./patchWin32Updater");
|
require("./patchWin32Updater");
|
||||||
|
@ -73,9 +73,6 @@ if (!IS_VANILLA) {
|
||||||
const original = options.webPreferences.preload;
|
const original = options.webPreferences.preload;
|
||||||
options.webPreferences.preload = join(__dirname, IS_DISCORD_DESKTOP ? "preload.js" : "vencordDesktopPreload.js");
|
options.webPreferences.preload = join(__dirname, IS_DISCORD_DESKTOP ? "preload.js" : "vencordDesktopPreload.js");
|
||||||
options.webPreferences.sandbox = false;
|
options.webPreferences.sandbox = false;
|
||||||
// work around discord unloading when in background
|
|
||||||
options.webPreferences.backgroundThrottling = false;
|
|
||||||
|
|
||||||
if (settings.frameless) {
|
if (settings.frameless) {
|
||||||
options.frame = false;
|
options.frame = false;
|
||||||
} else if (process.platform === "win32" && settings.winNativeTitleBar) {
|
} else if (process.platform === "win32" && settings.winNativeTitleBar) {
|
||||||
|
@ -87,11 +84,13 @@ if (!IS_VANILLA) {
|
||||||
options.backgroundColor = "#00000000";
|
options.backgroundColor = "#00000000";
|
||||||
}
|
}
|
||||||
|
|
||||||
const needsVibrancy = process.platform === "darwin" && settings.macosVibrancyStyle;
|
const needsVibrancy = process.platform === "darwin" || (settings.macosVibrancyStyle || settings.macosTranslucency);
|
||||||
|
|
||||||
if (needsVibrancy) {
|
if (needsVibrancy) {
|
||||||
options.backgroundColor = "#00000000";
|
options.backgroundColor = "#00000000";
|
||||||
if (settings.macosVibrancyStyle) {
|
if (settings.macosTranslucency) {
|
||||||
|
options.vibrancy = "sidebar";
|
||||||
|
} else if (settings.macosVibrancyStyle) {
|
||||||
options.vibrancy = settings.macosVibrancyStyle;
|
options.vibrancy = settings.macosVibrancyStyle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -139,15 +138,6 @@ if (!IS_VANILLA) {
|
||||||
}
|
}
|
||||||
return originalAppend.apply(this, args);
|
return originalAppend.apply(this, args);
|
||||||
};
|
};
|
||||||
|
|
||||||
// disable renderer backgrounding to prevent the app from unloading when in the background
|
|
||||||
// https://github.com/electron/electron/issues/2822
|
|
||||||
// https://github.com/GoogleChrome/chrome-launcher/blob/5a27dd574d47a75fec0fb50f7b774ebf8a9791ba/docs/chrome-flags-for-tools.md#task-throttling
|
|
||||||
// Work around discord unloading when in background
|
|
||||||
// Discord also recently started adding these flags but only on windows for some reason dunno why, it happens on Linux too
|
|
||||||
app.commandLine.appendSwitch("disable-renderer-backgrounding");
|
|
||||||
app.commandLine.appendSwitch("disable-background-timer-throttling");
|
|
||||||
app.commandLine.appendSwitch("disable-backgrounding-occluded-windows");
|
|
||||||
} else {
|
} else {
|
||||||
console.log("[Vencord] Running in vanilla mode. Not loading Vencord");
|
console.log("[Vencord] Running in vanilla mode. Not loading Vencord");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,69 +0,0 @@
|
||||||
/*
|
|
||||||
* Vencord, a Discord client mod
|
|
||||||
* Copyright (c) 2024 Vendicated and contributors
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { Settings } from "@api/Settings";
|
|
||||||
import { IpcEvents } from "@shared/IpcEvents";
|
|
||||||
import { SettingsStore } from "@shared/SettingsStore";
|
|
||||||
import { mergeDefaults } from "@utils/mergeDefaults";
|
|
||||||
import { ipcMain } from "electron";
|
|
||||||
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
||||||
|
|
||||||
import { NATIVE_SETTINGS_FILE, SETTINGS_DIR, SETTINGS_FILE } from "./utils/constants";
|
|
||||||
|
|
||||||
mkdirSync(SETTINGS_DIR, { recursive: true });
|
|
||||||
|
|
||||||
function readSettings<T = object>(name: string, file: string): Partial<T> {
|
|
||||||
try {
|
|
||||||
return JSON.parse(readFileSync(file, "utf-8"));
|
|
||||||
} catch (err: any) {
|
|
||||||
if (err?.code !== "ENOENT")
|
|
||||||
console.error(`Failed to read ${name} settings`, err);
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const RendererSettings = new SettingsStore(readSettings<Settings>("renderer", SETTINGS_FILE));
|
|
||||||
|
|
||||||
RendererSettings.addGlobalChangeListener(() => {
|
|
||||||
try {
|
|
||||||
writeFileSync(SETTINGS_FILE, JSON.stringify(RendererSettings.plain, null, 4));
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to write renderer settings", e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle(IpcEvents.GET_SETTINGS_DIR, () => SETTINGS_DIR);
|
|
||||||
ipcMain.on(IpcEvents.GET_SETTINGS, e => e.returnValue = RendererSettings.plain);
|
|
||||||
|
|
||||||
ipcMain.handle(IpcEvents.SET_SETTINGS, (_, data: Settings, pathToNotify?: string) => {
|
|
||||||
RendererSettings.setData(data, pathToNotify);
|
|
||||||
});
|
|
||||||
|
|
||||||
export interface NativeSettings {
|
|
||||||
plugins: {
|
|
||||||
[plugin: string]: {
|
|
||||||
[setting: string]: any;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const DefaultNativeSettings: NativeSettings = {
|
|
||||||
plugins: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
const nativeSettings = readSettings<NativeSettings>("native", NATIVE_SETTINGS_FILE);
|
|
||||||
mergeDefaults(nativeSettings, DefaultNativeSettings);
|
|
||||||
|
|
||||||
export const NativeSettings = new SettingsStore(nativeSettings);
|
|
||||||
|
|
||||||
NativeSettings.addGlobalChangeListener(() => {
|
|
||||||
try {
|
|
||||||
writeFileSync(NATIVE_SETTINGS_FILE, JSON.stringify(NativeSettings.plain, null, 4));
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to write native settings", e);
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -16,7 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IpcEvents } from "@shared/IpcEvents";
|
import { IpcEvents } from "@utils/IpcEvents";
|
||||||
import { execFile as cpExecFile } from "child_process";
|
import { execFile as cpExecFile } from "child_process";
|
||||||
import { ipcMain } from "electron";
|
import { ipcMain } from "electron";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
|
@ -28,7 +28,7 @@ const VENCORD_SRC_DIR = join(__dirname, "..");
|
||||||
|
|
||||||
const execFile = promisify(cpExecFile);
|
const execFile = promisify(cpExecFile);
|
||||||
|
|
||||||
const isFlatpak = process.platform === "linux" && !!process.env.FLATPAK_ID;
|
const isFlatpak = process.platform === "linux" && Boolean(process.env.FLATPAK_ID?.includes("discordapp") || process.env.FLATPAK_ID?.includes("Discord"));
|
||||||
|
|
||||||
if (process.platform === "darwin") process.env.PATH = `/usr/local/bin:${process.env.PATH}`;
|
if (process.platform === "darwin") process.env.PATH = `/usr/local/bin:${process.env.PATH}`;
|
||||||
|
|
||||||
|
@ -60,8 +60,7 @@ async function calculateGitChanges() {
|
||||||
return commits ? commits.split("\n").map(line => {
|
return commits ? commits.split("\n").map(line => {
|
||||||
const [author, hash, ...rest] = line.split("/");
|
const [author, hash, ...rest] = line.split("/");
|
||||||
return {
|
return {
|
||||||
hash, author,
|
hash, author, message: rest.join("/")
|
||||||
message: rest.join("/").split("\n")[0]
|
|
||||||
};
|
};
|
||||||
}) : [];
|
}) : [];
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,8 +16,8 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IpcEvents } from "@shared/IpcEvents";
|
import { VENCORD_USER_AGENT } from "@utils/constants";
|
||||||
import { VENCORD_USER_AGENT } from "@shared/vencordUserAgent";
|
import { IpcEvents } from "@utils/IpcEvents";
|
||||||
import { ipcMain } from "electron";
|
import { ipcMain } from "electron";
|
||||||
import { writeFile } from "fs/promises";
|
import { writeFile } from "fs/promises";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
|
@ -53,7 +53,7 @@ async function calculateGitChanges() {
|
||||||
// github api only sends the long sha
|
// github api only sends the long sha
|
||||||
hash: c.sha.slice(0, 7),
|
hash: c.sha.slice(0, 7),
|
||||||
author: c.author.login,
|
author: c.author.login,
|
||||||
message: c.commit.message.split("\n")[0]
|
message: c.commit.message
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,4 +17,4 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if (!IS_UPDATER_DISABLED)
|
if (!IS_UPDATER_DISABLED)
|
||||||
require(IS_STANDALONE ? "./http" : "./git");
|
import(IS_STANDALONE ? "./http" : "./git");
|
||||||
|
|
|
@ -28,14 +28,12 @@ export const SETTINGS_DIR = join(DATA_DIR, "settings");
|
||||||
export const THEMES_DIR = join(DATA_DIR, "themes");
|
export const THEMES_DIR = join(DATA_DIR, "themes");
|
||||||
export const QUICKCSS_PATH = join(SETTINGS_DIR, "quickCss.css");
|
export const QUICKCSS_PATH = join(SETTINGS_DIR, "quickCss.css");
|
||||||
export const SETTINGS_FILE = join(SETTINGS_DIR, "settings.json");
|
export const SETTINGS_FILE = join(SETTINGS_DIR, "settings.json");
|
||||||
export const NATIVE_SETTINGS_FILE = join(SETTINGS_DIR, "native-settings.json");
|
|
||||||
export const ALLOWED_PROTOCOLS = [
|
export const ALLOWED_PROTOCOLS = [
|
||||||
"https:",
|
"https:",
|
||||||
"http:",
|
"http:",
|
||||||
"steam:",
|
"steam:",
|
||||||
"spotify:",
|
"spotify:",
|
||||||
"com.epicgames.launcher:",
|
"com.epicgames.launcher:",
|
||||||
"tidal:"
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export const IS_VANILLA = /* @__PURE__ */ process.argv.includes("--vanilla");
|
export const IS_VANILLA = /* @__PURE__ */ process.argv.includes("--vanilla");
|
||||||
|
|
4
src/modules.d.ts
vendored
4
src/modules.d.ts
vendored
|
@ -20,7 +20,7 @@
|
||||||
/// <reference types="standalone-electron-types"/>
|
/// <reference types="standalone-electron-types"/>
|
||||||
|
|
||||||
declare module "~plugins" {
|
declare module "~plugins" {
|
||||||
const plugins: Record<string, import("./utils/types").Plugin>;
|
const plugins: Record<string, import("@utils/types").Plugin>;
|
||||||
export default plugins;
|
export default plugins;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ declare module "~git-remote" {
|
||||||
export default remote;
|
export default remote;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module "file://*" {
|
declare module "~fileContent/*" {
|
||||||
const content: string;
|
const content: string;
|
||||||
export default content;
|
export default content;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,14 +16,11 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import "./fixBadgeOverflow.css";
|
|
||||||
|
|
||||||
import { BadgePosition, BadgeUserArgs, ProfileBadge } from "@api/Badges";
|
import { BadgePosition, BadgeUserArgs, ProfileBadge } from "@api/Badges";
|
||||||
import DonateButton from "@components/DonateButton";
|
import DonateButton from "@components/DonateButton";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Flex } from "@components/Flex";
|
import { Flex } from "@components/Flex";
|
||||||
import { Heart } from "@components/Heart";
|
import { Heart } from "@components/Heart";
|
||||||
import { openContributorModal } from "@components/PluginSettings/ContributorModal";
|
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { isPluginDev } from "@utils/misc";
|
import { isPluginDev } from "@utils/misc";
|
||||||
|
@ -37,8 +34,14 @@ const ContributorBadge: ProfileBadge = {
|
||||||
description: "Vencord Contributor",
|
description: "Vencord Contributor",
|
||||||
image: CONTRIBUTOR_BADGE,
|
image: CONTRIBUTOR_BADGE,
|
||||||
position: BadgePosition.START,
|
position: BadgePosition.START,
|
||||||
|
props: {
|
||||||
|
style: {
|
||||||
|
borderRadius: "50%",
|
||||||
|
transform: "scale(0.9)" // The image is a bit too big compared to default badges
|
||||||
|
}
|
||||||
|
},
|
||||||
shouldShow: ({ user }) => isPluginDev(user.id),
|
shouldShow: ({ user }) => isPluginDev(user.id),
|
||||||
onClick: (_, { user }) => openContributorModal(user)
|
link: "https://github.com/Vendicated/Vencord"
|
||||||
};
|
};
|
||||||
|
|
||||||
let DonorBadges = {} as Record<string, Array<Record<"tooltip" | "badge", string>>>;
|
let DonorBadges = {} as Record<string, Array<Record<"tooltip" | "badge", string>>>;
|
||||||
|
@ -62,7 +65,7 @@ export default definePlugin({
|
||||||
patches: [
|
patches: [
|
||||||
/* Patch the badge list component on user profiles */
|
/* Patch the badge list component on user profiles */
|
||||||
{
|
{
|
||||||
find: 'id:"premium",',
|
find: "Messages.PROFILE_USER_BADGES,role:",
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /&&(\i)\.push\(\{id:"premium".+?\}\);/,
|
match: /&&(\i)\.push\(\{id:"premium".+?\}\);/,
|
||||||
|
@ -76,13 +79,13 @@ export default definePlugin({
|
||||||
},
|
},
|
||||||
// replace their component with ours if applicable
|
// replace their component with ours if applicable
|
||||||
{
|
{
|
||||||
match: /(?<=text:(\i)\.description,spacing:12,.{0,50})children:/,
|
match: /(?<=text:(\i)\.description,spacing:12,)children:/,
|
||||||
replace: "children:$1.component ? () => $self.renderBadgeComponent($1) :"
|
replace: "children:$1.component ? () => $self.renderBadgeComponent($1) :"
|
||||||
},
|
},
|
||||||
// conditionally override their onClick with badge.onClick if it exists
|
// conditionally override their onClick with badge.onClick if it exists
|
||||||
{
|
{
|
||||||
match: /href:(\i)\.link/,
|
match: /href:(\i)\.link/,
|
||||||
replace: "...($1.onClick && { onClick: vcE => $1.onClick(vcE, arguments[0]) }),$&"
|
replace: "...($1.onClick && { onClick: $1.onClick }),$&"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -1,3 +0,0 @@
|
||||||
[class*="profileBadges"] {
|
|
||||||
flex: none;
|
|
||||||
}
|
|
|
@ -13,10 +13,10 @@ export default definePlugin({
|
||||||
authors: [Devs.Ven],
|
authors: [Devs.Ven],
|
||||||
|
|
||||||
patches: [{
|
patches: [{
|
||||||
find: '"sticker")',
|
find: 'location:"ChannelTextAreaButtons"',
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /!\i\.isMobile(?=.+?(\i)\.push\(.{0,50}"gift")/,
|
match: /if\(!\i\.isMobile\)\{(?=.+?&&(\i)\.push\(.{0,50}"gift")/,
|
||||||
replace: "$& &&(Vencord.Api.ChatButtons._injectButtons($1,arguments[0]),true)"
|
replace: "$&Vencord.Api.ChatButtons._injectButtons($1,arguments[0]);"
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
|
|
@ -31,7 +31,7 @@ export default definePlugin({
|
||||||
match: /let\{[^}]*lostPermissionTooltipText:\i[^}]*\}=(\i),/,
|
match: /let\{[^}]*lostPermissionTooltipText:\i[^}]*\}=(\i),/,
|
||||||
replace: "$&vencordProps=$1,"
|
replace: "$&vencordProps=$1,"
|
||||||
}, {
|
}, {
|
||||||
match: /\.Messages\.GUILD_OWNER(?=.+?decorators:(\i)\(\)).+?\1=?\(\)=>.+?children:\[/,
|
match: /decorators:.{0,100}?children:\[/,
|
||||||
replace: "$&...(typeof vencordProps=='undefined'?[]:Vencord.Api.MemberListDecorators.__getDecorators(vencordProps)),"
|
replace: "$&...(typeof vencordProps=='undefined'?[]:Vencord.Api.MemberListDecorators.__getDecorators(vencordProps)),"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -35,7 +35,7 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: ".handleSendMessage",
|
find: ".handleSendMessage=",
|
||||||
replacement: {
|
replacement: {
|
||||||
// props.chatInputType...then((function(isMessageValid)... var parsedMessage = b.c.parse(channel,... var replyOptions = f.g.getSendMessageOptionsForReply(pendingReply);
|
// props.chatInputType...then((function(isMessageValid)... var parsedMessage = b.c.parse(channel,... var replyOptions = f.g.getSendMessageOptionsForReply(pendingReply);
|
||||||
// Lookbehind: validateMessage)({openWarningPopout:..., type: i.props.chatInputType, content: t, stickers: r, ...}).then((function(isMessageValid)
|
// Lookbehind: validateMessage)({openWarningPopout:..., type: i.props.chatInputType, content: t, stickers: r, ...}).then((function(isMessageValid)
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
/*
|
|
||||||
* Vencord, a modification for Discord's desktop app
|
|
||||||
* Copyright (c) 2022 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 { Devs } from "@utils/constants";
|
|
||||||
import definePlugin from "@utils/types";
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "MessageUpdaterAPI",
|
|
||||||
description: "API for updating and re-rendering messages.",
|
|
||||||
authors: [Devs.Nuckyz],
|
|
||||||
|
|
||||||
patches: [
|
|
||||||
{
|
|
||||||
// Message accessories have a custom logic to decide if they should render again, so we need to make it not ignore changed message reference
|
|
||||||
find: "}renderEmbeds(",
|
|
||||||
replacement: {
|
|
||||||
match: /(?<=this.props,\i,\[)"message",/,
|
|
||||||
replace: ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
|
@ -26,10 +26,10 @@ export default definePlugin({
|
||||||
required: true,
|
required: true,
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: '"NoticeStore"',
|
find: 'displayName="NoticeStore"',
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /(?<=!1;)\i=null;(?=.{0,80}getPremiumSubscription\(\))/g,
|
match: /\i=null;(?=.{0,80}getPremiumSubscription\(\))/g,
|
||||||
replace: "if(Vencord.Api.Notices.currentNotice)return false;$&"
|
replace: "if(Vencord.Api.Notices.currentNotice)return false;$&"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -16,31 +16,17 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { definePluginSettings } from "@api/Settings";
|
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
|
||||||
disableAnalytics: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
description: "Disable Discord's tracking (analytics/'science')",
|
|
||||||
default: true,
|
|
||||||
restartNeeded: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "NoTrack",
|
name: "NoTrack",
|
||||||
description: "Disable Discord's tracking (analytics/'science'), metrics and Sentry crash reporting",
|
description: "Disable Discord's tracking ('science'), metrics and Sentry crash reporting",
|
||||||
authors: [Devs.Cyn, Devs.Ven, Devs.Nuckyz, Devs.Arrow],
|
authors: [Devs.Cyn, Devs.Ven, Devs.Nuckyz, Devs.Arrow],
|
||||||
required: true,
|
required: true,
|
||||||
|
|
||||||
settings,
|
|
||||||
|
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: "AnalyticsActionHandlers.handle",
|
find: "AnalyticsActionHandlers.handle",
|
||||||
predicate: () => settings.store.disableAnalytics,
|
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /^.+$/,
|
match: /^.+$/,
|
||||||
replace: "()=>{}",
|
replace: "()=>{}",
|
||||||
|
@ -58,11 +44,11 @@ export default definePlugin({
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /this\._intervalId=/,
|
match: /this\._intervalId=/,
|
||||||
replace: "this._intervalId=void 0&&"
|
replace: "this._intervalId=undefined&&"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /(?:increment|distribution)\(\i(?:,\i)?\){/g,
|
match: /(increment\(\i\){)/,
|
||||||
replace: "$&return;"
|
replace: "$1return;"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -16,92 +16,70 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { findGroupChildrenByChildId } from "@api/ContextMenu";
|
||||||
import { Settings } from "@api/Settings";
|
import { Settings } from "@api/Settings";
|
||||||
import BackupAndRestoreTab from "@components/VencordSettings/BackupAndRestoreTab";
|
|
||||||
import CloudTab from "@components/VencordSettings/CloudTab";
|
|
||||||
import PatchHelperTab from "@components/VencordSettings/PatchHelperTab";
|
|
||||||
import PluginsTab from "@components/VencordSettings/PluginsTab";
|
|
||||||
import ThemesTab from "@components/VencordSettings/ThemesTab";
|
|
||||||
import UpdaterTab from "@components/VencordSettings/UpdaterTab";
|
|
||||||
import VencordTab from "@components/VencordSettings/VencordTab";
|
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { i18n, React } from "@webpack/common";
|
import { React, SettingsRouter } from "@webpack/common";
|
||||||
|
|
||||||
import gitHash from "~git-hash";
|
import gitHash from "~git-hash";
|
||||||
|
|
||||||
type SectionType = "HEADER" | "DIVIDER" | "CUSTOM";
|
|
||||||
type SectionTypes = Record<SectionType, SectionType>;
|
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "Settings",
|
name: "Settings",
|
||||||
description: "Adds Settings UI and debug info",
|
description: "Adds Settings UI and debug info",
|
||||||
authors: [Devs.Ven, Devs.Megu],
|
authors: [Devs.Ven, Devs.Megu],
|
||||||
required: true,
|
required: true,
|
||||||
|
|
||||||
patches: [
|
contextMenus: {
|
||||||
{
|
// The settings shortcuts in the user settings cog context menu
|
||||||
find: ".versionHash",
|
// read the elements from a hardcoded map which for obvious reason
|
||||||
replacement: [
|
// doesn't contain our sections. This patches the actions of our
|
||||||
{
|
// sections to manually use SettingsRouter (which only works on desktop
|
||||||
match: /\[\(0,\i\.jsxs?\)\((.{1,10}),(\{[^{}}]+\{.{0,20}.versionHash,.+?\})\)," "/,
|
// but the context menu is usually not available on mobile anyway)
|
||||||
replace: (m, component, props) => {
|
"user-settings-cog"(children) {
|
||||||
props = props.replace(/children:\[.+\]/, "");
|
const section = findGroupChildrenByChildId("VencordSettings", children);
|
||||||
return `${m},$self.makeInfoElements(${component}, ${props})`;
|
section?.forEach(c => {
|
||||||
}
|
const id = c?.props?.id;
|
||||||
},
|
if (id?.startsWith("Vencord") || id?.startsWith("Vesktop")) {
|
||||||
{
|
c!.props.action = () => SettingsRouter.open(id);
|
||||||
match: /copyValue:\i\.join\(" "\)/,
|
|
||||||
replace: "$& + $self.getInfoString()"
|
|
||||||
}
|
}
|
||||||
]
|
});
|
||||||
},
|
|
||||||
// Discord Stable
|
|
||||||
// FIXME: remove once change merged to stable
|
|
||||||
{
|
|
||||||
find: "Messages.ACTIVITY_SETTINGS",
|
|
||||||
replacement: {
|
|
||||||
get match() {
|
|
||||||
switch (Settings.plugins.Settings.settingsLocation) {
|
|
||||||
case "top": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.USER_SETTINGS/;
|
|
||||||
case "aboveNitro": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.BILLING_SETTINGS/;
|
|
||||||
case "belowNitro": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.APP_SETTINGS/;
|
|
||||||
case "belowActivity": return /(?<=\{section:(\i\.\i)\.DIVIDER},)\{section:"changelog"/;
|
|
||||||
case "bottom": return /\{section:(\i\.\i)\.CUSTOM,\s*element:.+?}/;
|
|
||||||
case "aboveActivity":
|
|
||||||
default:
|
|
||||||
return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.ACTIVITY_SETTINGS/;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
replace: "...$self.makeSettingsCategories($1),$&"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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}`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: "useDefaultUserSettingsSections:function",
|
|
||||||
replacement: {
|
|
||||||
match: /(?<=useDefaultUserSettingsSections:function\(\){return )(\i)\}/,
|
|
||||||
replace: "$self.wrapSettingsHook($1)}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",
|
|
||||||
replacement: {
|
|
||||||
match: /(?<=function\((\i),\i\)\{)(?=let \i=Object.values\(\i.UserSettingsSections\).*?(\i)\.default\.open\()/,
|
|
||||||
replace: "$2.default.open($1);return;"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
},
|
||||||
|
|
||||||
customSections: [] as ((SectionTypes: SectionTypes) => any)[],
|
patches: [{
|
||||||
|
find: ".versionHash",
|
||||||
|
replacement: [
|
||||||
|
{
|
||||||
|
match: /\[\(0,.{1,3}\.jsxs?\)\((.{1,10}),(\{[^{}}]+\{.{0,20}.versionHash,.+?\})\)," "/,
|
||||||
|
replace: (m, component, props) => {
|
||||||
|
props = props.replace(/children:\[.+\]/, "");
|
||||||
|
return `${m},Vencord.Plugins.plugins.Settings.makeInfoElements(${component}, ${props})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}, {
|
||||||
|
find: "Messages.ACTIVITY_SETTINGS",
|
||||||
|
replacement: {
|
||||||
|
get match() {
|
||||||
|
switch (Settings.plugins.Settings.settingsLocation) {
|
||||||
|
case "top": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.USER_SETTINGS\}/;
|
||||||
|
case "aboveNitro": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.BILLING_SETTINGS\}/;
|
||||||
|
case "belowNitro": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.APP_SETTINGS\}/;
|
||||||
|
case "belowActivity": return /(?<=\{section:(\i\.\i)\.DIVIDER},)\{section:"changelog"/;
|
||||||
|
case "bottom": return /\{section:(\i\.\i)\.CUSTOM,\s*element:.+?}/;
|
||||||
|
case "aboveActivity":
|
||||||
|
default:
|
||||||
|
return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.ACTIVITY_SETTINGS\}/;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
replace: "...$self.makeSettingsCategories($1),$&"
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
|
||||||
makeSettingsCategories(SectionTypes: SectionTypes) {
|
customSections: [] as ((SectionTypes: Record<string, unknown>) => any)[],
|
||||||
|
|
||||||
|
makeSettingsCategories(SectionTypes: Record<string, unknown>) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
section: SectionTypes.HEADER,
|
section: SectionTypes.HEADER,
|
||||||
|
@ -111,43 +89,43 @@ export default definePlugin({
|
||||||
{
|
{
|
||||||
section: "VencordSettings",
|
section: "VencordSettings",
|
||||||
label: "Vencord",
|
label: "Vencord",
|
||||||
element: VencordTab,
|
element: require("@components/VencordSettings/VencordTab").default,
|
||||||
className: "vc-settings"
|
className: "vc-settings"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
section: "VencordPlugins",
|
section: "VencordPlugins",
|
||||||
label: "Plugins",
|
label: "Plugins",
|
||||||
element: PluginsTab,
|
element: require("@components/VencordSettings/PluginsTab").default,
|
||||||
className: "vc-plugins"
|
className: "vc-plugins"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
section: "VencordThemes",
|
section: "VencordThemes",
|
||||||
label: "Themes",
|
label: "Themes",
|
||||||
element: ThemesTab,
|
element: require("@components/VencordSettings/ThemesTab").default,
|
||||||
className: "vc-themes"
|
className: "vc-themes"
|
||||||
},
|
},
|
||||||
!IS_UPDATER_DISABLED && {
|
!IS_UPDATER_DISABLED && {
|
||||||
section: "VencordUpdater",
|
section: "VencordUpdater",
|
||||||
label: "Updater",
|
label: "Updater",
|
||||||
element: UpdaterTab,
|
element: require("@components/VencordSettings/UpdaterTab").default,
|
||||||
className: "vc-updater"
|
className: "vc-updater"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
section: "VencordCloud",
|
section: "VencordCloud",
|
||||||
label: "Cloud",
|
label: "Cloud",
|
||||||
element: CloudTab,
|
element: require("@components/VencordSettings/CloudTab").default,
|
||||||
className: "vc-cloud"
|
className: "vc-cloud"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
section: "VencordSettingsSync",
|
section: "VencordSettingsSync",
|
||||||
label: "Backup & Restore",
|
label: "Backup & Restore",
|
||||||
element: BackupAndRestoreTab,
|
element: require("@components/VencordSettings/BackupAndRestoreTab").default,
|
||||||
className: "vc-backup-restore"
|
className: "vc-backup-restore"
|
||||||
},
|
},
|
||||||
IS_DEV && {
|
IS_DEV && {
|
||||||
section: "VencordPatchHelper",
|
section: "VencordPatchHelper",
|
||||||
label: "Patch Helper",
|
label: "Patch Helper",
|
||||||
element: PatchHelperTab,
|
element: require("@components/VencordSettings/PatchHelperTab").default,
|
||||||
className: "vc-patch-helper"
|
className: "vc-patch-helper"
|
||||||
},
|
},
|
||||||
...this.customSections.map(func => func(SectionTypes)),
|
...this.customSections.map(func => func(SectionTypes)),
|
||||||
|
@ -157,63 +135,19 @@ export default definePlugin({
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
},
|
},
|
||||||
|
|
||||||
isRightSpot({ header, settings }: { header?: string; settings?: string[]; }) {
|
|
||||||
const firstChild = settings?.[0];
|
|
||||||
// lowest two elements... sanity backup
|
|
||||||
if (firstChild === "LOGOUT" || firstChild === "SOCIAL_LINKS") return true;
|
|
||||||
|
|
||||||
const { settingsLocation } = Settings.plugins.Settings;
|
|
||||||
|
|
||||||
if (settingsLocation === "bottom") return firstChild === "LOGOUT";
|
|
||||||
if (settingsLocation === "belowActivity") return firstChild === "CHANGELOG";
|
|
||||||
|
|
||||||
if (!header) return;
|
|
||||||
|
|
||||||
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(),
|
|
||||||
|
|
||||||
addSettings(elements: any[], element: { header?: string; settings: string[]; }, sectionTypes: SectionTypes) {
|
|
||||||
if (this.patchedSettings.has(elements) || !this.isRightSpot(element)) return;
|
|
||||||
|
|
||||||
this.patchedSettings.add(elements);
|
|
||||||
|
|
||||||
elements.push(...this.makeSettingsCategories(sectionTypes));
|
|
||||||
},
|
|
||||||
|
|
||||||
wrapSettingsHook(originalHook: (...args: any[]) => Record<string, unknown>[]) {
|
|
||||||
return (...args: any[]) => {
|
|
||||||
const elements = originalHook(...args);
|
|
||||||
if (!this.patchedSettings.has(elements))
|
|
||||||
elements.unshift(...this.makeSettingsCategories({
|
|
||||||
HEADER: "HEADER",
|
|
||||||
DIVIDER: "DIVIDER",
|
|
||||||
CUSTOM: "CUSTOM"
|
|
||||||
}));
|
|
||||||
|
|
||||||
return elements;
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
options: {
|
options: {
|
||||||
settingsLocation: {
|
settingsLocation: {
|
||||||
type: OptionType.SELECT,
|
type: OptionType.SELECT,
|
||||||
description: "Where to put the Vencord settings section",
|
description: "Where to put the Vencord settings section",
|
||||||
options: [
|
options: [
|
||||||
{ label: "At the very top", value: "top" },
|
{ label: "At the very top", value: "top" },
|
||||||
{ label: "Above the Nitro section", value: "aboveNitro", default: true },
|
{ label: "Above the Nitro section", value: "aboveNitro" },
|
||||||
{ label: "Below the Nitro section", value: "belowNitro" },
|
{ label: "Below the Nitro section", value: "belowNitro" },
|
||||||
{ label: "Above Activity Settings", value: "aboveActivity" },
|
{ label: "Above Activity Settings", value: "aboveActivity", default: true },
|
||||||
{ label: "Below Activity Settings", value: "belowActivity" },
|
{ label: "Below Activity Settings", value: "belowActivity" },
|
||||||
{ label: "At the very bottom", value: "bottom" },
|
{ label: "At the very bottom", value: "bottom" },
|
||||||
]
|
],
|
||||||
|
restartNeeded: true
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -240,24 +174,15 @@ export default definePlugin({
|
||||||
return "";
|
return "";
|
||||||
},
|
},
|
||||||
|
|
||||||
getInfoRows() {
|
makeInfoElements(Component: React.ComponentType<React.PropsWithChildren>, props: React.PropsWithChildren) {
|
||||||
const { electronVersion, chromiumVersion, additionalInfo } = this;
|
const { electronVersion, chromiumVersion, additionalInfo } = this;
|
||||||
|
|
||||||
const rows = [`Vencord ${gitHash}${additionalInfo}`];
|
return (
|
||||||
|
<>
|
||||||
if (electronVersion) rows.push(`Electron ${electronVersion}`);
|
<Component {...props}>Vencord {gitHash}{additionalInfo}</Component>
|
||||||
if (chromiumVersion) rows.push(`Chromium ${chromiumVersion}`);
|
{electronVersion && <Component {...props}>Electron {electronVersion}</Component>}
|
||||||
|
{chromiumVersion && <Component {...props}>Chromium {chromiumVersion}</Component>}
|
||||||
return rows;
|
</>
|
||||||
},
|
|
||||||
|
|
||||||
getInfoString() {
|
|
||||||
return "\n" + this.getInfoRows().join("\n");
|
|
||||||
},
|
|
||||||
|
|
||||||
makeInfoElements(Component: React.ComponentType<React.PropsWithChildren>, props: React.PropsWithChildren) {
|
|
||||||
return this.getInfoRows().map((text, i) =>
|
|
||||||
<Component key={i} {...props}>{text}</Component>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,24 +16,20 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import { DataStore } from "@api/index";
|
||||||
import { Link } from "@components/Link";
|
|
||||||
import { openUpdaterModal } from "@components/VencordSettings/UpdaterTab";
|
|
||||||
import { Devs, SUPPORT_CHANNEL_ID } from "@utils/constants";
|
import { Devs, SUPPORT_CHANNEL_ID } from "@utils/constants";
|
||||||
import { Margins } from "@utils/margins";
|
|
||||||
import { isPluginDev } from "@utils/misc";
|
import { isPluginDev } from "@utils/misc";
|
||||||
import { relaunch } from "@utils/native";
|
|
||||||
import { makeCodeblock } from "@utils/text";
|
import { makeCodeblock } from "@utils/text";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { isOutdated, update } from "@utils/updater";
|
import { isOutdated } from "@utils/updater";
|
||||||
import { Alerts, Card, ChannelStore, Forms, GuildMemberStore, NavigationRouter, Parser, RelationshipStore, UserStore } from "@webpack/common";
|
import { Alerts, Forms, UserStore } from "@webpack/common";
|
||||||
|
|
||||||
import gitHash from "~git-hash";
|
import gitHash from "~git-hash";
|
||||||
import plugins from "~plugins";
|
import plugins from "~plugins";
|
||||||
|
|
||||||
import settings from "./settings";
|
import settings from "./settings";
|
||||||
|
|
||||||
const VENCORD_GUILD_ID = "1015060230222131221";
|
const REMEMBER_DISMISS_KEY = "Vencord-SupportHelper-Dismiss";
|
||||||
|
|
||||||
const AllowedChannelIds = [
|
const AllowedChannelIds = [
|
||||||
SUPPORT_CHANNEL_ID,
|
SUPPORT_CHANNEL_ID,
|
||||||
|
@ -41,12 +37,6 @@ const AllowedChannelIds = [
|
||||||
"1033680203433660458", // Vencord > #v
|
"1033680203433660458", // Vencord > #v
|
||||||
];
|
];
|
||||||
|
|
||||||
const TrustedRolesIds = [
|
|
||||||
"1026534353167208489", // contributor
|
|
||||||
"1026504932959977532", // regular
|
|
||||||
"1042507929485586532", // donor
|
|
||||||
];
|
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "SupportHelper",
|
name: "SupportHelper",
|
||||||
required: true,
|
required: true,
|
||||||
|
@ -54,18 +44,10 @@ export default definePlugin({
|
||||||
authors: [Devs.Ven],
|
authors: [Devs.Ven],
|
||||||
dependencies: ["CommandsAPI"],
|
dependencies: ["CommandsAPI"],
|
||||||
|
|
||||||
patches: [{
|
|
||||||
find: ".BEGINNING_DM.format",
|
|
||||||
replacement: {
|
|
||||||
match: /BEGINNING_DM\.format\(\{.+?\}\),(?=.{0,100}userId:(\i\.getRecipientId\(\)))/,
|
|
||||||
replace: "$& $self.ContributorDmWarningCard({ userId: $1 }),"
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
|
|
||||||
commands: [{
|
commands: [{
|
||||||
name: "vencord-debug",
|
name: "vencord-debug",
|
||||||
description: "Send Vencord Debug info",
|
description: "Send Vencord Debug info",
|
||||||
predicate: ctx => isPluginDev(UserStore.getCurrentUser()?.id) || AllowedChannelIds.includes(ctx.channel.id),
|
predicate: ctx => AllowedChannelIds.includes(ctx.channel.id),
|
||||||
async execute() {
|
async execute() {
|
||||||
const { RELEASE_CHANNEL } = window.GLOBAL_ENV;
|
const { RELEASE_CHANNEL } = window.GLOBAL_ENV;
|
||||||
|
|
||||||
|
@ -82,13 +64,15 @@ export default definePlugin({
|
||||||
const isApiPlugin = (plugin: string) => plugin.endsWith("API") || plugins[plugin].required;
|
const isApiPlugin = (plugin: string) => plugin.endsWith("API") || plugins[plugin].required;
|
||||||
|
|
||||||
const enabledPlugins = Object.keys(plugins).filter(p => Vencord.Plugins.isPluginEnabled(p) && !isApiPlugin(p));
|
const enabledPlugins = Object.keys(plugins).filter(p => Vencord.Plugins.isPluginEnabled(p) && !isApiPlugin(p));
|
||||||
|
const enabledApiPlugins = Object.keys(plugins).filter(p => Vencord.Plugins.isPluginEnabled(p) && isApiPlugin(p));
|
||||||
|
|
||||||
const info = {
|
const info = {
|
||||||
Vencord:
|
Vencord: `v${VERSION} • ${gitHash}${settings.additionalInfo} - ${Intl.DateTimeFormat("en-GB", { dateStyle: "medium" }).format(BUILD_TIMESTAMP)}`,
|
||||||
`v${VERSION} • [${gitHash}](<https://github.com/Vendicated/Vencord/commit/${gitHash}>)` +
|
"Discord Branch": RELEASE_CHANNEL,
|
||||||
`${settings.additionalInfo} - ${Intl.DateTimeFormat("en-GB", { dateStyle: "medium" }).format(BUILD_TIMESTAMP)}`,
|
Client: client,
|
||||||
Client: `${RELEASE_CHANNEL} ~ ${client}`,
|
Platform: window.navigator.platform,
|
||||||
Platform: window.navigator.platform
|
Outdated: isOutdated,
|
||||||
|
OpenAsar: "openasar" in window,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (IS_DISCORD_DESKTOP) {
|
if (IS_DISCORD_DESKTOP) {
|
||||||
|
@ -96,10 +80,11 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
|
|
||||||
const debugInfo = `
|
const debugInfo = `
|
||||||
>>> ${Object.entries(info).map(([k, v]) => `**${k}**: ${v}`).join("\n")}
|
**Vencord Debug Info**
|
||||||
|
>>> ${Object.entries(info).map(([k, v]) => `${k}: ${v}`).join("\n")}
|
||||||
|
|
||||||
Enabled Plugins (${enabledPlugins.length}):
|
Enabled Plugins (${enabledPlugins.length + enabledApiPlugins.length}):
|
||||||
${makeCodeblock(enabledPlugins.join(", "))}
|
${makeCodeblock(enabledPlugins.join(", ") + "\n\n" + enabledApiPlugins.join(", "))}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -112,75 +97,24 @@ ${makeCodeblock(enabledPlugins.join(", "))}
|
||||||
async CHANNEL_SELECT({ channelId }) {
|
async CHANNEL_SELECT({ channelId }) {
|
||||||
if (channelId !== SUPPORT_CHANNEL_ID) return;
|
if (channelId !== SUPPORT_CHANNEL_ID) return;
|
||||||
|
|
||||||
const selfId = UserStore.getCurrentUser()?.id;
|
if (isPluginDev(UserStore.getCurrentUser().id)) return;
|
||||||
if (!selfId || isPluginDev(selfId)) return;
|
|
||||||
|
|
||||||
if (isOutdated) {
|
if (isOutdated && gitHash !== await DataStore.get(REMEMBER_DISMISS_KEY)) {
|
||||||
return Alerts.show({
|
const rememberDismiss = () => DataStore.set(REMEMBER_DISMISS_KEY, gitHash);
|
||||||
|
|
||||||
|
Alerts.show({
|
||||||
title: "Hold on!",
|
title: "Hold on!",
|
||||||
body: <div>
|
body: <div>
|
||||||
<Forms.FormText>You are using an outdated version of Vencord! Chances are, your issue is already fixed.</Forms.FormText>
|
<Forms.FormText>You are using an outdated version of Vencord! Chances are, your issue is already fixed.</Forms.FormText>
|
||||||
<Forms.FormText className={Margins.top8}>
|
<Forms.FormText>
|
||||||
Please first update before asking for support!
|
Please first update using the Updater Page in Settings, or use the VencordInstaller (Update Vencord Button)
|
||||||
|
to do so, in case you can't access the Updater page.
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
</div>,
|
</div>,
|
||||||
onCancel: () => openUpdaterModal!(),
|
onCancel: rememberDismiss,
|
||||||
cancelText: "View Updates",
|
onConfirm: rememberDismiss
|
||||||
confirmText: "Update & Restart Now",
|
|
||||||
async onConfirm() {
|
|
||||||
await update();
|
|
||||||
relaunch();
|
|
||||||
},
|
|
||||||
secondaryConfirmText: "I know what I'm doing or I can't update"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-ignore outdated type
|
|
||||||
const roles = GuildMemberStore.getSelfMember(VENCORD_GUILD_ID)?.roles;
|
|
||||||
if (!roles || TrustedRolesIds.some(id => roles.includes(id))) return;
|
|
||||||
|
|
||||||
if (!IS_WEB && IS_UPDATER_DISABLED) {
|
|
||||||
return Alerts.show({
|
|
||||||
title: "Hold on!",
|
|
||||||
body: <div>
|
|
||||||
<Forms.FormText>You are using an externally updated Vencord version, which we do not provide support for!</Forms.FormText>
|
|
||||||
<Forms.FormText className={Margins.top8}>
|
|
||||||
Please either switch to an <Link href="https://vencord.dev/download">officially supported version of Vencord</Link>, or
|
|
||||||
contact your package maintainer for support instead.
|
|
||||||
</Forms.FormText>
|
|
||||||
</div>,
|
|
||||||
onCloseCallback: () => setTimeout(() => NavigationRouter.back(), 50)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const repo = await VencordNative.updater.getRepo();
|
|
||||||
if (repo.ok && !repo.value.includes("Vendicated/Vencord")) {
|
|
||||||
return Alerts.show({
|
|
||||||
title: "Hold on!",
|
|
||||||
body: <div>
|
|
||||||
<Forms.FormText>You are using a fork of Vencord, which we do not provide support for!</Forms.FormText>
|
|
||||||
<Forms.FormText className={Margins.top8}>
|
|
||||||
Please either switch to an <Link href="https://vencord.dev/download">officially supported version of Vencord</Link>, or
|
|
||||||
contact your package maintainer for support instead.
|
|
||||||
</Forms.FormText>
|
|
||||||
</div>,
|
|
||||||
onCloseCallback: () => setTimeout(() => NavigationRouter.back(), 50)
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
ContributorDmWarningCard: ErrorBoundary.wrap(({ userId }) => {
|
|
||||||
if (!isPluginDev(userId)) return null;
|
|
||||||
if (RelationshipStore.isFriend(userId)) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className={`vc-plugins-restart-card ${Margins.top8}`}>
|
|
||||||
Please do not private message Vencord plugin developers for support!
|
|
||||||
<br />
|
|
||||||
Instead, use the Vencord support channel: {Parser.parse("https://discord.com/channels/1015060230222131221/1026515880080842772")}
|
|
||||||
{!ChannelStore.getChannel(SUPPORT_CHANNEL_ID) && " (Click the link to join)"}
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}, { noop: true })
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -31,10 +31,10 @@ export default definePlugin({
|
||||||
// Some modules match the find but the replacement is returned untouched
|
// Some modules match the find but the replacement is returned untouched
|
||||||
noWarn: true,
|
noWarn: true,
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /canAnimate:.+?([,}].*?\))/g,
|
match: /canAnimate:.+?(?=([,}].*?\)))/g,
|
||||||
replace: (m, rest) => {
|
replace: (m, rest) => {
|
||||||
const destructuringMatch = rest.match(/}=.+/);
|
const destructuringMatch = rest.match(/}=.+/);
|
||||||
if (destructuringMatch == null) return `canAnimate:!0${rest}`;
|
if (destructuringMatch == null) return "canAnimate:!0";
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,46 +16,27 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { definePluginSettings } from "@api/Settings";
|
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
|
||||||
domain: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
default: true,
|
|
||||||
description: "Remove the untrusted domain popup when opening links",
|
|
||||||
restartNeeded: true
|
|
||||||
},
|
|
||||||
file: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
default: true,
|
|
||||||
description: "Remove the 'Potentially Dangerous Download' popup when opening links",
|
|
||||||
restartNeeded: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "AlwaysTrust",
|
name: "AlwaysTrust",
|
||||||
description: "Removes the annoying untrusted domain and suspicious file popup",
|
description: "Removes the annoying untrusted domain and suspicious file popup",
|
||||||
authors: [Devs.zt, Devs.Trwy],
|
authors: [Devs.zt],
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: '="MaskedLinkStore",',
|
find: ".displayName=\"MaskedLinkStore\"",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<=isTrustedDomain\(\i\){)return \i\(\i\)/,
|
match: /(?<=isTrustedDomain\(\i\){)return \i\(\i\)/,
|
||||||
replace: "return true"
|
replace: "return true"
|
||||||
},
|
}
|
||||||
predicate: () => settings.store.domain
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: "isSuspiciousDownload:",
|
find: "isSuspiciousDownload:",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /function \i\(\i\){(?=.{0,60}\.parse\(\i\))/,
|
match: /function \i\(\i\){(?=.{0,60}\.parse\(\i\))/,
|
||||||
replace: "$&return null;"
|
replace: "$&return null;"
|
||||||
},
|
}
|
||||||
predicate: () => settings.store.file
|
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
settings
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -67,24 +67,17 @@ const settings = definePluginSettings({
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "AnonymiseFileNames",
|
name: "AnonymiseFileNames",
|
||||||
authors: [Devs.fawn],
|
authors: [Devs.obscurity],
|
||||||
description: "Anonymise uploaded file names",
|
description: "Anonymise uploaded file names",
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: "instantBatchUpload:function",
|
find: "instantBatchUpload:function",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /uploadFiles:(\i),/,
|
match: /uploadFiles:(.{1,2}),/,
|
||||||
replace:
|
replace:
|
||||||
"uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=$self.anonymise(f)),$1(...args)),",
|
"uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=$self.anonymise(f)),$1(...args)),",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
find: 'addFilesTo:"message.attachments"',
|
|
||||||
replacement: {
|
|
||||||
match: /(\i.uploadFiles\((\i),)/,
|
|
||||||
replace: "$2.forEach(f=>f.filename=$self.anonymise(f)),$1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
find: ".Messages.ATTACHMENT_UTILITIES_SPOILER",
|
find: ".Messages.ATTACHMENT_UTILITIES_SPOILER",
|
||||||
replacement: {
|
replacement: {
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
# AppleMusicRichPresence
|
|
||||||
|
|
||||||
This plugin enables Discord rich presence for your Apple Music! (This only works on macOS with the Music app.)
|
|
||||||
|
|
||||||
![Screenshot of the activity in Discord](https://github.com/Vendicated/Vencord/assets/70191398/1f811090-ab5f-4060-a9ee-d0ac44a1d3c0)
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
For the customizable activity format strings, you can use several special strings to include track data in activities! `{name}` is replaced with the track name; `{artist}` is replaced with the artist(s)' name(s); and `{album}` is replaced with the album name.
|
|
|
@ -1,253 +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 { Devs } from "@utils/constants";
|
|
||||||
import definePlugin, { OptionType, PluginNative } from "@utils/types";
|
|
||||||
import { ApplicationAssetUtils, FluxDispatcher, Forms } from "@webpack/common";
|
|
||||||
|
|
||||||
const Native = VencordNative.pluginHelpers.AppleMusic as PluginNative<typeof import("./native")>;
|
|
||||||
|
|
||||||
interface ActivityAssets {
|
|
||||||
large_image?: string;
|
|
||||||
large_text?: string;
|
|
||||||
small_image?: string;
|
|
||||||
small_text?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ActivityButton {
|
|
||||||
label: string;
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Activity {
|
|
||||||
state: string;
|
|
||||||
details?: string;
|
|
||||||
timestamps?: {
|
|
||||||
start?: number;
|
|
||||||
end?: number;
|
|
||||||
};
|
|
||||||
assets?: ActivityAssets;
|
|
||||||
buttons?: Array<string>;
|
|
||||||
name: string;
|
|
||||||
application_id: string;
|
|
||||||
metadata?: {
|
|
||||||
button_urls?: Array<string>;
|
|
||||||
};
|
|
||||||
type: number;
|
|
||||||
flags: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const enum ActivityType {
|
|
||||||
PLAYING = 0,
|
|
||||||
LISTENING = 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
const enum ActivityFlag {
|
|
||||||
INSTANCE = 1 << 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TrackData {
|
|
||||||
name: string;
|
|
||||||
album: string;
|
|
||||||
artist: string;
|
|
||||||
|
|
||||||
appleMusicLink?: string;
|
|
||||||
songLink?: string;
|
|
||||||
|
|
||||||
albumArtwork?: string;
|
|
||||||
artistArtwork?: string;
|
|
||||||
|
|
||||||
playerPosition: number;
|
|
||||||
duration: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const enum AssetImageType {
|
|
||||||
Album = "Album",
|
|
||||||
Artist = "Artist",
|
|
||||||
}
|
|
||||||
|
|
||||||
const applicationId = "1239490006054207550";
|
|
||||||
|
|
||||||
function setActivity(activity: Activity | null) {
|
|
||||||
FluxDispatcher.dispatch({
|
|
||||||
type: "LOCAL_ACTIVITY_UPDATE",
|
|
||||||
activity,
|
|
||||||
socketId: "AppleMusic",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
|
||||||
activityType: {
|
|
||||||
type: OptionType.SELECT,
|
|
||||||
description: "Which type of activity",
|
|
||||||
options: [
|
|
||||||
{ label: "Playing", value: ActivityType.PLAYING, default: true },
|
|
||||||
{ label: "Listening", value: ActivityType.LISTENING }
|
|
||||||
],
|
|
||||||
},
|
|
||||||
refreshInterval: {
|
|
||||||
type: OptionType.SLIDER,
|
|
||||||
description: "The interval between activity refreshes (seconds)",
|
|
||||||
markers: [1, 2, 2.5, 3, 5, 10, 15],
|
|
||||||
default: 5,
|
|
||||||
restartNeeded: true,
|
|
||||||
},
|
|
||||||
enableTimestamps: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
description: "Whether or not to enable timestamps",
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
enableButtons: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
description: "Whether or not to enable buttons",
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
nameString: {
|
|
||||||
type: OptionType.STRING,
|
|
||||||
description: "Activity name format string",
|
|
||||||
default: "Apple Music"
|
|
||||||
},
|
|
||||||
detailsString: {
|
|
||||||
type: OptionType.STRING,
|
|
||||||
description: "Activity details format string",
|
|
||||||
default: "{name}"
|
|
||||||
},
|
|
||||||
stateString: {
|
|
||||||
type: OptionType.STRING,
|
|
||||||
description: "Activity state format string",
|
|
||||||
default: "{artist}"
|
|
||||||
},
|
|
||||||
largeImageType: {
|
|
||||||
type: OptionType.SELECT,
|
|
||||||
description: "Activity assets large image type",
|
|
||||||
options: [
|
|
||||||
{ label: "Album artwork", value: AssetImageType.Album, default: true },
|
|
||||||
{ label: "Artist artwork", value: AssetImageType.Artist }
|
|
||||||
],
|
|
||||||
},
|
|
||||||
largeTextString: {
|
|
||||||
type: OptionType.STRING,
|
|
||||||
description: "Activity assets large text format string",
|
|
||||||
default: "{album}"
|
|
||||||
},
|
|
||||||
smallImageType: {
|
|
||||||
type: OptionType.SELECT,
|
|
||||||
description: "Activity assets small image type",
|
|
||||||
options: [
|
|
||||||
{ label: "Album artwork", value: AssetImageType.Album },
|
|
||||||
{ label: "Artist artwork", value: AssetImageType.Artist, default: true }
|
|
||||||
],
|
|
||||||
},
|
|
||||||
smallTextString: {
|
|
||||||
type: OptionType.STRING,
|
|
||||||
description: "Activity assets small text format string",
|
|
||||||
default: "{artist}"
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
function customFormat(formatStr: string, data: TrackData) {
|
|
||||||
return formatStr
|
|
||||||
.replaceAll("{name}", data.name)
|
|
||||||
.replaceAll("{album}", data.album)
|
|
||||||
.replaceAll("{artist}", data.artist);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getImageAsset(type: AssetImageType, data: TrackData) {
|
|
||||||
const source = type === AssetImageType.Album
|
|
||||||
? data.albumArtwork
|
|
||||||
: data.artistArtwork;
|
|
||||||
|
|
||||||
if (!source) return undefined;
|
|
||||||
|
|
||||||
return ApplicationAssetUtils.fetchAssetIds(applicationId, [source]).then(ids => ids[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "AppleMusicRichPresence",
|
|
||||||
description: "Discord rich presence for your Apple Music!",
|
|
||||||
authors: [Devs.RyanCaoDev],
|
|
||||||
hidden: !navigator.platform.startsWith("Mac"),
|
|
||||||
|
|
||||||
settingsAboutComponent() {
|
|
||||||
return <>
|
|
||||||
<Forms.FormText>
|
|
||||||
For the customizable activity format strings, you can use several special strings to include track data in activities!{" "}
|
|
||||||
<code>{"{name}"}</code> is replaced with the track name; <code>{"{artist}"}</code> is replaced with the artist(s)' name(s); and <code>{"{album}"}</code> is replaced with the album name.
|
|
||||||
</Forms.FormText>
|
|
||||||
</>;
|
|
||||||
},
|
|
||||||
|
|
||||||
settings,
|
|
||||||
|
|
||||||
start() {
|
|
||||||
this.updatePresence();
|
|
||||||
this.updateInterval = setInterval(() => { this.updatePresence(); }, settings.store.refreshInterval * 1000);
|
|
||||||
},
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
clearInterval(this.updateInterval);
|
|
||||||
FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", activity: null });
|
|
||||||
},
|
|
||||||
|
|
||||||
updatePresence() {
|
|
||||||
this.getActivity().then(activity => { setActivity(activity); });
|
|
||||||
},
|
|
||||||
|
|
||||||
async getActivity(): Promise<Activity | null> {
|
|
||||||
const trackData = await Native.fetchTrackData();
|
|
||||||
if (!trackData) return null;
|
|
||||||
|
|
||||||
const [largeImageAsset, smallImageAsset] = await Promise.all([
|
|
||||||
getImageAsset(settings.store.largeImageType, trackData),
|
|
||||||
getImageAsset(settings.store.smallImageType, trackData)
|
|
||||||
]);
|
|
||||||
|
|
||||||
const assets: ActivityAssets = {
|
|
||||||
large_image: largeImageAsset,
|
|
||||||
large_text: customFormat(settings.store.largeTextString, trackData),
|
|
||||||
small_image: smallImageAsset,
|
|
||||||
small_text: customFormat(settings.store.smallTextString, trackData),
|
|
||||||
};
|
|
||||||
|
|
||||||
const buttons: ActivityButton[] = [];
|
|
||||||
|
|
||||||
if (settings.store.enableButtons) {
|
|
||||||
if (trackData.appleMusicLink)
|
|
||||||
buttons.push({
|
|
||||||
label: "Listen on Apple Music",
|
|
||||||
url: trackData.appleMusicLink,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (trackData.songLink)
|
|
||||||
buttons.push({
|
|
||||||
label: "View on SongLink",
|
|
||||||
url: trackData.songLink,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
application_id: applicationId,
|
|
||||||
|
|
||||||
name: customFormat(settings.store.nameString, trackData),
|
|
||||||
details: customFormat(settings.store.detailsString, trackData),
|
|
||||||
state: customFormat(settings.store.stateString, trackData),
|
|
||||||
|
|
||||||
timestamps: (settings.store.enableTimestamps ? {
|
|
||||||
start: Date.now() - (trackData.playerPosition * 1000),
|
|
||||||
end: Date.now() - (trackData.playerPosition * 1000) + (trackData.duration * 1000),
|
|
||||||
} : undefined),
|
|
||||||
|
|
||||||
assets,
|
|
||||||
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,120 +0,0 @@
|
||||||
/*
|
|
||||||
* Vencord, a Discord client mod
|
|
||||||
* Copyright (c) 2024 Vendicated and contributors
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { execFile } from "child_process";
|
|
||||||
import { promisify } from "util";
|
|
||||||
|
|
||||||
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,
|
|
||||||
albumArtwork?: string,
|
|
||||||
artistArtwork?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
let cachedRemoteData: { id: string, data: RemoteData; } | { id: string, failures: number; } | null = null;
|
|
||||||
|
|
||||||
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;
|
|
||||||
if ("failures" in cachedRemoteData && cachedRemoteData.failures >= 5) return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
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 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 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, songLink, albumArtwork, artistArtwork }
|
|
||||||
};
|
|
||||||
return cachedRemoteData.data;
|
|
||||||
} catch (e) {
|
|
||||||
console.error("[AppleMusicRichPresence] Failed to fetch remote data:", e);
|
|
||||||
cachedRemoteData = {
|
|
||||||
id,
|
|
||||||
failures: (id === cachedRemoteData?.id && "failures" in cachedRemoteData ? cachedRemoteData.failures : 0) + 1
|
|
||||||
};
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchTrackData(): Promise<TrackData | null> {
|
|
||||||
try {
|
|
||||||
await exec("pgrep", ["^Music$"]);
|
|
||||||
} catch (error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const playerState = await applescript(['tell application "Music"', "get player state", "end tell"])
|
|
||||||
.then(out => out.trim());
|
|
||||||
if (playerState !== "playing") return null;
|
|
||||||
|
|
||||||
const playerPosition = await applescript(['tell application "Music"', "get player position", "end tell"])
|
|
||||||
.then(text => Number.parseFloat(text.trim()));
|
|
||||||
|
|
||||||
const stdout = await applescript([
|
|
||||||
'set output to ""',
|
|
||||||
'tell application "Music"',
|
|
||||||
"set t_id to database id of current track",
|
|
||||||
"set t_name to name of current track",
|
|
||||||
"set t_album to album of current track",
|
|
||||||
"set t_artist to artist of current track",
|
|
||||||
"set t_duration to duration of current track",
|
|
||||||
'set output to "" & t_id & "\\n" & t_name & "\\n" & t_album & "\\n" & t_artist & "\\n" & t_duration',
|
|
||||||
"end tell",
|
|
||||||
"return output"
|
|
||||||
]);
|
|
||||||
|
|
||||||
const [id, name, album, artist, durationStr] = stdout.split("\n").filter(k => !!k);
|
|
||||||
const duration = Number.parseFloat(durationStr);
|
|
||||||
|
|
||||||
const remoteData = await fetchRemoteData({ id, name, artist, album });
|
|
||||||
|
|
||||||
return { name, album, artist, playerPosition, duration, ...remoteData };
|
|
||||||
}
|
|
|
@ -19,7 +19,7 @@
|
||||||
import { popNotice, showNotice } from "@api/Notices";
|
import { popNotice, showNotice } from "@api/Notices";
|
||||||
import { Link } from "@components/Link";
|
import { Link } from "@components/Link";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { ReporterTestable } from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
import { ApplicationAssetUtils, FluxDispatcher, Forms, Toasts } from "@webpack/common";
|
import { ApplicationAssetUtils, FluxDispatcher, Forms, Toasts } from "@webpack/common";
|
||||||
|
|
||||||
|
@ -41,7 +41,6 @@ export default definePlugin({
|
||||||
name: "WebRichPresence (arRPC)",
|
name: "WebRichPresence (arRPC)",
|
||||||
description: "Client plugin for arRPC to enable RPC on Discord Web (experimental)",
|
description: "Client plugin for arRPC to enable RPC on Discord Web (experimental)",
|
||||||
authors: [Devs.Ducko],
|
authors: [Devs.Ducko],
|
||||||
reporterTestable: ReporterTestable.None,
|
|
||||||
|
|
||||||
settingsAboutComponent: () => (
|
settingsAboutComponent: () => (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
# 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)
|
|
|
@ -1,73 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 })
|
|
||||||
});
|
|
|
@ -20,7 +20,7 @@ import { definePluginSettings } from "@api/Settings";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { findByPropsLazy, findStoreLazy } from "@webpack";
|
import { findByPropsLazy, findStoreLazy } from "@webpack";
|
||||||
import { FluxDispatcher, i18n, useMemo } from "@webpack/common";
|
import { FluxDispatcher, i18n } from "@webpack/common";
|
||||||
|
|
||||||
import FolderSideBar from "./FolderSideBar";
|
import FolderSideBar from "./FolderSideBar";
|
||||||
|
|
||||||
|
@ -112,13 +112,13 @@ export default definePlugin({
|
||||||
replacement: [
|
replacement: [
|
||||||
// Create the isBetterFolders variable in the GuildsBar component
|
// Create the isBetterFolders variable in the GuildsBar component
|
||||||
{
|
{
|
||||||
match: /let{disableAppDownload:\i=\i\.isPlatformEmbedded,isOverlay:.+?(?=}=\i,)/,
|
match: /(?<=let{disableAppDownload:\i=\i\.isPlatformEmbedded,isOverlay:.+?)(?=}=\i,)/,
|
||||||
replace: "$&,isBetterFolders"
|
replace: ",isBetterFolders"
|
||||||
},
|
},
|
||||||
// If we are rendering the Better Folders sidebar, we filter out guilds that are not in folders and unexpanded folders
|
// If we are rendering the Better Folders sidebar, we filter out guilds that are not in folders and unexpanded folders
|
||||||
{
|
{
|
||||||
match: /\[(\i)\]=(\(0,\i\.useStateFromStoresArray\).{0,40}getGuildsTree\(\).+?}\))(?=,)/,
|
match: /(useStateFromStoresArray\).{0,25}let \i)=(\i\.\i.getGuildsTree\(\))/,
|
||||||
replace: (_, originalTreeVar, rest) => `[betterFoldersOriginalTree]=${rest},${originalTreeVar}=$self.getGuildTree(!!arguments[0].isBetterFolders,betterFoldersOriginalTree,arguments[0].betterFoldersExpandedIds)`
|
replace: (_, rest, guildsTree) => `${rest}=$self.getGuildTree(!!arguments[0].isBetterFolders,${guildsTree},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
|
// If we are rendering the Better Folders sidebar, we filter out everything but the servers and folders from the GuildsBar Guild List children
|
||||||
{
|
{
|
||||||
|
@ -127,7 +127,7 @@ export default definePlugin({
|
||||||
},
|
},
|
||||||
// If we are rendering the Better Folders sidebar, we filter out everything but the scroller for the guild list from the GuildsBar Tree children
|
// 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,.+?}\)\]/,
|
match: /unreadMentionsIndicatorBottom,barClassName.+?}\)\]/,
|
||||||
replace: "$&.filter($self.makeGuildsBarTreeFilter(!!arguments[0].isBetterFolders))"
|
replace: "$&.filter($self.makeGuildsBarTreeFilter(!!arguments[0].isBetterFolders))"
|
||||||
},
|
},
|
||||||
// Export the isBetterFolders variable to the folders component
|
// Export the isBetterFolders variable to the folders component
|
||||||
|
@ -209,7 +209,7 @@ export default definePlugin({
|
||||||
predicate: () => settings.store.closeAllHomeButton,
|
predicate: () => settings.store.closeAllHomeButton,
|
||||||
replacement: {
|
replacement: {
|
||||||
// Close all folders when clicking the home button
|
// Close all folders when clicking the home button
|
||||||
match: /(?<=onClick:\(\)=>{)(?=.{0,300}"discodo")/,
|
match: /(?<=onClick:\(\)=>{)(?=.{0,200}"discodo")/,
|
||||||
replace: "$self.closeFolders();"
|
replace: "$self.closeFolders();"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -252,21 +252,19 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getGuildTree(isBetterFolders: boolean, originalTree: any, expandedFolderIds?: Set<any>) {
|
getGuildTree(isBetterFolders: boolean, oldTree: any, expandedFolderIds?: Set<any>) {
|
||||||
return useMemo(() => {
|
if (!isBetterFolders || expandedFolderIds == null) return oldTree;
|
||||||
if (!isBetterFolders || expandedFolderIds == null) return originalTree;
|
|
||||||
|
|
||||||
const newTree = new GuildsTree();
|
const newTree = new GuildsTree();
|
||||||
// Children is every folder and guild which is not in a folder, this filters out only the expanded folders
|
// Children is every folder and guild which is not in a folder, this filters out only the expanded folders
|
||||||
newTree.root.children = originalTree.root.children.filter(guildOrFolder => expandedFolderIds.has(guildOrFolder.id));
|
newTree.root.children = oldTree.root.children.filter(guildOrFolder => expandedFolderIds.has(guildOrFolder.id));
|
||||||
// Nodes is every folder and guild, even if it's in a folder, this filters out only the expanded folders and guilds inside them
|
// Nodes is every folder and guild, even if it's in a folder, this filters out only the expanded folders and guilds inside them
|
||||||
newTree.nodes = Object.fromEntries(
|
newTree.nodes = Object.fromEntries(
|
||||||
Object.entries(originalTree.nodes)
|
Object.entries(oldTree.nodes)
|
||||||
.filter(([_, guildOrFolder]: any[]) => expandedFolderIds.has(guildOrFolder.id) || expandedFolderIds.has(guildOrFolder.parentId))
|
.filter(([_, guildOrFolder]: any[]) => expandedFolderIds.has(guildOrFolder.id) || expandedFolderIds.has(guildOrFolder.parentId))
|
||||||
);
|
);
|
||||||
|
|
||||||
return newTree;
|
return newTree;
|
||||||
}, [isBetterFolders, originalTree, expandedFolderIds]);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
makeGuildsBarGuildListFilter(isBetterFolders: boolean) {
|
makeGuildsBarGuildListFilter(isBetterFolders: boolean) {
|
||||||
|
@ -281,7 +279,7 @@ export default definePlugin({
|
||||||
makeGuildsBarTreeFilter(isBetterFolders: boolean) {
|
makeGuildsBarTreeFilter(isBetterFolders: boolean) {
|
||||||
return child => {
|
return child => {
|
||||||
if (isBetterFolders) {
|
if (isBetterFolders) {
|
||||||
return child?.props?.onScroll != null;
|
return "onScroll" in child.props;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
|
@ -26,7 +26,7 @@ export default definePlugin({
|
||||||
"Change GIF alt text from simply being 'GIF' to containing the gif tags / filename",
|
"Change GIF alt text from simply being 'GIF' to containing the gif tags / filename",
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: '"onCloseImage",',
|
find: "onCloseImage=",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(return.{0,10}\.jsx.{0,50}isWindowFocused)/,
|
match: /(return.{0,10}\.jsx.{0,50}isWindowFocused)/,
|
||||||
replace:
|
replace:
|
||||||
|
|
|
@ -15,8 +15,8 @@ export default definePlugin({
|
||||||
{
|
{
|
||||||
find: ".GIFPickerResultTypes.SEARCH",
|
find: ".GIFPickerResultTypes.SEARCH",
|
||||||
replacement: [{
|
replacement: [{
|
||||||
match: /(?<="state",{resultType:)null/,
|
match: "this.state={resultType:null}",
|
||||||
replace: '"Favorites"'
|
replace: 'this.state={resultType:"Favorites"}'
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -17,9 +17,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Settings } from "@api/Settings";
|
import { Settings } from "@api/Settings";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { canonicalizeMatch } from "@utils/patches";
|
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
|
|
||||||
|
@ -41,12 +39,8 @@ export default definePlugin({
|
||||||
match: /hideNote:.+?(?=([,}].*?\)))/g,
|
match: /hideNote:.+?(?=([,}].*?\)))/g,
|
||||||
replace: (m, rest) => {
|
replace: (m, rest) => {
|
||||||
const destructuringMatch = rest.match(/}=.+/);
|
const destructuringMatch = rest.match(/}=.+/);
|
||||||
if (destructuringMatch) {
|
if (destructuringMatch == null) return "hideNote:!0";
|
||||||
const defaultValueMatch = m.match(canonicalizeMatch(/hideNote:(\i)=!?\d/));
|
return m;
|
||||||
return defaultValueMatch ? `hideNote:${defaultValueMatch[1]}=!0` : m;
|
|
||||||
}
|
|
||||||
|
|
||||||
return "hideNote:!0";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -58,10 +52,10 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: ".popularApplicationCommandIds,",
|
find: ".Messages.NOTE}",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /lastSection:(!?\i)}\),/,
|
match: /(?<=return \i\?)null(?=:\(0,\i\.jsxs)/,
|
||||||
replace: "$&$self.patchPadding({lastSection:$1}),"
|
replace: "$self.patchPadding(arguments[0])"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -81,10 +75,10 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
patchPadding: ErrorBoundary.wrap(({ lastSection }) => {
|
patchPadding(e: any) {
|
||||||
if (!lastSection) return null;
|
if (!e.lastSection) return;
|
||||||
return (
|
return (
|
||||||
<div className={UserPopoutSectionCssClasses.lastSection} ></div>
|
<div className={UserPopoutSectionCssClasses.lastSection}></div>
|
||||||
);
|
);
|
||||||
})
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# BetterRoleContext
|
# BetterRoleContext
|
||||||
|
|
||||||
Adds options to copy role color, edit role and view role icon when right clicking roles in the user profile
|
Adds options to copy role color and edit role when right clicking roles in the user profile
|
||||||
|
|
||||||
![](https://github.com/Vendicated/Vencord/assets/45497981/354220a4-09f3-4c5f-a28e-4b19ca775190)
|
![](https://github.com/Vendicated/Vencord/assets/45497981/d1765e9e-7db2-4a3c-b110-139c59235326)
|
||||||
|
|
||||||
|
|
|
@ -4,13 +4,11 @@
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { definePluginSettings } from "@api/Settings";
|
|
||||||
import { ImageIcon } from "@components/Icons";
|
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { getCurrentGuild, openImageModal } from "@utils/discord";
|
import { getCurrentGuild, getGuildRoles } from "@utils/discord";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
import { Clipboard, GuildStore, Menu, PermissionStore, TextAndImagesSettingsStores } from "@webpack/common";
|
import { Clipboard, Menu, PermissionStore, TextAndImagesSettingsStores } from "@webpack/common";
|
||||||
|
|
||||||
const GuildSettingsActions = findByPropsLazy("open", "selectRole", "updateGuild");
|
const GuildSettingsActions = findByPropsLazy("open", "selectRole", "updateGuild");
|
||||||
|
|
||||||
|
@ -36,34 +34,10 @@ function AppearanceIcon() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
|
||||||
roleIconFileFormat: {
|
|
||||||
type: OptionType.SELECT,
|
|
||||||
description: "File format to use when viewing role icons",
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
label: "png",
|
|
||||||
value: "png",
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "webp",
|
|
||||||
value: "webp",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "jpg",
|
|
||||||
value: "jpg"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "BetterRoleContext",
|
name: "BetterRoleContext",
|
||||||
description: "Adds options to copy role color / edit role / view role icon when right clicking roles in the user profile",
|
description: "Adds options to copy role color / edit role when right clicking roles in the user profile",
|
||||||
authors: [Devs.Ven, Devs.goodbee],
|
authors: [Devs.Ven],
|
||||||
|
|
||||||
settings,
|
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
// DeveloperMode needs to be enabled for the context menu to be shown
|
// DeveloperMode needs to be enabled for the context menu to be shown
|
||||||
|
@ -75,7 +49,7 @@ export default definePlugin({
|
||||||
const guild = getCurrentGuild();
|
const guild = getCurrentGuild();
|
||||||
if (!guild) return;
|
if (!guild) return;
|
||||||
|
|
||||||
const role = GuildStore.getRole(guild.id, id);
|
const role = getGuildRoles(guild.id)[id];
|
||||||
if (!role) return;
|
if (!role) return;
|
||||||
|
|
||||||
if (role.colorString) {
|
if (role.colorString) {
|
||||||
|
@ -89,20 +63,6 @@ export default definePlugin({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role.icon) {
|
|
||||||
children.push(
|
|
||||||
<Menu.MenuItem
|
|
||||||
id="vc-view-role-icon"
|
|
||||||
label="View Role Icon"
|
|
||||||
action={() => {
|
|
||||||
openImageModal(`${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/role-icons/${role.id}/${role.icon}.${settings.store.roleIconFileFormat}`);
|
|
||||||
}}
|
|
||||||
icon={ImageIcon}
|
|
||||||
/>
|
|
||||||
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (PermissionStore.getGuildPermissionProps(guild).canManageRoles) {
|
if (PermissionStore.getGuildPermissionProps(guild).canManageRoles) {
|
||||||
children.push(
|
children.push(
|
||||||
<Menu.MenuItem
|
<Menu.MenuItem
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
# BetterSessions
|
|
||||||
|
|
||||||
Enhances the sessions (devices) menu. Allows you to view exact timestamps, give each session a custom name, and receive notifications about new sessions.
|
|
||||||
|
|
||||||
![](https://github.com/Vendicated/Vencord/assets/9750071/4a44b617-bb8f-4dcb-93f1-b7d2575ed3d8)
|
|
|
@ -1,37 +0,0 @@
|
||||||
/*
|
|
||||||
* Vencord, a Discord client mod
|
|
||||||
* Copyright (c) 2024 Vendicated and contributors
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { openModal } from "@utils/modal";
|
|
||||||
import { Button } from "@webpack/common";
|
|
||||||
|
|
||||||
import { SessionInfo } from "../types";
|
|
||||||
import { RenameModal } from "./RenameModal";
|
|
||||||
|
|
||||||
export function RenameButton({ session, state }: { session: SessionInfo["session"], state: [string, React.Dispatch<React.SetStateAction<string>>]; }) {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
look={Button.Looks.LINK}
|
|
||||||
color={Button.Colors.LINK}
|
|
||||||
size={Button.Sizes.NONE}
|
|
||||||
style={{
|
|
||||||
paddingTop: "0px",
|
|
||||||
paddingBottom: "0px",
|
|
||||||
top: "-2px"
|
|
||||||
}}
|
|
||||||
onClick={() =>
|
|
||||||
openModal(props => (
|
|
||||||
<RenameModal
|
|
||||||
props={props}
|
|
||||||
session={session}
|
|
||||||
state={state}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Rename
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,94 +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 { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot } from "@utils/modal";
|
|
||||||
import { Button, Forms, React, TextInput } from "@webpack/common";
|
|
||||||
import { KeyboardEvent } from "react";
|
|
||||||
|
|
||||||
import { SessionInfo } from "../types";
|
|
||||||
import { getDefaultName, savedSessionsCache, saveSessionsToDataStore } from "../utils";
|
|
||||||
|
|
||||||
export function RenameModal({ props, session, state }: { props: ModalProps, session: SessionInfo["session"], state: [string, React.Dispatch<React.SetStateAction<string>>]; }) {
|
|
||||||
const [title, setTitle] = state;
|
|
||||||
const [value, setValue] = React.useState(savedSessionsCache.get(session.id_hash)?.name ?? "");
|
|
||||||
|
|
||||||
function onSaveClick() {
|
|
||||||
savedSessionsCache.set(session.id_hash, { name: value, isNew: false });
|
|
||||||
if (value !== "") {
|
|
||||||
setTitle(`${value}*`);
|
|
||||||
} else {
|
|
||||||
setTitle(getDefaultName(session.client_info));
|
|
||||||
}
|
|
||||||
|
|
||||||
saveSessionsToDataStore();
|
|
||||||
props.onClose();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalRoot {...props}>
|
|
||||||
<ModalHeader>
|
|
||||||
<Forms.FormTitle tag="h4">Rename</Forms.FormTitle>
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalContent>
|
|
||||||
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>New device name</Forms.FormTitle>
|
|
||||||
<TextInput
|
|
||||||
style={{ marginBottom: "10px" }}
|
|
||||||
placeholder={getDefaultName(session.client_info)}
|
|
||||||
value={value}
|
|
||||||
onChange={setValue}
|
|
||||||
onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
onSaveClick();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
style={{
|
|
||||||
marginBottom: "20px",
|
|
||||||
paddingLeft: "1px",
|
|
||||||
paddingRight: "1px",
|
|
||||||
opacity: 0.6
|
|
||||||
}}
|
|
||||||
look={Button.Looks.LINK}
|
|
||||||
color={Button.Colors.LINK}
|
|
||||||
size={Button.Sizes.NONE}
|
|
||||||
onClick={() => setValue("")}
|
|
||||||
>
|
|
||||||
Reset Name
|
|
||||||
</Button>
|
|
||||||
</ModalContent>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
<Button
|
|
||||||
color={Button.Colors.BRAND}
|
|
||||||
onClick={onSaveClick}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
color={Button.Colors.TRANSPARENT}
|
|
||||||
look={Button.Looks.LINK}
|
|
||||||
onClick={() => props.onClose()}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalRoot >
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,106 +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 { LazyComponent } from "@utils/react";
|
|
||||||
import { findByCode } from "@webpack";
|
|
||||||
import { SVGProps } from "react";
|
|
||||||
|
|
||||||
export const DiscordIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (
|
|
||||||
<svg
|
|
||||||
{...props}
|
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
>
|
|
||||||
<path d="M13.545 2.907a13.227 13.227 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.19 12.19 0 0 0-3.658 0 8.258 8.258 0 0 0-.412-.833.051.051 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.041.041 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032c.001.014.01.028.021.037a13.276 13.276 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019c.308-.42.582-.863.818-1.329a.05.05 0 0 0-.01-.059.051.051 0 0 0-.018-.011 8.875 8.875 0 0 1-1.248-.595.05.05 0 0 1-.02-.066.051.051 0 0 1 .015-.019c.084-.063.168-.129.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.052.052 0 0 1 .053.007c.08.066.164.132.248.195a.051.051 0 0 1-.004.085 8.254 8.254 0 0 1-1.249.594.05.05 0 0 0-.03.03.052.052 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.235 13.235 0 0 0 4.001-2.02.049.049 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.034.034 0 0 0-.02-.019Zm-8.198 7.307c-.789 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612Zm5.316 0c-.788 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612Z" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const ChromeIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (
|
|
||||||
<svg
|
|
||||||
{...props}
|
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 512 512"
|
|
||||||
>
|
|
||||||
<path d="M188.8,255.93A67.2,67.2,0,1,0,256,188.75,67.38,67.38,0,0,0,188.8,255.93Z" />
|
|
||||||
<path d="M476.75,217.79s0,0,0,.05a206.63,206.63,0,0,0-7-28.84h-.11a202.16,202.16,0,0,1,7.07,29h0a203.5,203.5,0,0,0-7.07-29H314.24c19.05,17,31.36,40.17,31.36,67.05a86.55,86.55,0,0,1-12.31,44.73L231,478.45a2.44,2.44,0,0,1,0,.27V479h0v-.26A224,224,0,0,0,256,480c6.84,0,13.61-.39,20.3-1a222.91,222.91,0,0,0,29.78-4.74C405.68,451.52,480,362.4,480,255.94A225.25,225.25,0,0,0,476.75,217.79Z" />
|
|
||||||
<path d="M256,345.5c-33.6,0-61.6-17.91-77.29-44.79L76,123.05l-.14-.24A224,224,0,0,0,207.4,474.55l0-.05,77.69-134.6A84.13,84.13,0,0,1,256,345.5Z" />
|
|
||||||
<path d="M91.29,104.57l77.35,133.25A89.19,89.19,0,0,1,256,166H461.17a246.51,246.51,0,0,0-25.78-43.94l.12.08A245.26,245.26,0,0,1,461.17,166h.17a245.91,245.91,0,0,0-25.66-44,2.63,2.63,0,0,1-.35-.26A223.93,223.93,0,0,0,91.14,104.34l.14.24Z" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const EdgeIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (
|
|
||||||
<svg
|
|
||||||
{...props}
|
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path d="M21.86 17.86q.14 0 .25.12.1.13.1.25t-.11.33l-.32.46-.43.53-.44.5q-.21.25-.38.42l-.22.23q-.58.53-1.34 1.04-.76.51-1.6.91-.86.4-1.74.64t-1.67.24q-.9 0-1.69-.28-.8-.28-1.48-.78-.68-.5-1.22-1.17-.53-.66-.92-1.44-.38-.77-.58-1.6-.2-.83-.2-1.67 0-1 .32-1.96.33-.97.87-1.8.14.95.55 1.77.41.82 1.02 1.5.6.68 1.38 1.21.78.54 1.64.9.86.36 1.77.56.92.2 1.8.2 1.12 0 2.18-.24 1.06-.23 2.06-.72l.2-.1.2-.05zm-15.5-1.27q0 1.1.27 2.15.27 1.06.78 2.03.51.96 1.24 1.77.74.82 1.66 1.4-1.47-.2-2.8-.74-1.33-.55-2.48-1.37-1.15-.83-2.08-1.9-.92-1.07-1.58-2.33T.36 14.94Q0 13.54 0 12.06q0-.81.32-1.49.31-.68.83-1.23.53-.55 1.2-.96.66-.4 1.35-.66.74-.27 1.5-.39.78-.12 1.55-.12.7 0 1.42.1.72.12 1.4.35.68.23 1.32.57.63.35 1.16.83-.35 0-.7.07-.33.07-.65.23v-.02q-.63.28-1.2.74-.57.46-1.05 1.04-.48.58-.87 1.26-.38.67-.65 1.39-.27.71-.42 1.44-.15.72-.15 1.38zM11.96.06q1.7 0 3.33.39 1.63.38 3.07 1.15 1.43.77 2.62 1.93 1.18 1.16 1.98 2.7.49.94.76 1.96.28 1 .28 2.08 0 .89-.23 1.7-.24.8-.69 1.48-.45.68-1.1 1.22-.64.53-1.45.88-.54.24-1.11.36-.58.13-1.16.13-.42 0-.97-.03-.54-.03-1.1-.12-.55-.1-1.05-.28-.5-.19-.84-.5-.12-.09-.23-.24-.1-.16-.1-.33 0-.15.16-.35.16-.2.35-.5.2-.28.36-.68.16-.4.16-.95 0-1.06-.4-1.96-.4-.91-1.06-1.64-.66-.74-1.52-1.28-.86-.55-1.79-.89-.84-.3-1.72-.44-.87-.14-1.76-.14-1.55 0-3.06.45T.94 7.55q.71-1.74 1.81-3.13 1.1-1.38 2.52-2.35Q6.68 1.1 8.37.58q1.7-.52 3.58-.52Z" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const FirefoxIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (
|
|
||||||
<svg
|
|
||||||
{...props}
|
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 512 512"
|
|
||||||
>
|
|
||||||
<path d="M130.22 127.548C130.38 127.558 130.3 127.558 130.22 127.548V127.548ZM481.64 172.898C471.03 147.398 449.56 119.898 432.7 111.168C446.42 138.058 454.37 165.048 457.4 185.168C457.405 185.306 457.422 185.443 457.45 185.578C429.87 116.828 383.098 89.1089 344.9 28.7479C329.908 5.05792 333.976 3.51792 331.82 4.08792L331.7 4.15792C284.99 30.1109 256.365 82.5289 249.12 126.898C232.503 127.771 216.219 131.895 201.19 139.035C199.838 139.649 198.736 140.706 198.066 142.031C197.396 143.356 197.199 144.87 197.506 146.323C197.7 147.162 198.068 147.951 198.586 148.639C199.103 149.327 199.76 149.899 200.512 150.318C201.264 150.737 202.096 150.993 202.954 151.071C203.811 151.148 204.676 151.045 205.491 150.768L206.011 150.558C221.511 143.255 238.408 139.393 255.541 139.238C318.369 138.669 352.698 183.262 363.161 201.528C350.161 192.378 326.811 183.338 304.341 187.248C392.081 231.108 368.541 381.784 246.951 376.448C187.487 373.838 149.881 325.467 146.421 285.648C146.421 285.648 157.671 243.698 227.041 243.698C234.541 243.698 255.971 222.778 256.371 216.698C256.281 214.698 213.836 197.822 197.281 181.518C188.434 172.805 184.229 168.611 180.511 165.458C178.499 163.75 176.392 162.158 174.201 160.688C168.638 141.231 168.399 120.638 173.51 101.058C148.45 112.468 128.96 130.508 114.8 146.428H114.68C105.01 134.178 105.68 93.7779 106.25 85.3479C106.13 84.8179 99.022 89.0159 98.1 89.6579C89.5342 95.7103 81.5528 102.55 74.26 110.088C57.969 126.688 30.128 160.242 18.76 211.318C14.224 231.701 12 255.739 12 263.618C12 398.318 121.21 507.508 255.92 507.508C376.56 507.508 478.939 420.281 496.35 304.888C507.922 228.192 481.64 173.82 481.64 172.898Z" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const IEIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (
|
|
||||||
<svg
|
|
||||||
{...props}
|
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 512 512"
|
|
||||||
>
|
|
||||||
<path d="M483.049 159.706c10.855-24.575 21.424-60.438 21.424-87.871 0-72.722-79.641-98.371-209.673-38.577-107.632-7.181-211.221 73.67-237.098 186.457 30.852-34.862 78.271-82.298 121.977-101.158C125.404 166.85 79.128 228.002 43.992 291.725 23.246 329.651 0 390.94 0 436.747c0 98.575 92.854 86.5 180.251 42.006 31.423 15.43 66.559 15.573 101.695 15.573 97.124 0 184.249-54.294 216.814-146.022H377.927c-52.509 88.593-196.819 52.996-196.819-47.436H509.9c6.407-43.581-1.655-95.715-26.851-141.162zM64.559 346.877c17.711 51.15 53.703 95.871 100.266 123.304-88.741 48.94-173.267 29.096-100.266-123.304zm115.977-108.873c2-55.151 50.276-94.871 103.98-94.871 53.418 0 101.981 39.72 103.981 94.871H180.536zm184.536-187.6c21.425-10.287 48.563-22.003 72.558-22.003 31.422 0 54.274 21.717 54.274 53.722 0 20.003-7.427 49.007-14.569 67.867-26.28-42.292-65.986-81.584-112.263-99.586z" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const OperaIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (
|
|
||||||
<svg
|
|
||||||
{...props}
|
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 496 512"
|
|
||||||
>
|
|
||||||
<path d="M313.9 32.7c-170.2 0-252.6 223.8-147.5 355.1 36.5 45.4 88.6 75.6 147.5 75.6 36.3 0 70.3-11.1 99.4-30.4-43.8 39.2-101.9 63-165.3 63-3.9 0-8 0-11.9-.3C104.6 489.6 0 381.1 0 248 0 111 111 0 248 0h.8c63.1.3 120.7 24.1 164.4 63.1-29-19.4-63.1-30.4-99.3-30.4zm101.8 397.7c-40.9 24.7-90.7 23.6-132-5.8 56.2-20.5 97.7-91.6 97.7-176.6 0-84.7-41.2-155.8-97.4-176.6 41.8-29.2 91.2-30.3 132.9-5 105.9 98.7 105.5 265.7-1.2 364z" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const SafariIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (
|
|
||||||
<svg
|
|
||||||
{...props}
|
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 512 512"
|
|
||||||
>
|
|
||||||
<path d="M274.69,274.69l-37.38-37.38L166,346ZM256,8C119,8,8,119,8,256S119,504,256,504,504,393,504,256,393,8,256,8ZM411.85,182.79l14.78-6.13A8,8,0,0,1,437.08,181h0a8,8,0,0,1-4.33,10.46L418,197.57a8,8,0,0,1-10.45-4.33h0A8,8,0,0,1,411.85,182.79ZM314.43,94l6.12-14.78A8,8,0,0,1,331,74.92h0a8,8,0,0,1,4.33,10.45l-6.13,14.78a8,8,0,0,1-10.45,4.33h0A8,8,0,0,1,314.43,94ZM256,60h0a8,8,0,0,1,8,8V84a8,8,0,0,1-8,8h0a8,8,0,0,1-8-8V68A8,8,0,0,1,256,60ZM181,74.92a8,8,0,0,1,10.46,4.33L197.57,94a8,8,0,1,1-14.78,6.12l-6.13-14.78A8,8,0,0,1,181,74.92Zm-63.58,42.49h0a8,8,0,0,1,11.31,0L140,128.72A8,8,0,0,1,140,140h0a8,8,0,0,1-11.31,0l-11.31-11.31A8,8,0,0,1,117.41,117.41ZM60,256h0a8,8,0,0,1,8-8H84a8,8,0,0,1,8,8h0a8,8,0,0,1-8,8H68A8,8,0,0,1,60,256Zm40.15,73.21-14.78,6.13A8,8,0,0,1,74.92,331h0a8,8,0,0,1,4.33-10.46L94,314.43a8,8,0,0,1,10.45,4.33h0A8,8,0,0,1,100.15,329.21Zm4.33-136h0A8,8,0,0,1,94,197.57l-14.78-6.12A8,8,0,0,1,74.92,181h0a8,8,0,0,1,10.45-4.33l14.78,6.13A8,8,0,0,1,104.48,193.24ZM197.57,418l-6.12,14.78a8,8,0,0,1-14.79-6.12l6.13-14.78A8,8,0,1,1,197.57,418ZM264,444a8,8,0,0,1-8,8h0a8,8,0,0,1-8-8V428a8,8,0,0,1,8-8h0a8,8,0,0,1,8,8Zm67-6.92h0a8,8,0,0,1-10.46-4.33L314.43,418a8,8,0,0,1,4.33-10.45h0a8,8,0,0,1,10.45,4.33l6.13,14.78A8,8,0,0,1,331,437.08Zm63.58-42.49h0a8,8,0,0,1-11.31,0L372,383.28A8,8,0,0,1,372,372h0a8,8,0,0,1,11.31,0l11.31,11.31A8,8,0,0,1,394.59,394.59ZM286.25,286.25,110.34,401.66,225.75,225.75,401.66,110.34ZM437.08,331h0a8,8,0,0,1-10.45,4.33l-14.78-6.13a8,8,0,0,1-4.33-10.45h0A8,8,0,0,1,418,314.43l14.78,6.12A8,8,0,0,1,437.08,331ZM444,264H428a8,8,0,0,1-8-8h0a8,8,0,0,1,8-8h16a8,8,0,0,1,8,8h0A8,8,0,0,1,444,264Z" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const UnknownIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElement>>) => (
|
|
||||||
<svg
|
|
||||||
{...props}
|
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
>
|
|
||||||
<path fill-rule="evenodd" d="M4.475 5.458c-.284 0-.514-.237-.47-.517C4.28 3.24 5.576 2 7.825 2c2.25 0 3.767 1.36 3.767 3.215 0 1.344-.665 2.288-1.79 2.973-1.1.659-1.414 1.118-1.414 2.01v.03a.5.5 0 0 1-.5.5h-.77a.5.5 0 0 1-.5-.495l-.003-.2c-.043-1.221.477-2.001 1.645-2.712 1.03-.632 1.397-1.135 1.397-2.028 0-.979-.758-1.698-1.926-1.698-1.009 0-1.71.529-1.938 1.402-.066.254-.278.461-.54.461h-.777ZM7.496 14c.622 0 1.095-.474 1.095-1.09 0-.618-.473-1.092-1.095-1.092-.606 0-1.087.474-1.087 1.091S6.89 14 7.496 14Z" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const MobileIcon = LazyComponent(() => findByCode("M15.5 1h-8C6.12 1 5 2.12 5 3.5v17C5 21.88 6.12 23 7.5 23h8c1.38"));
|
|
|
@ -1,227 +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 { showNotification } from "@api/Notifications";
|
|
||||||
import { definePluginSettings } from "@api/Settings";
|
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
|
||||||
import { Devs } from "@utils/constants";
|
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
|
||||||
import { findByPropsLazy, findExportedComponentLazy, findStoreLazy } from "@webpack";
|
|
||||||
import { Constants, React, RestAPI, Tooltip } from "@webpack/common";
|
|
||||||
|
|
||||||
import { RenameButton } from "./components/RenameButton";
|
|
||||||
import { Session, SessionInfo } from "./types";
|
|
||||||
import { fetchNamesFromDataStore, getDefaultName, GetOsColor, GetPlatformIcon, savedSessionsCache, saveSessionsToDataStore } from "./utils";
|
|
||||||
|
|
||||||
const AuthSessionsStore = findStoreLazy("AuthSessionsStore");
|
|
||||||
const UserSettingsModal = findByPropsLazy("saveAccountChanges", "open");
|
|
||||||
|
|
||||||
const TimestampClasses = findByPropsLazy("timestampTooltip", "blockquoteContainer");
|
|
||||||
const SessionIconClasses = findByPropsLazy("sessionIcon");
|
|
||||||
|
|
||||||
const BlobMask = findExportedComponentLazy("BlobMask");
|
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
|
||||||
backgroundCheck: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
description: "Check for new sessions in the background, and display notifications when they are detected",
|
|
||||||
default: false,
|
|
||||||
restartNeeded: true
|
|
||||||
},
|
|
||||||
checkInterval: {
|
|
||||||
description: "How often to check for new sessions in the background (if enabled), in minutes",
|
|
||||||
type: OptionType.NUMBER,
|
|
||||||
default: 20,
|
|
||||||
restartNeeded: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "BetterSessions",
|
|
||||||
description: "Enhances the sessions (devices) menu. Allows you to view exact timestamps, give each session a custom name, and receive notifications about new sessions.",
|
|
||||||
authors: [Devs.amia],
|
|
||||||
|
|
||||||
settings: settings,
|
|
||||||
|
|
||||||
patches: [
|
|
||||||
{
|
|
||||||
find: "Messages.AUTH_SESSIONS_SESSION_LOG_OUT",
|
|
||||||
replacement: [
|
|
||||||
// Replace children with a single label with state
|
|
||||||
{
|
|
||||||
match: /({variant:"eyebrow",className:\i\.sessionInfoRow,children:).{70,110}{children:"\\xb7"}\),\(0,\i\.\i\)\("span",{children:\i\[\d+\]}\)\]}\)\]/,
|
|
||||||
replace: "$1$self.renderName(arguments[0])"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
match: /({variant:"text-sm\/medium",className:\i\.sessionInfoRow,children:.{70,110}{children:"\\xb7"}\),\(0,\i\.\i\)\("span",{children:)(\i\[\d+\])}/,
|
|
||||||
replace: "$1$self.renderTimestamp({ ...arguments[0], timeLabel: $2 })}"
|
|
||||||
},
|
|
||||||
// Replace the icon
|
|
||||||
{
|
|
||||||
match: /\.currentSession:null\),children:\[(?<=,icon:(\i)\}.+?)/,
|
|
||||||
replace: "$& $self.renderIcon({ ...arguments[0], DeviceIcon: $1 }), false &&"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Add the ability to change BlobMask's lower badge height
|
|
||||||
// (it allows changing width so we can mirror that logic)
|
|
||||||
find: "this.getBadgePositionInterpolation(",
|
|
||||||
replacement: {
|
|
||||||
match: /(\i\.animated\.rect,{id:\i,x:48-\(\i\+8\)\+4,y:)28(,width:\i\+8,height:)24,/,
|
|
||||||
replace: (_, leftPart, rightPart) => `${leftPart} 48 - ((this.props.lowerBadgeHeight ?? 16) + 8) + 4 ${rightPart} (this.props.lowerBadgeHeight ?? 16) + 8,`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
renderName: ErrorBoundary.wrap(({ session }: SessionInfo) => {
|
|
||||||
const savedSession = savedSessionsCache.get(session.id_hash);
|
|
||||||
|
|
||||||
const state = React.useState(savedSession?.name ? `${savedSession.name}*` : getDefaultName(session.client_info));
|
|
||||||
const [title, setTitle] = state;
|
|
||||||
|
|
||||||
// Show a "NEW" badge if the session is seen for the first time
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<span>{title}</span>
|
|
||||||
{(savedSession == null || savedSession.isNew) && (
|
|
||||||
<div
|
|
||||||
className="vc-plugins-badge"
|
|
||||||
style={{
|
|
||||||
backgroundColor: "#ED4245",
|
|
||||||
marginLeft: "2px"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
NEW
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<RenameButton session={session} state={state} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}, { noop: true }),
|
|
||||||
|
|
||||||
renderTimestamp: ErrorBoundary.wrap(({ session, timeLabel }: { session: Session, timeLabel: string; }) => {
|
|
||||||
return (
|
|
||||||
<Tooltip text={session.approx_last_used_time.toLocaleString()} tooltipClassName={TimestampClasses.timestampTooltip}>
|
|
||||||
{props => (
|
|
||||||
<span {...props} className={TimestampClasses.timestamp}>
|
|
||||||
{timeLabel}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}, { noop: true }),
|
|
||||||
|
|
||||||
renderIcon: ErrorBoundary.wrap(({ session, DeviceIcon }: { session: Session, DeviceIcon: React.ComponentType<any>; }) => {
|
|
||||||
const PlatformIcon = GetPlatformIcon(session.client_info.platform);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BlobMask
|
|
||||||
style={{ cursor: "unset" }}
|
|
||||||
selected={false}
|
|
||||||
lowerBadge={
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: "20px",
|
|
||||||
height: "20px",
|
|
||||||
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
overflow: "hidden",
|
|
||||||
|
|
||||||
borderRadius: "50%",
|
|
||||||
backgroundColor: "var(--interactive-normal)",
|
|
||||||
color: "var(--background-secondary)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PlatformIcon width={14} height={14} />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
lowerBadgeWidth={20}
|
|
||||||
lowerBadgeHeight={20}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={SessionIconClasses.sessionIcon}
|
|
||||||
style={{ backgroundColor: GetOsColor(session.client_info.os) }}
|
|
||||||
>
|
|
||||||
<DeviceIcon width={28} height={28} />
|
|
||||||
</div>
|
|
||||||
</BlobMask>
|
|
||||||
);
|
|
||||||
}, { noop: true }),
|
|
||||||
|
|
||||||
async checkNewSessions() {
|
|
||||||
const data = await RestAPI.get({
|
|
||||||
url: Constants.Endpoints.AUTH_SESSIONS
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const session of data.body.user_sessions) {
|
|
||||||
if (savedSessionsCache.has(session.id_hash)) continue;
|
|
||||||
|
|
||||||
savedSessionsCache.set(session.id_hash, { name: "", isNew: true });
|
|
||||||
showNotification({
|
|
||||||
title: "BetterSessions",
|
|
||||||
body: `New session:\n${session.client_info.os} · ${session.client_info.platform} · ${session.client_info.location}`,
|
|
||||||
permanent: true,
|
|
||||||
onClick: () => UserSettingsModal.open("Sessions")
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
saveSessionsToDataStore();
|
|
||||||
},
|
|
||||||
|
|
||||||
flux: {
|
|
||||||
USER_SETTINGS_ACCOUNT_RESET_AND_CLOSE_FORM() {
|
|
||||||
const lastFetchedHashes: string[] = AuthSessionsStore.getSessions().map((session: SessionInfo["session"]) => session.id_hash);
|
|
||||||
|
|
||||||
// Add new sessions to cache
|
|
||||||
lastFetchedHashes.forEach(idHash => {
|
|
||||||
if (!savedSessionsCache.has(idHash)) savedSessionsCache.set(idHash, { name: "", isNew: false });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Delete removed sessions from cache
|
|
||||||
if (lastFetchedHashes.length > 0) {
|
|
||||||
savedSessionsCache.forEach((_, idHash) => {
|
|
||||||
if (!lastFetchedHashes.includes(idHash)) savedSessionsCache.delete(idHash);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dismiss the "NEW" badge of all sessions.
|
|
||||||
// Since the only way for a session to be marked as "NEW" is going to the Devices tab,
|
|
||||||
// closing the settings means they've been viewed and are no longer considered new.
|
|
||||||
savedSessionsCache.forEach(data => {
|
|
||||||
data.isNew = false;
|
|
||||||
});
|
|
||||||
saveSessionsToDataStore();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async start() {
|
|
||||||
await fetchNamesFromDataStore();
|
|
||||||
|
|
||||||
this.checkNewSessions();
|
|
||||||
if (settings.store.backgroundCheck) {
|
|
||||||
this.checkInterval = setInterval(this.checkNewSessions, settings.store.checkInterval * 60 * 1000);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
clearInterval(this.checkInterval);
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,32 +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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface SessionInfo {
|
|
||||||
session: {
|
|
||||||
id_hash: string;
|
|
||||||
approx_last_used_time: Date;
|
|
||||||
client_info: {
|
|
||||||
os: string;
|
|
||||||
platform: string;
|
|
||||||
location: string;
|
|
||||||
};
|
|
||||||
},
|
|
||||||
current?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Session = SessionInfo["session"];
|
|
|
@ -1,90 +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 { DataStore } from "@api/index";
|
|
||||||
import { UserStore } from "@webpack/common";
|
|
||||||
|
|
||||||
import { ChromeIcon, DiscordIcon, EdgeIcon, FirefoxIcon, IEIcon, MobileIcon, OperaIcon, SafariIcon, UnknownIcon } from "./components/icons";
|
|
||||||
import { SessionInfo } from "./types";
|
|
||||||
|
|
||||||
const getDataKey = () => `BetterSessions_savedSessions_${UserStore.getCurrentUser().id}`;
|
|
||||||
|
|
||||||
export const savedSessionsCache: Map<string, { name: string, isNew: boolean; }> = new Map();
|
|
||||||
|
|
||||||
export function getDefaultName(clientInfo: SessionInfo["session"]["client_info"]) {
|
|
||||||
return `${clientInfo.os} · ${clientInfo.platform}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function saveSessionsToDataStore() {
|
|
||||||
return DataStore.set(getDataKey(), savedSessionsCache);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchNamesFromDataStore() {
|
|
||||||
const savedSessions = await DataStore.get<Map<string, { name: string, isNew: boolean; }>>(getDataKey()) || new Map();
|
|
||||||
savedSessions.forEach((data, idHash) => {
|
|
||||||
savedSessionsCache.set(idHash, data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function GetOsColor(os: string) {
|
|
||||||
switch (os) {
|
|
||||||
case "Windows Mobile":
|
|
||||||
case "Windows":
|
|
||||||
return "#55a6ef"; // Light blue
|
|
||||||
case "Linux":
|
|
||||||
return "#cdcd31"; // Yellow
|
|
||||||
case "Android":
|
|
||||||
return "#7bc958"; // Green
|
|
||||||
case "Mac OS X":
|
|
||||||
case "iOS":
|
|
||||||
return ""; // Default to white/black (theme-dependent)
|
|
||||||
default:
|
|
||||||
return "#f3799a"; // Pink
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function GetPlatformIcon(platform: string) {
|
|
||||||
switch (platform) {
|
|
||||||
case "Discord Android":
|
|
||||||
case "Discord iOS":
|
|
||||||
case "Discord Client":
|
|
||||||
return DiscordIcon;
|
|
||||||
case "Android Chrome":
|
|
||||||
case "Chrome iOS":
|
|
||||||
case "Chrome":
|
|
||||||
return ChromeIcon;
|
|
||||||
case "Edge":
|
|
||||||
return EdgeIcon;
|
|
||||||
case "Firefox":
|
|
||||||
return FirefoxIcon;
|
|
||||||
case "Internet Explorer":
|
|
||||||
return IEIcon;
|
|
||||||
case "Opera Mini":
|
|
||||||
case "Opera":
|
|
||||||
return OperaIcon;
|
|
||||||
case "Mobile Safari":
|
|
||||||
case "Safari":
|
|
||||||
return SafariIcon;
|
|
||||||
case "BlackBerry":
|
|
||||||
case "Facebook Mobile":
|
|
||||||
case "Android Mobile":
|
|
||||||
return MobileIcon;
|
|
||||||
default:
|
|
||||||
return UnknownIcon;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
# BetterSettings
|
|
||||||
|
|
||||||
Improves Discord's Settings via multiple (toggleable) changes:
|
|
||||||
- makes opening settings much faster
|
|
||||||
- removes the scuffed transition animation
|
|
||||||
- organises the settings cog context menu into categories
|
|
||||||
|
|
||||||
![](https://github.com/Vendicated/Vencord/assets/45497981/e8d67a95-3909-4be5-8281-8cf9d2f1c30e)
|
|
||||||
|
|
|
@ -1,185 +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 { classNameFactory } from "@api/Styles";
|
|
||||||
import { Devs } from "@utils/constants";
|
|
||||||
import { Logger } from "@utils/Logger";
|
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
|
||||||
import { waitFor } from "@webpack";
|
|
||||||
import { ComponentDispatch, FocusLock, i18n, Menu, useEffect, useRef } from "@webpack/common";
|
|
||||||
import type { HTMLAttributes, ReactElement } from "react";
|
|
||||||
|
|
||||||
type SettingsEntry = { section: string, label: string; };
|
|
||||||
|
|
||||||
const cl = classNameFactory("");
|
|
||||||
let Classes: Record<string, string>;
|
|
||||||
waitFor(["animating", "baseLayer", "bg", "layer", "layers"], m => Classes = m);
|
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
|
||||||
disableFade: {
|
|
||||||
description: "Disable the crossfade animation",
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
default: true,
|
|
||||||
restartNeeded: true
|
|
||||||
},
|
|
||||||
organizeMenu: {
|
|
||||||
description: "Organizes the settings cog context menu into categories",
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
eagerLoad: {
|
|
||||||
description: "Removes the loading delay when opening the menu for the first time",
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
default: true,
|
|
||||||
restartNeeded: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
interface LayerProps extends HTMLAttributes<HTMLDivElement> {
|
|
||||||
mode: "SHOWN" | "HIDDEN";
|
|
||||||
baseLayer?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Layer({ mode, baseLayer = false, ...props }: LayerProps) {
|
|
||||||
const hidden = mode === "HIDDEN";
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => () => {
|
|
||||||
ComponentDispatch.dispatch("LAYER_POP_START");
|
|
||||||
ComponentDispatch.dispatch("LAYER_POP_COMPLETE");
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const node = (
|
|
||||||
<div
|
|
||||||
ref={containerRef}
|
|
||||||
aria-hidden={hidden}
|
|
||||||
className={cl({
|
|
||||||
[Classes.layer]: true,
|
|
||||||
[Classes.baseLayer]: baseLayer,
|
|
||||||
"stop-animations": hidden
|
|
||||||
})}
|
|
||||||
style={{ opacity: hidden ? 0 : undefined }}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return baseLayer
|
|
||||||
? node
|
|
||||||
: <FocusLock containerRef={containerRef}>{node}</FocusLock>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "BetterSettings",
|
|
||||||
description: "Enhances your settings-menu-opening experience",
|
|
||||||
authors: [Devs.Kyuuhachi],
|
|
||||||
settings,
|
|
||||||
|
|
||||||
patches: [
|
|
||||||
{
|
|
||||||
find: "this.renderArtisanalHack()",
|
|
||||||
replacement: [
|
|
||||||
{ // Fade in on layer
|
|
||||||
match: /(?<=\((\i),"contextType",\i\.AccessibilityPreferencesContext\);)/,
|
|
||||||
replace: "$1=$self.Layer;",
|
|
||||||
predicate: () => settings.store.disableFade
|
|
||||||
},
|
|
||||||
{ // Lazy-load contents
|
|
||||||
match: /createPromise:\(\)=>([^:}]*?),webpackId:"\d+",name:(?!="CollectiblesShop")"[^"]+"/g,
|
|
||||||
replace: "$&,_:$1",
|
|
||||||
predicate: () => settings.store.eagerLoad
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{ // For some reason standardSidebarView also has a small fade-in
|
|
||||||
find: "DefaultCustomContentScroller:function()",
|
|
||||||
replacement: [
|
|
||||||
{
|
|
||||||
match: /\(0,\i\.useTransition\)\((\i)/,
|
|
||||||
replace: "(_cb=>_cb(void 0,$1))||$&"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
match: /\i\.animated\.div/,
|
|
||||||
replace: '"div"'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
predicate: () => settings.store.disableFade
|
|
||||||
},
|
|
||||||
{ // Load menu TOC eagerly
|
|
||||||
find: "Messages.USER_SETTINGS_WITH_BUILD_OVERRIDE.format",
|
|
||||||
replacement: {
|
|
||||||
match: /(\i)\(this,"handleOpenSettingsContextMenu",.{0,100}?openContextMenuLazy.{0,100}?(await Promise\.all[^};]*?\)\)).*?,(?=\1\(this)/,
|
|
||||||
replace: "$&(async ()=>$2)(),"
|
|
||||||
},
|
|
||||||
predicate: () => settings.store.eagerLoad
|
|
||||||
},
|
|
||||||
{ // Settings cog context menu
|
|
||||||
find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",
|
|
||||||
replacement: {
|
|
||||||
match: /\(0,\i.useDefaultUserSettingsSections\)\(\)(?=\.filter\(\i=>\{let\{section:\i\}=)/,
|
|
||||||
replace: "$self.wrapMenu($&)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
//
|
|
||||||
// Thus, we sanity check webpack modules & do this really hacky try catch to hopefully prevent hard crashes if something goes wrong.
|
|
||||||
// try catch will only catch errors in the Layer function (hence why it's called as a plain function rather than a component), but
|
|
||||||
// not in children
|
|
||||||
Layer(props: LayerProps) {
|
|
||||||
if (!FocusLock || !ComponentDispatch || !Classes) {
|
|
||||||
new Logger("BetterSettings").error("Failed to find some components");
|
|
||||||
return props.children;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Layer {...props} />;
|
|
||||||
},
|
|
||||||
|
|
||||||
wrapMenu(list: SettingsEntry[]) {
|
|
||||||
if (!settings.store.organizeMenu) return list;
|
|
||||||
|
|
||||||
const items = [{ label: null as string | null, items: [] as SettingsEntry[] }];
|
|
||||||
|
|
||||||
for (const item of list) {
|
|
||||||
if (item.section === "HEADER") {
|
|
||||||
items.push({ label: item.label, items: [] });
|
|
||||||
} else if (item.section === "DIVIDER") {
|
|
||||||
items.push({ label: i18n.Messages.OTHER_OPTIONS, items: [] });
|
|
||||||
} else {
|
|
||||||
items.at(-1)!.items.push(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
filter(predicate: (item: SettingsEntry) => boolean) {
|
|
||||||
for (const category of items) {
|
|
||||||
category.items = category.items.filter(predicate);
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
map(render: (item: SettingsEntry) => ReactElement) {
|
|
||||||
return items
|
|
||||||
.filter(a => a.items.length > 0)
|
|
||||||
.map(({ label, items }) => {
|
|
||||||
const children = items.map(render);
|
|
||||||
if (label) {
|
|
||||||
return (
|
|
||||||
<Menu.MenuItem
|
|
||||||
id={label.replace(/\W/, "_")}
|
|
||||||
label={label}
|
|
||||||
children={children}
|
|
||||||
action={children[0].props.action}
|
|
||||||
/>);
|
|
||||||
} else {
|
|
||||||
return children;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -21,7 +21,7 @@ import definePlugin from "@utils/types";
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "BetterUploadButton",
|
name: "BetterUploadButton",
|
||||||
authors: [Devs.fawn, Devs.Ven],
|
authors: [Devs.obscurity, Devs.Ven],
|
||||||
description: "Upload with a single click, open menu with right click",
|
description: "Upload with a single click, open menu with right click",
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
|
|
|
@ -34,9 +34,9 @@ export default definePlugin({
|
||||||
{
|
{
|
||||||
find: ".AVATAR_STATUS_MOBILE_16;",
|
find: ".AVATAR_STATUS_MOBILE_16;",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(fromIsMobile:\i=!0,.+?)status:(\i)/,
|
match: /(?<=fromIsMobile:\i=!0,.+?)status:(\i)/,
|
||||||
// Rename field to force it to always use "online"
|
// Rename field to force it to always use "online"
|
||||||
replace: '$1status_$:$2="online"'
|
replace: 'status_$:$1="online"'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue