mirror of
https://github.com/Equicord/Equicord.git
synced 2025-06-27 07:24:55 -04:00
Merge branch 'dev' into bun
This commit is contained in:
commit
b415877b2f
73 changed files with 1365 additions and 642 deletions
|
@ -6,12 +6,12 @@
|
||||||
|
|
||||||
Equicord is a fork of [Vencord](https://github.com/Vendicated/Vencord), with over 300+ plugins.
|
Equicord is a fork of [Vencord](https://github.com/Vendicated/Vencord), with over 300+ plugins.
|
||||||
|
|
||||||
You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, changes, chat or even support.<br><br></br>
|
You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, changes, chat or even support.
|
||||||
|
|
||||||
### Extra included plugins
|
### Extra included plugins
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>183 additional plugins</summary>
|
<summary>186 additional plugins</summary>
|
||||||
|
|
||||||
### All Platforms
|
### All Platforms
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
|
||||||
- AlwaysExpandProfile by thororen
|
- AlwaysExpandProfile by thororen
|
||||||
- AmITyping by MrDiamond
|
- AmITyping by MrDiamond
|
||||||
- Anammox by Kyuuhachi
|
- Anammox by Kyuuhachi
|
||||||
|
- AudiobookShelfRPC by vMohammad
|
||||||
- AtSomeone by Joona
|
- AtSomeone by Joona
|
||||||
- BannersEverywhere by ImLvna & AutumnVN
|
- BannersEverywhere by ImLvna & AutumnVN
|
||||||
- BetterActivities by D3SOX, Arjix, AutumnVN
|
- BetterActivities by D3SOX, Arjix, AutumnVN
|
||||||
|
@ -95,6 +96,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
|
||||||
- InRole by nin0dev
|
- InRole by nin0dev
|
||||||
- InstantScreenshare by HAHALOSAH & thororen
|
- InstantScreenshare by HAHALOSAH & thororen
|
||||||
- IRememberYou by zoodogood
|
- IRememberYou by zoodogood
|
||||||
|
- JellyfinRichPresence by vMohammad
|
||||||
- Jumpscare by Surgedevs
|
- Jumpscare by Surgedevs
|
||||||
- JumpToStart by Samwich
|
- JumpToStart by Samwich
|
||||||
- KeyboardSounds by HypedDomi
|
- KeyboardSounds by HypedDomi
|
||||||
|
@ -160,6 +162,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
|
||||||
- StatusPresets by iamme
|
- StatusPresets by iamme
|
||||||
- SteamStatusSync by niko
|
- SteamStatusSync by niko
|
||||||
- StickerBlocker by Samwich
|
- StickerBlocker by Samwich
|
||||||
|
- StreamingCodecDisabler by davidkra230
|
||||||
- TalkInReverse by Tolgchu
|
- TalkInReverse by Tolgchu
|
||||||
- TeX by Kyuuhachi
|
- TeX by Kyuuhachi
|
||||||
- TextToSpeech by Samwich
|
- TextToSpeech by Samwich
|
||||||
|
|
|
@ -20,15 +20,12 @@
|
||||||
/// <reference path="../src/globals.d.ts" />
|
/// <reference path="../src/globals.d.ts" />
|
||||||
|
|
||||||
import monacoHtmlLocal from "file://monacoWin.html?minify";
|
import monacoHtmlLocal from "file://monacoWin.html?minify";
|
||||||
import monacoHtmlCdn from "file://../src/main/monacoWin.html?minify";
|
|
||||||
import * as DataStore from "../src/api/DataStore";
|
import * as DataStore from "../src/api/DataStore";
|
||||||
import { debounce } from "../src/utils";
|
import { debounce, localStorage } 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 { Settings } from "../src/Vencord";
|
import { Settings } from "../src/Vencord";
|
||||||
|
import { getStylusWebStoreUrl } from "@utils/web";
|
||||||
// Discord deletes this so need to store in variable
|
|
||||||
const { localStorage } = window;
|
|
||||||
|
|
||||||
// listeners for ipc.on
|
// listeners for ipc.on
|
||||||
const cssListeners = new Set<(css: string) => void>();
|
const cssListeners = new Set<(css: string) => void>();
|
||||||
|
@ -76,6 +73,14 @@ window.VencordNative = {
|
||||||
addThemeChangeListener: NOOP,
|
addThemeChangeListener: NOOP,
|
||||||
openFile: NOOP_ASYNC,
|
openFile: NOOP_ASYNC,
|
||||||
async openEditor() {
|
async openEditor() {
|
||||||
|
if (IS_USERSCRIPT) {
|
||||||
|
const shouldOpenWebStore = confirm("QuickCSS is not supported on the Userscript. You can instead use the Stylus extension.\n\nDo you want to open the Stylus web store page?");
|
||||||
|
if (shouldOpenWebStore) {
|
||||||
|
window.open(getStylusWebStoreUrl(), "_blank");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const features = `popup,width=${Math.min(window.innerWidth, 1000)},height=${Math.min(window.innerHeight, 1000)}`;
|
const features = `popup,width=${Math.min(window.innerWidth, 1000)},height=${Math.min(window.innerHeight, 1000)}`;
|
||||||
const win = open("about:blank", "VencordQuickCss", features);
|
const win = open("about:blank", "VencordQuickCss", features);
|
||||||
if (!win) {
|
if (!win) {
|
||||||
|
@ -91,7 +96,7 @@ window.VencordNative = {
|
||||||
? "vs-light"
|
? "vs-light"
|
||||||
: "vs-dark";
|
: "vs-dark";
|
||||||
|
|
||||||
win.document.write(IS_EXTENSION ? monacoHtmlLocal : monacoHtmlCdn);
|
win.document.write(monacoHtmlLocal);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
200
package.json
200
package.json
|
@ -1,100 +1,102 @@
|
||||||
{
|
{
|
||||||
"name": "equicord",
|
"name": "equicord",
|
||||||
"private": "true",
|
"private": "true",
|
||||||
"version": "1.12.2",
|
"version": "1.12.4",
|
||||||
"description": "The other cutest Discord client mod",
|
"description": "The other cutest Discord client mod",
|
||||||
"homepage": "https://github.com/Equicord/Equicord#readme",
|
"homepage": "https://github.com/Equicord/Equicord#readme",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/Equicord/Equicord/issues"
|
"url": "https://github.com/Equicord/Equicord/issues"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/Equicord/Equicord.git"
|
"url": "git+https://github.com/Equicord/Equicord.git"
|
||||||
},
|
},
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
"author": "Equicord",
|
"author": "Equicord",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "bun run scripts/build/build.mjs",
|
"build": "bun run scripts/build/build.mjs",
|
||||||
"buildStandalone": "bun run build --standalone",
|
"buildStandalone": "bun run build --standalone",
|
||||||
"buildWeb": "bun run scripts/build/buildWeb.mjs",
|
"buildWeb": "bun run scripts/build/buildWeb.mjs",
|
||||||
"buildWebStandalone": "bun run buildWeb --standalone",
|
"buildWebStandalone": "bun run buildWeb --standalone",
|
||||||
"buildReporter": "bun run buildWebStandalone --reporter --skip-extension",
|
"buildReporter": "bun run buildWebStandalone --reporter --skip-extension",
|
||||||
"buildReporterDesktop": "bun run build --reporter",
|
"buildReporterDesktop": "bun run build --reporter",
|
||||||
"watch": "bun run build --watch",
|
"watch": "bun run build --watch",
|
||||||
"dev": "bun run watch",
|
"dev": "bun run watch",
|
||||||
"watchWeb": "bun run buildWeb --watch",
|
"watchWeb": "bun run buildWeb --watch",
|
||||||
"generatePluginJson": "bun run scripts/generatePluginList.ts",
|
"generatePluginJson": "bun run scripts/generatePluginList.ts",
|
||||||
"generateEquicordPluginJson": "bun run scripts/generateEquicordPluginList.ts",
|
"generateEquicordPluginJson": "bun run scripts/generateEquicordPluginList.ts",
|
||||||
"generateTypes": "tspc --emitDeclarationOnly --declaration --outDir packages/vencord-types --allowJs false",
|
"generateTypes": "tspc --emitDeclarationOnly --declaration --outDir packages/vencord-types --allowJs false",
|
||||||
"inject": "bun run scripts/runInstaller.mjs --install",
|
"inject": "bun run scripts/runInstaller.mjs --install",
|
||||||
"uninject": "bun run scripts/runInstaller.mjs --uninstall",
|
"uninject": "bun run scripts/runInstaller.mjs --uninstall",
|
||||||
"lint": "eslint",
|
"lint": "eslint",
|
||||||
"lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins",
|
"lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins",
|
||||||
"lint:fix": "bun run lint --fix",
|
"lint:fix": "bun run lint --fix",
|
||||||
"test": "bun run buildStandalone && bun run testTsc && bun run lint:fix && bun run lint-styles && bun run generatePluginJson",
|
"test": "bun run buildStandalone && bun run testTsc && bun run lint:fix && bun run lint-styles && bun run generatePluginJson",
|
||||||
"testWeb": "bun run lint && bun run buildWeb && bun run testTsc",
|
"testWeb": "bun run lint && bun run buildWeb && bun run testTsc",
|
||||||
"testTsc": "tsc --noEmit"
|
"testTsc": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ffmpeg/ffmpeg": "^0.12.10",
|
"@ffmpeg/ffmpeg": "^0.12.10",
|
||||||
"@ffmpeg/util": "^0.12.1",
|
"@ffmpeg/util": "^0.12.1",
|
||||||
"@intrnl/xxhash64": "^0.1.2",
|
"@intrnl/xxhash64": "^0.1.2",
|
||||||
"@sapphi-red/web-noise-suppressor": "0.3.5",
|
"@sapphi-red/web-noise-suppressor": "0.3.5",
|
||||||
"@types/less": "^3.0.6",
|
"@types/less": "^3.0.6",
|
||||||
"@types/stylus": "^0.48.42",
|
"@types/stylus": "^0.48.42",
|
||||||
"@types/tinycolor2": "^1.4.6",
|
"@types/tinycolor2": "^1.4.6",
|
||||||
"@vap/core": "0.0.12",
|
"@vap/core": "0.0.12",
|
||||||
"@vap/shiki": "0.10.5",
|
"@vap/shiki": "0.10.5",
|
||||||
"fflate": "^0.8.2",
|
"fflate": "^0.8.2",
|
||||||
"gifenc": "github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3",
|
"gifenc": "github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3",
|
||||||
"jsqr": "1.4.0",
|
"jsqr": "1.4.0",
|
||||||
"idb": "8.0.0",
|
"idb": "8.0.0",
|
||||||
"monaco-editor": "^0.52.2",
|
"monaco-editor": "^0.52.2",
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"usercss-meta": "^0.12.0",
|
"usercss-meta": "^0.12.0",
|
||||||
"openai": "^4.30.0",
|
"openai": "^4.30.0",
|
||||||
"virtual-merge": "^1.0.1"
|
"virtual-merge": "^1.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@electron/asar": "^3.2.10",
|
"@electron/asar": "^3.2.10",
|
||||||
"@stylistic/eslint-plugin": "^4.2.0",
|
"@stylistic/eslint-plugin": "^4.2.0",
|
||||||
"@types/chrome": "^0.0.312",
|
"@types/chrome": "^0.0.312",
|
||||||
"@types/diff": "^7.0.2",
|
"@types/diff": "^7.0.2",
|
||||||
"@types/lodash": "^4.17.14",
|
"@types/lodash": "^4.17.14",
|
||||||
"@types/node": "^22.13.13",
|
"@types/node": "^22.13.13",
|
||||||
"@types/react": "^19.0.10",
|
"@types/react": "^19.0.10",
|
||||||
"@types/react-dom": "^19.0.4",
|
"@types/react-dom": "^19.0.4",
|
||||||
"@types/yazl": "^2.4.5",
|
"@types/yazl": "^2.4.5",
|
||||||
"diff": "^7.0.0",
|
"diff": "^7.0.0",
|
||||||
"discord-types": "^1.3.26",
|
"discord-types": "^1.3.26",
|
||||||
"esbuild": "^0.25.1",
|
"esbuild": "^0.25.1",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-import-resolver-alias": "^1.1.2",
|
"eslint-import-resolver-alias": "^1.1.2",
|
||||||
"eslint-plugin-react": "^7.37.3",
|
"eslint-plugin-react": "^7.37.3",
|
||||||
"eslint-plugin-simple-header": "^1.2.1",
|
"eslint-plugin-simple-header": "^1.2.1",
|
||||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
"eslint-plugin-unused-imports": "^4.1.4",
|
"eslint-plugin-unused-imports": "^4.1.4",
|
||||||
"highlight.js": "11.11.1",
|
"highlight.js": "11.11.1",
|
||||||
"html-minifier-terser": "^7.2.0",
|
"html-minifier-terser": "^7.2.0",
|
||||||
"moment": "^2.22.2",
|
"moment": "^2.22.2",
|
||||||
"puppeteer-core": "^24.4.0",
|
"puppeteer-core": "^24.4.0",
|
||||||
"standalone-electron-types": "^34.2.0",
|
"standalone-electron-types": "^34.2.0",
|
||||||
"stylelint": "^16.17.0",
|
"stylelint": "^16.17.0",
|
||||||
"stylelint-config-standard": "^37.0.0",
|
"stylelint-config-standard": "^37.0.0",
|
||||||
"ts-patch": "^3.3.0",
|
"ts-patch": "^3.3.0",
|
||||||
"ts-pattern": "^5.6.0",
|
"ts-pattern": "^5.6.0",
|
||||||
"type-fest": "^4.41.0",
|
"type-fest": "^4.41.0",
|
||||||
"typescript": "^5.8.2",
|
"typescript": "^5.8.2",
|
||||||
"typescript-eslint": "^8.28.0",
|
"typescript-eslint": "^8.28.0",
|
||||||
"typescript-transform-paths": "^3.5.5",
|
"typescript-transform-paths": "^3.5.5",
|
||||||
"zip-local": "^0.3.5",
|
"zip-local": "^0.3.5",
|
||||||
"zustand": "^3.7.2"
|
"zustand": "^3.7.2"
|
||||||
},
|
},
|
||||||
"packageManager": "bun@1.1.0",
|
"packageManager": "bun@1.1.0",
|
||||||
"trustedDependencies": ["esbuild"],
|
"trustedDependencies": [
|
||||||
"engines": {
|
"esbuild"
|
||||||
"node": ">=18",
|
],
|
||||||
"bun": ">=1.0.0"
|
"engines": {
|
||||||
}
|
"node": ">=18",
|
||||||
}
|
"bun": ">=1.0.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -34,6 +34,7 @@ const defines = stringifyValues({
|
||||||
IS_UPDATER_DISABLED,
|
IS_UPDATER_DISABLED,
|
||||||
IS_WEB: false,
|
IS_WEB: false,
|
||||||
IS_EXTENSION: false,
|
IS_EXTENSION: false,
|
||||||
|
IS_USERSCRIPT: false,
|
||||||
VERSION,
|
VERSION,
|
||||||
BUILD_TIMESTAMP
|
BUILD_TIMESTAMP
|
||||||
});
|
});
|
||||||
|
|
|
@ -43,6 +43,7 @@ const commonOptions = {
|
||||||
define: stringifyValues({
|
define: stringifyValues({
|
||||||
IS_WEB: true,
|
IS_WEB: true,
|
||||||
IS_EXTENSION: false,
|
IS_EXTENSION: false,
|
||||||
|
IS_USERSCRIPT: false,
|
||||||
IS_STANDALONE: true,
|
IS_STANDALONE: true,
|
||||||
IS_DEV,
|
IS_DEV,
|
||||||
IS_REPORTER,
|
IS_REPORTER,
|
||||||
|
@ -108,6 +109,7 @@ const buildConfigs = [
|
||||||
inject: ["browser/GMPolyfill.js", ...(commonOptions?.inject || [])],
|
inject: ["browser/GMPolyfill.js", ...(commonOptions?.inject || [])],
|
||||||
define: {
|
define: {
|
||||||
...commonOptions.define,
|
...commonOptions.define,
|
||||||
|
IS_USERSCRIPT: "true",
|
||||||
window: "unsafeWindow",
|
window: "unsafeWindow",
|
||||||
},
|
},
|
||||||
outfile: "dist/Vencord.user.js",
|
outfile: "dist/Vencord.user.js",
|
||||||
|
|
|
@ -146,7 +146,7 @@ export const globPlugins = kind => ({
|
||||||
});
|
});
|
||||||
|
|
||||||
build.onLoad({ filter, namespace: "import-plugins" }, async () => {
|
build.onLoad({ filter, namespace: "import-plugins" }, async () => {
|
||||||
const pluginDirs = ["plugins/_api", "plugins/_core", "plugins", "userplugins", "equicordplugins", "equicordplugins/_core"];
|
const pluginDirs = ["plugins/_api", "plugins/_core", "plugins", "userplugins", "equicordplugins"];
|
||||||
let code = "";
|
let code = "";
|
||||||
let pluginsCode = "\n";
|
let pluginsCode = "\n";
|
||||||
let metaCode = "\n";
|
let metaCode = "\n";
|
||||||
|
|
|
@ -264,7 +264,7 @@ function isPluginFile({ name }: { name: string; }) {
|
||||||
|
|
||||||
const plugins = [] as PluginData[];
|
const plugins = [] as PluginData[];
|
||||||
|
|
||||||
await Promise.all(["src/plugins", "src/plugins/_core", "src/equicordplugins", "src/equicordplugins/_core"].flatMap(dir =>
|
await Promise.all(["src/plugins", "src/plugins/_core", "src/equicordplugins"].flatMap(dir =>
|
||||||
readdirSync(dir, { withFileTypes: true })
|
readdirSync(dir, { withFileTypes: true })
|
||||||
.filter(isPluginFile)
|
.filter(isPluginFile)
|
||||||
.map(async dirent => {
|
.map(async dirent => {
|
||||||
|
|
|
@ -153,7 +153,11 @@ async function init() {
|
||||||
|
|
||||||
if (!IS_WEB && !IS_UPDATER_DISABLED) {
|
if (!IS_WEB && !IS_UPDATER_DISABLED) {
|
||||||
runUpdateCheck();
|
runUpdateCheck();
|
||||||
setInterval(runUpdateCheck, 1000 * 60 * 30); // 30 minutes
|
|
||||||
|
// this tends to get really annoying, so only do this if the user has auto-update without notification enabled
|
||||||
|
if (Settings.autoUpdate && !Settings.autoUpdateNotification) {
|
||||||
|
setInterval(runUpdateCheck, 1000 * 60 * 30); // 30 minutes
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IS_DEV) {
|
if (IS_DEV) {
|
||||||
|
|
|
@ -48,7 +48,7 @@ export function _modifyAccessories(
|
||||||
) {
|
) {
|
||||||
for (const [key, accessory] of accessories.entries()) {
|
for (const [key, accessory] of accessories.entries()) {
|
||||||
const res = (
|
const res = (
|
||||||
<ErrorBoundary message={`Failed to render ${key} Message Accessory`} key={key}>
|
<ErrorBoundary noop message={`Failed to render ${key} Message Accessory`} key={key}>
|
||||||
<accessory.render {...props} />
|
<accessory.render {...props} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
|
|
|
@ -11,6 +11,10 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.visual-refresh .vc-notification-root {
|
||||||
|
background-color: var(--background-base-low);
|
||||||
|
}
|
||||||
|
|
||||||
.vc-notification-root:not(.vc-notification-log-wrapper > .vc-notification-root) {
|
.vc-notification-root:not(.vc-notification-log-wrapper > .vc-notification-root) {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 2147483647;
|
z-index: 2147483647;
|
||||||
|
|
|
@ -75,10 +75,15 @@ const ErrorBoundary = LazyComponent(() => {
|
||||||
logger.error(`${this.props.message || "A component threw an Error"}\n`, error, errorInfo.componentStack);
|
logger.error(`${this.props.message || "A component threw an Error"}\n`, error, errorInfo.componentStack);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isNoop() {
|
||||||
|
if (IS_DEV) return false;
|
||||||
|
return this.props.noop;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this.state.error === NO_ERROR) return this.props.children;
|
if (this.state.error === NO_ERROR) return this.props.children;
|
||||||
|
|
||||||
if (this.props.noop) return null;
|
if (this.isNoop) return null;
|
||||||
|
|
||||||
if (this.props.fallback)
|
if (this.props.fallback)
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -4,4 +4,8 @@
|
||||||
border: 1px solid #e78284;
|
border: 1px solid #e78284;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
color: var(--text-normal, white);
|
color: var(--text-normal, white);
|
||||||
|
|
||||||
|
& a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,9 @@ export function Link(props: React.PropsWithChildren<Props>) {
|
||||||
props.style.pointerEvents = "none";
|
props.style.pointerEvents = "none";
|
||||||
props["aria-disabled"] = true;
|
props["aria-disabled"] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
props.rel ??= "noreferrer";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a role="link" target="_blank" {...props}>
|
<a role="link" target="_blank" {...props}>
|
||||||
{props.children}
|
{props.children}
|
||||||
|
|
|
@ -270,7 +270,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
||||||
{!!plugin.settingsAboutComponent && (
|
{!!plugin.settingsAboutComponent && (
|
||||||
<div className={classes(Margins.bottom8, "vc-text-selectable")}>
|
<div className={classes(Margins.bottom8, "vc-text-selectable")}>
|
||||||
<Forms.FormSection>
|
<Forms.FormSection>
|
||||||
<ErrorBoundary message="An error occurred while rendering this plugin's custom InfoComponent">
|
<ErrorBoundary message="An error occurred while rendering this plugin's custom Info Component">
|
||||||
<plugin.settingsAboutComponent tempSettings={tempSettings} />
|
<plugin.settingsAboutComponent tempSettings={tempSettings} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</Forms.FormSection>
|
</Forms.FormSection>
|
||||||
|
|
|
@ -244,7 +244,7 @@ export default function PluginSettings() {
|
||||||
}));
|
}));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const depMap = React.useMemo(() => {
|
const depMap = useMemo(() => {
|
||||||
const o = {} as Record<string, string[]>;
|
const o = {} as Record<string, string[]>;
|
||||||
for (const plugin in Plugins) {
|
for (const plugin in Plugins) {
|
||||||
const deps = Plugins[plugin].dependencies;
|
const deps = Plugins[plugin].dependencies;
|
||||||
|
|
|
@ -34,6 +34,7 @@ import { useAwaiter } from "@utils/react";
|
||||||
import type { ThemeHeader } from "@utils/themes";
|
import type { ThemeHeader } from "@utils/themes";
|
||||||
import { getThemeInfo, stripBOM, type UserThemeHeader } from "@utils/themes/bd";
|
import { getThemeInfo, stripBOM, type UserThemeHeader } from "@utils/themes/bd";
|
||||||
import { usercssParse } from "@utils/themes/usercss";
|
import { usercssParse } from "@utils/themes/usercss";
|
||||||
|
import { getStylusWebStoreUrl } from "@utils/web";
|
||||||
import { findLazy } from "@webpack";
|
import { findLazy } from "@webpack";
|
||||||
import { Button, Card, Forms, React, showToast, TabBar, TextInput, Tooltip, useEffect, useMemo, useRef, useState } from "@webpack/common";
|
import { Button, Card, Forms, React, showToast, TabBar, TextInput, Tooltip, useEffect, useMemo, useRef, useState } from "@webpack/common";
|
||||||
import type { ComponentType, Ref, SyntheticEvent } from "react";
|
import type { ComponentType, Ref, SyntheticEvent } from "react";
|
||||||
|
@ -502,4 +503,20 @@ function ThemesTab() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default wrapTab(ThemesTab, "Themes");
|
function UserscriptThemesTab() {
|
||||||
|
return (
|
||||||
|
<SettingsTab title="Themes">
|
||||||
|
<Card className="vc-settings-card">
|
||||||
|
<Forms.FormTitle tag="h5">Themes are not supported on the Userscript!</Forms.FormTitle>
|
||||||
|
|
||||||
|
<Forms.FormText>
|
||||||
|
You can instead install themes with the <Link href={getStylusWebStoreUrl()}>Stylus extension</Link>!
|
||||||
|
</Forms.FormText>
|
||||||
|
</Card>
|
||||||
|
</SettingsTab>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IS_USERSCRIPT
|
||||||
|
? wrapTab(UserscriptThemesTab, "Themes")
|
||||||
|
: wrapTab(ThemesTab, "Themes");
|
||||||
|
|
250
src/equicordplugins/ABSRPC/index.tsx
Normal file
250
src/equicordplugins/ABSRPC/index.tsx
Normal file
|
@ -0,0 +1,250 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2025 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
// alot of the code is from JellyfinRPC
|
||||||
|
import { definePluginSettings } from "@api/Settings";
|
||||||
|
import { EquicordDevs } from "@utils/constants";
|
||||||
|
import { Logger } from "@utils/Logger";
|
||||||
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
|
import { ApplicationAssetUtils, FluxDispatcher, Forms, showToast } from "@webpack/common";
|
||||||
|
|
||||||
|
|
||||||
|
interface ActivityAssets {
|
||||||
|
large_image?: string;
|
||||||
|
large_text?: string;
|
||||||
|
small_image?: string;
|
||||||
|
small_text?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Activity {
|
||||||
|
state: string;
|
||||||
|
details?: string;
|
||||||
|
timestamps?: {
|
||||||
|
start?: number;
|
||||||
|
};
|
||||||
|
assets?: ActivityAssets;
|
||||||
|
name: string;
|
||||||
|
application_id: string;
|
||||||
|
metadata?: {
|
||||||
|
button_urls?: Array<string>;
|
||||||
|
};
|
||||||
|
type: number;
|
||||||
|
flags: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MediaData {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
author?: string;
|
||||||
|
series?: string;
|
||||||
|
duration?: number;
|
||||||
|
currentTime?: number;
|
||||||
|
progress?: number;
|
||||||
|
url?: string;
|
||||||
|
imageUrl?: string;
|
||||||
|
isFinished?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const settings = definePluginSettings({
|
||||||
|
serverUrl: {
|
||||||
|
description: "AudioBookShelf server URL (e.g., https://abs.example.com)",
|
||||||
|
type: OptionType.STRING,
|
||||||
|
},
|
||||||
|
username: {
|
||||||
|
description: "AudioBookShelf username",
|
||||||
|
type: OptionType.STRING,
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
description: "AudioBookShelf password",
|
||||||
|
type: OptionType.STRING,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const applicationId = "1381423044907503636";
|
||||||
|
|
||||||
|
const logger = new Logger("AudioBookShelfRichPresence");
|
||||||
|
|
||||||
|
let authToken: string | null = null;
|
||||||
|
|
||||||
|
async function getApplicationAsset(key: string): Promise<string> {
|
||||||
|
return (await ApplicationAssetUtils.fetchAssetIds(applicationId, [key]))[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function setActivity(activity: Activity | null) {
|
||||||
|
FluxDispatcher.dispatch({
|
||||||
|
type: "LOCAL_ACTIVITY_UPDATE",
|
||||||
|
activity,
|
||||||
|
socketId: "ABSRPC",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "AudioBookShelfRichPresence",
|
||||||
|
description: "Rich presence for AudioBookShelf media server",
|
||||||
|
authors: [EquicordDevs.vmohammad],
|
||||||
|
|
||||||
|
settingsAboutComponent: () => (
|
||||||
|
<>
|
||||||
|
<Forms.FormTitle tag="h3">How to connect to AudioBookShelf</Forms.FormTitle>
|
||||||
|
<Forms.FormText>
|
||||||
|
Enter your AudioBookShelf server URL, username, and password to display your currently playing audiobooks as Discord Rich Presence.
|
||||||
|
<br /><br />
|
||||||
|
The plugin will automatically authenticate and fetch your listening progress.
|
||||||
|
</Forms.FormText>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
|
||||||
|
settings,
|
||||||
|
|
||||||
|
start() {
|
||||||
|
this.updatePresence();
|
||||||
|
this.updateInterval = setInterval(() => { this.updatePresence(); }, 10000);
|
||||||
|
},
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
clearInterval(this.updateInterval);
|
||||||
|
},
|
||||||
|
|
||||||
|
async authenticate(): Promise<boolean> {
|
||||||
|
if (!settings.store.serverUrl || !settings.store.username || !settings.store.password) {
|
||||||
|
logger.warn("AudioBookShelf server URL, username, or password is not set in settings.");
|
||||||
|
showToast("AudioBookShelf RPC is not configured.", "failure", {
|
||||||
|
duration: 15000,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const baseUrl = settings.store.serverUrl.replace(/\/$/, "");
|
||||||
|
const url = `${baseUrl}/login`;
|
||||||
|
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
username: settings.store.username,
|
||||||
|
password: settings.store.password,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) throw `${res.status} ${res.statusText}`;
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
authToken = data.user?.token;
|
||||||
|
return !!authToken;
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("Failed to authenticate with AudioBookShelf", e);
|
||||||
|
authToken = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchMediaData(): Promise<MediaData | null> {
|
||||||
|
if (!authToken && !(await this.authenticate())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPlayingNow = session => {
|
||||||
|
const now = Date.now();
|
||||||
|
const lastUpdate = session.updatedAt;
|
||||||
|
const diffSeconds = (now - lastUpdate) / 1000;
|
||||||
|
return diffSeconds <= 30;
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const baseUrl = settings.store.serverUrl!.replace(/\/$/, "");
|
||||||
|
const url = `${baseUrl}/api/me/listening-sessions`;
|
||||||
|
|
||||||
|
const res = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Bearer ${authToken}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
if (res.status === 401) {
|
||||||
|
authToken = null;
|
||||||
|
if (await this.authenticate()) {
|
||||||
|
return this.fetchMediaData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw `${res.status} ${res.statusText}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { sessions } = await res.json();
|
||||||
|
const activeSession = sessions.find((session: any) =>
|
||||||
|
session.updatedAt && !session.isFinished
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!activeSession || !isPlayingNow(activeSession)) return null;
|
||||||
|
|
||||||
|
const { mediaMetadata: media, mediaType, duration, currentTime, libraryItemId } = activeSession;
|
||||||
|
if (!media) return null;
|
||||||
|
console.log(media);
|
||||||
|
return {
|
||||||
|
name: media.title || "Unknown",
|
||||||
|
type: mediaType || "book",
|
||||||
|
author: media.author || media.publisher,
|
||||||
|
series: media.series[0]?.name,
|
||||||
|
duration,
|
||||||
|
currentTime,
|
||||||
|
imageUrl: libraryItemId ? `${baseUrl}/api/items/${libraryItemId}/cover` : undefined,
|
||||||
|
isFinished: activeSession.isFinished || false,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("Failed to query AudioBookShelf API", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async updatePresence() {
|
||||||
|
setActivity(await this.getActivity());
|
||||||
|
},
|
||||||
|
|
||||||
|
async getActivity(): Promise<Activity | null> {
|
||||||
|
const mediaData = await this.fetchMediaData();
|
||||||
|
if (!mediaData || mediaData.isFinished) return null;
|
||||||
|
|
||||||
|
const largeImage = mediaData.imageUrl;
|
||||||
|
console.log("Large Image URL:", largeImage);
|
||||||
|
const assets: ActivityAssets = {
|
||||||
|
large_image: largeImage ? await getApplicationAsset(largeImage) : await getApplicationAsset("audiobookshelf"),
|
||||||
|
large_text: mediaData.series || mediaData.author || undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDetails = () => {
|
||||||
|
return mediaData.name;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getState = () => {
|
||||||
|
if (mediaData.series && mediaData.author) {
|
||||||
|
return `${mediaData.series} • ${mediaData.author}`;
|
||||||
|
}
|
||||||
|
return mediaData.author || "AudioBook";
|
||||||
|
};
|
||||||
|
|
||||||
|
const timestamps = mediaData.currentTime && mediaData.duration ? {
|
||||||
|
start: Date.now() - (mediaData.currentTime * 1000),
|
||||||
|
end: Date.now() + ((mediaData.duration - mediaData.currentTime) * 1000)
|
||||||
|
} : undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
application_id: applicationId,
|
||||||
|
name: "AudioBookShelf",
|
||||||
|
|
||||||
|
details: getDetails(),
|
||||||
|
state: getState(),
|
||||||
|
assets,
|
||||||
|
timestamps,
|
||||||
|
|
||||||
|
type: 2,
|
||||||
|
flags: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,11 +0,0 @@
|
||||||
/*
|
|
||||||
* Vencord, a Discord client mod
|
|
||||||
* Copyright (c) 2024 Vendicated and contributors
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { SVGProps } from "react";
|
|
||||||
|
|
||||||
export function SpotifyIcon(props: SVGProps<SVGSVGElement>) {
|
|
||||||
return (<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" {...props}><path fill="#1ed760" d="M128 0C57.308 0 0 57.309 0 128c0 70.696 57.309 128 128 128c70.697 0 128-57.304 128-128C256 57.314 198.697.007 127.998.007zm58.699 184.614c-2.293 3.76-7.215 4.952-10.975 2.644c-30.053-18.357-67.885-22.515-112.44-12.335a7.981 7.981 0 0 1-9.552-6.007a7.968 7.968 0 0 1 6-9.553c48.76-11.14 90.583-6.344 124.323 14.276c3.76 2.308 4.952 7.215 2.644 10.975m15.667-34.853c-2.89 4.695-9.034 6.178-13.726 3.289c-34.406-21.148-86.853-27.273-127.548-14.92c-5.278 1.594-10.852-1.38-12.454-6.649c-1.59-5.278 1.386-10.842 6.655-12.446c46.485-14.106 104.275-7.273 143.787 17.007c4.692 2.89 6.175 9.034 3.286 13.72zm1.345-36.293C162.457 88.964 94.394 86.71 55.007 98.666c-6.325 1.918-13.014-1.653-14.93-7.978c-1.917-6.328 1.65-13.012 7.98-14.935C93.27 62.027 168.434 64.68 215.929 92.876c5.702 3.376 7.566 10.724 4.188 16.405c-3.362 5.69-10.73 7.565-16.4 4.187z"></path></svg>);
|
|
||||||
}
|
|
|
@ -35,16 +35,17 @@ export default definePlugin({
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
// Patch activity icons
|
// Patch activity icons
|
||||||
find: "isBlockedOrIgnored(null",
|
find: '"ActivityStatus"',
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<=hideTooltip:.{0,4}}=(\i).*?{}\))\]/,
|
match: /(?<=hideTooltip:.{0,4}}=(\i).*?{}\))\]/,
|
||||||
replace: ",$self.patchActivityList($1)]"
|
replace: ",$self.patchActivityList($1)]"
|
||||||
},
|
},
|
||||||
predicate: () => settings.store.memberList,
|
predicate: () => settings.store.memberList,
|
||||||
|
all: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Show all activities in the user popout/sidebar
|
// Show all activities in the user popout/sidebar
|
||||||
find: "hasAvatarForGuild(null",
|
find: '"UserProfilePopoutBody"',
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<=(\i)\.id\)\}\)\),(\i).*?)\(0,.{0,100}\i\.id,onClose:\i\}\)/,
|
match: /(?<=(\i)\.id\)\}\)\),(\i).*?)\(0,.{0,100}\i\.id,onClose:\i\}\)/,
|
||||||
replace: "$self.showAllActivitiesComponent({ activity: $2, user: $1 })"
|
replace: "$self.showAllActivitiesComponent({ activity: $2, user: $1 })"
|
||||||
|
|
|
@ -10,14 +10,13 @@ import { React, Tooltip } from "@webpack/common";
|
||||||
import { JSX } from "react";
|
import { JSX } from "react";
|
||||||
|
|
||||||
import { ActivityTooltip } from "../components/ActivityTooltip";
|
import { ActivityTooltip } from "../components/ActivityTooltip";
|
||||||
import { SpotifyIcon } from "../components/SpotifyIcon";
|
|
||||||
import { TwitchIcon } from "../components/TwitchIcon";
|
import { TwitchIcon } from "../components/TwitchIcon";
|
||||||
import { settings } from "../settings";
|
import { settings } from "../settings";
|
||||||
import { ActivityListIcon, ActivityListProps, ApplicationIcon, IconCSSProperties } from "../types";
|
import { ActivityListIcon, ActivityListProps, ApplicationIcon, IconCSSProperties } from "../types";
|
||||||
import { cl, getApplicationIcons } from "../utils";
|
import { cl, getApplicationIcons } from "../utils";
|
||||||
|
|
||||||
// if discord one day decides to change their icon this needs to be updated
|
// Discord no longer shows an icon here by default but we use the one from the popout now here
|
||||||
const DefaultActivityIcon = findComponentByCodeLazy("M6,7 L2,7 L2,6 L6,6 L6,7 Z M8,5 L2,5 L2,4 L8,4 L8,5 Z M8,3 L2,3 L2,2 L8,2 L8,3 Z M8.88888889,0 L1.11111111,0 C0.494444444,0 0,0.494444444 0,1.11111111 L0,8.88888889 C0,9.50253861 0.497461389,10 1.11111111,10 L8.88888889,10 C9.50253861,10 10,9.50253861 10,8.88888889 L10,1.11111111 C10,0.494444444 9.5,0 8.88888889,0 Z");
|
const DefaultActivityIcon = findComponentByCodeLazy("M5 2a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3V5a3 3 0 0 0-3-3H5Zm6.81 7c-.54 0-1 .26-1.23.61A1 1 0 0 1 8.92 8.5 3.49 3.49 0 0 1 11.82 7c1.81 0 3.43 1.38 3.43 3.25 0 1.45-.98 2.61-2.27 3.06a1 1 0 0 1-1.96.37l-.19-1a1 1 0 0 1 .98-1.18c.87 0 1.44-.63 1.44-1.25S12.68 9 11.81 9ZM13 16a1 1 0 1 1-2 0 1 1 0 0 1 2 0Zm7-10.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0ZM18.5 20a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM7 18.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0ZM5.5 7a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z");
|
||||||
|
|
||||||
export function patchActivityList({ activities, user, hideTooltip }: ActivityListProps): JSX.Element | null {
|
export function patchActivityList({ activities, user, hideTooltip }: ActivityListProps): JSX.Element | null {
|
||||||
const icons: ActivityListIcon[] = [];
|
const icons: ActivityListIcon[] = [];
|
||||||
|
@ -57,7 +56,6 @@ export function patchActivityList({ activities, user, hideTooltip }: ActivityLis
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
addActivityIcon("Twitch", TwitchIcon);
|
addActivityIcon("Twitch", TwitchIcon);
|
||||||
addActivityIcon("Spotify", SpotifyIcon);
|
|
||||||
|
|
||||||
if (icons.length) {
|
if (icons.length) {
|
||||||
const iconStyle: IconCSSProperties = {
|
const iconStyle: IconCSSProperties = {
|
||||||
|
@ -86,7 +84,7 @@ export function patchActivityList({ activities, user, hideTooltip }: ActivityLis
|
||||||
// We need to filter out custom statuses
|
// We need to filter out custom statuses
|
||||||
const shouldShow = activities.filter(a => a.type !== 4).length !== icons.length;
|
const shouldShow = activities.filter(a => a.type !== 4).length !== icons.length;
|
||||||
if (shouldShow) {
|
if (shouldShow) {
|
||||||
return <DefaultActivityIcon />;
|
return <DefaultActivityIcon size="xs" />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -39,11 +39,11 @@ export function getActivityApplication(activity: Activity | null) {
|
||||||
|
|
||||||
export function getApplicationIcons(activities: Activity[], preferSmall = false): ApplicationIcon[] {
|
export function getApplicationIcons(activities: Activity[], preferSmall = false): ApplicationIcon[] {
|
||||||
const applicationIcons: ApplicationIcon[] = [];
|
const applicationIcons: ApplicationIcon[] = [];
|
||||||
const applications = activities.filter(activity => activity.application_id || activity.platform);
|
const applications = activities.filter(activity => activity.application_id || activity.platform || activity?.id?.startsWith("spotify:"));
|
||||||
|
|
||||||
for (const activity of applications) {
|
for (const activity of applications) {
|
||||||
const { assets, application_id, platform } = activity;
|
const { assets, application_id, platform, id } = activity;
|
||||||
if (!application_id && !platform) continue;
|
if (!application_id && !platform && !id.startsWith("spotify:")) continue;
|
||||||
|
|
||||||
if (assets) {
|
if (assets) {
|
||||||
const { small_image, small_text, large_image, large_text } = assets;
|
const { small_image, small_text, large_image, large_text } = assets;
|
||||||
|
@ -59,6 +59,12 @@ export function getApplicationIcons(activities: Activity[], preferSmall = false)
|
||||||
activity
|
activity
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else if (image.startsWith("spotify:")) {
|
||||||
|
const url = `https://i.scdn.co/image/${image.split(":")[1]}`;
|
||||||
|
applicationIcons.push({
|
||||||
|
image: { src: url, alt },
|
||||||
|
activity
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
const src = `https://cdn.discordapp.com/app-assets/${application_id}/${image}.png`;
|
const src = `https://cdn.discordapp.com/app-assets/${application_id}/${image}.png`;
|
||||||
applicationIcons.push({
|
applicationIcons.push({
|
||||||
|
|
|
@ -7,4 +7,4 @@
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import definePlugin from "@utils/types";
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "BypassPinPrompt",
|
name: "BypassPinPrompt",
|
||||||
description: "Bypass the pin prompt when pinning messages",
|
description: "Bypass the pin prompt when using the pin functions",
|
||||||
authors: [EquicordDevs.thororen],
|
authors: [EquicordDevs.thororen],
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
|
|
9
src/equicordplugins/equicordHelper/native.ts
Normal file
9
src/equicordplugins/equicordHelper/native.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2025 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { CspPolicies, MediaScriptsAndCssSrc } from "@main/csp";
|
||||||
|
|
||||||
|
CspPolicies["*"] = MediaScriptsAndCssSrc;
|
|
@ -147,7 +147,7 @@ function VencordPopoutButton() {
|
||||||
function ToolboxFragmentWrapper({ children }: { children: ReactNode[]; }) {
|
function ToolboxFragmentWrapper({ children }: { children: ReactNode[]; }) {
|
||||||
children.splice(
|
children.splice(
|
||||||
children.length - 1, 0,
|
children.length - 1, 0,
|
||||||
<ErrorBoundary noop={true}>
|
<ErrorBoundary noop>
|
||||||
<VencordPopoutButton />
|
<VencordPopoutButton />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
|
|
|
@ -24,8 +24,6 @@ export const reverseExtensionMap = Object.entries(extensionMap).reduce((acc, [ta
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as Record<string, string>);
|
}, {} as Record<string, string>);
|
||||||
|
|
||||||
type ExtUpload = Upload & { fixExtension?: boolean; };
|
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "FixFileExtensions",
|
name: "FixFileExtensions",
|
||||||
authors: [EquicordDevs.thororen],
|
authors: [EquicordDevs.thororen],
|
||||||
|
@ -34,15 +32,21 @@ export default definePlugin({
|
||||||
patches: [
|
patches: [
|
||||||
// Taken from AnonymiseFileNames
|
// Taken from AnonymiseFileNames
|
||||||
{
|
{
|
||||||
find: 'type:"UPLOAD_START"',
|
find: "async uploadFiles(",
|
||||||
replacement: {
|
replacement: [
|
||||||
match: /await \i\.uploadFiles\((\i),/,
|
{
|
||||||
replace: "$1.forEach($self.fixExt),$&"
|
match: /async uploadFiles\((\i),\i\){/,
|
||||||
},
|
replace: "$&$1.forEach($self.fixExt);"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: /async uploadFilesSimple\((\i)\){/,
|
||||||
|
replace: "$&$1.forEach($self.fixExt);"
|
||||||
|
}
|
||||||
|
],
|
||||||
predicate: () => !Settings.plugins.AnonymiseFileNames.enabled,
|
predicate: () => !Settings.plugins.AnonymiseFileNames.enabled,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
fixExt(upload: ExtUpload) {
|
fixExt(upload: Upload) {
|
||||||
const file = upload.filename;
|
const file = upload.filename;
|
||||||
const tarMatch = tarExtMatcher.exec(file);
|
const tarMatch = tarExtMatcher.exec(file);
|
||||||
const extIdx = tarMatch?.index ?? file.lastIndexOf(".");
|
const extIdx = tarMatch?.index ?? file.lastIndexOf(".");
|
||||||
|
|
|
@ -24,7 +24,7 @@ export default async (
|
||||||
}: FurudoSettings,
|
}: FurudoSettings,
|
||||||
repliedMessage?: Message
|
repliedMessage?: Message
|
||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
const completion = await fetch("http://localhost:11434/api/chat", {
|
const completion = await fetch("http://127.0.0.1:11434/api/chat", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|
215
src/equicordplugins/jellyfinRichPresence/index.tsx
Normal file
215
src/equicordplugins/jellyfinRichPresence/index.tsx
Normal file
|
@ -0,0 +1,215 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2025 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
// alot of the code is from LastFMRichPresence
|
||||||
|
import { definePluginSettings } from "@api/Settings";
|
||||||
|
import { EquicordDevs } from "@utils/constants";
|
||||||
|
import { Logger } from "@utils/Logger";
|
||||||
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
|
import { ApplicationAssetUtils, FluxDispatcher, Forms, showToast } from "@webpack/common";
|
||||||
|
|
||||||
|
|
||||||
|
interface ActivityAssets {
|
||||||
|
large_image?: string;
|
||||||
|
large_text?: string;
|
||||||
|
small_image?: string;
|
||||||
|
small_text?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Activity {
|
||||||
|
state: string;
|
||||||
|
details?: string;
|
||||||
|
timestamps?: {
|
||||||
|
start?: number;
|
||||||
|
};
|
||||||
|
assets?: ActivityAssets;
|
||||||
|
name: string;
|
||||||
|
application_id: string;
|
||||||
|
metadata?: {
|
||||||
|
button_urls?: Array<string>;
|
||||||
|
};
|
||||||
|
type: number;
|
||||||
|
flags: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MediaData {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
artist?: string;
|
||||||
|
album?: string;
|
||||||
|
seriesName?: string;
|
||||||
|
seasonNumber?: number;
|
||||||
|
episodeNumber?: number;
|
||||||
|
year?: number;
|
||||||
|
url?: string;
|
||||||
|
imageUrl?: string;
|
||||||
|
duration?: number;
|
||||||
|
position?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const settings = definePluginSettings({
|
||||||
|
serverUrl: {
|
||||||
|
description: "Jellyfin server URL (e.g., https://jellyfin.example.com)",
|
||||||
|
type: OptionType.STRING,
|
||||||
|
},
|
||||||
|
apiKey: {
|
||||||
|
description: "Jellyfin API key obtained from your Jellyfin administration dashboard",
|
||||||
|
type: OptionType.STRING,
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
description: "Jellyfin user ID obtained from your user profile URL",
|
||||||
|
type: OptionType.STRING,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const applicationId = "1381368130164625469";
|
||||||
|
|
||||||
|
const logger = new Logger("JellyfinRichPresence");
|
||||||
|
|
||||||
|
async function getApplicationAsset(key: string): Promise<string> {
|
||||||
|
return (await ApplicationAssetUtils.fetchAssetIds(applicationId, [key]))[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function setActivity(activity: Activity | null) {
|
||||||
|
FluxDispatcher.dispatch({
|
||||||
|
type: "LOCAL_ACTIVITY_UPDATE",
|
||||||
|
activity,
|
||||||
|
socketId: "Jellyfin",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "JellyfinRichPresence",
|
||||||
|
description: "Rich presence for Jellyfin media server",
|
||||||
|
authors: [EquicordDevs.vmohammad],
|
||||||
|
|
||||||
|
settingsAboutComponent: () => (
|
||||||
|
<>
|
||||||
|
<Forms.FormTitle tag="h3">How to get an API key</Forms.FormTitle>
|
||||||
|
<Forms.FormText>
|
||||||
|
An API key is required to fetch your current media. To get one, go to your
|
||||||
|
Jellyfin dashboard, navigate to Administration {">"} API Keys and
|
||||||
|
create a new API key. <br /> <br />
|
||||||
|
|
||||||
|
You'll also need your User ID, which can be found in the url of your user profile page.
|
||||||
|
</Forms.FormText>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
|
||||||
|
settings,
|
||||||
|
|
||||||
|
start() {
|
||||||
|
this.updatePresence();
|
||||||
|
this.updateInterval = setInterval(() => { this.updatePresence(); }, 10000);
|
||||||
|
},
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
clearInterval(this.updateInterval);
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchMediaData(): Promise<MediaData | null> {
|
||||||
|
if (!settings.store.serverUrl || !settings.store.apiKey || !settings.store.userId) {
|
||||||
|
logger.warn("Jellyfin server URL, API key, or user ID is not set in settings.");
|
||||||
|
showToast("JellyfinRPC is not configured.", "failure", {
|
||||||
|
duration: 15000,
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const baseUrl = settings.store.serverUrl.replace(/\/$/, "");
|
||||||
|
const url = `${baseUrl}/Sessions?api_key=${settings.store.apiKey}`;
|
||||||
|
|
||||||
|
const res = await fetch(url);
|
||||||
|
if (!res.ok) throw `${res.status} ${res.statusText}`;
|
||||||
|
|
||||||
|
const sessions = await res.json();
|
||||||
|
const userSession = sessions.find((session: any) =>
|
||||||
|
session.UserId === settings.store.userId && session.NowPlayingItem
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!userSession || !userSession.NowPlayingItem) return null;
|
||||||
|
|
||||||
|
const item = userSession.NowPlayingItem;
|
||||||
|
const playState = userSession.PlayState;
|
||||||
|
|
||||||
|
if (playState?.IsPaused) return null;
|
||||||
|
|
||||||
|
const imageUrl = item.ImageTags?.Primary
|
||||||
|
? `${baseUrl}/Items/${item.Id}/Images/Primary`
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: item.Name || "Unknown",
|
||||||
|
type: item.Type,
|
||||||
|
artist: item.Artists?.[0] || item.AlbumArtist,
|
||||||
|
album: item.Album,
|
||||||
|
seriesName: item.SeriesName,
|
||||||
|
seasonNumber: item.ParentIndexNumber,
|
||||||
|
episodeNumber: item.IndexNumber,
|
||||||
|
year: item.ProductionYear,
|
||||||
|
url: `${baseUrl}/web/#!/details?id=${item.Id}`,
|
||||||
|
imageUrl,
|
||||||
|
duration: item.RunTimeTicks ? Math.floor(item.RunTimeTicks / 10000000) : undefined,
|
||||||
|
position: playState?.PositionTicks ? Math.floor(playState.PositionTicks / 10000000) : undefined
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("Failed to query Jellyfin API", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async updatePresence() {
|
||||||
|
setActivity(await this.getActivity());
|
||||||
|
},
|
||||||
|
|
||||||
|
async getActivity(): Promise<Activity | null> {
|
||||||
|
const mediaData = await this.fetchMediaData();
|
||||||
|
if (!mediaData) return null;
|
||||||
|
|
||||||
|
const largeImage = mediaData.imageUrl;
|
||||||
|
const assets: ActivityAssets = {
|
||||||
|
large_image: largeImage ? await getApplicationAsset(largeImage) : await getApplicationAsset("jellyfin"),
|
||||||
|
large_text: mediaData.album || mediaData.seriesName || undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDetails = () => {
|
||||||
|
if (mediaData.type === "Episode" && mediaData.seriesName) {
|
||||||
|
return mediaData.name;
|
||||||
|
}
|
||||||
|
return mediaData.name;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getState = () => {
|
||||||
|
if (mediaData.type === "Episode" && mediaData.seriesName) {
|
||||||
|
const season = mediaData.seasonNumber ? `S${mediaData.seasonNumber}` : "";
|
||||||
|
const episode = mediaData.episodeNumber ? `E${mediaData.episodeNumber}` : "";
|
||||||
|
return `${mediaData.seriesName} ${season}${episode}`.trim();
|
||||||
|
}
|
||||||
|
return mediaData.artist || (mediaData.year ? `(${mediaData.year})` : undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const timestamps = mediaData.position && mediaData.duration ? {
|
||||||
|
start: Date.now() - (mediaData.position * 1000),
|
||||||
|
end: Date.now() + ((mediaData.duration - mediaData.position) * 1000)
|
||||||
|
} : undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
application_id: applicationId,
|
||||||
|
name: "Jellyfin",
|
||||||
|
|
||||||
|
details: getDetails(),
|
||||||
|
state: getState() || "something",
|
||||||
|
assets,
|
||||||
|
timestamps,
|
||||||
|
|
||||||
|
type: 3,
|
||||||
|
flags: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
|
@ -51,40 +51,40 @@ async function openCompleteQuestUI() {
|
||||||
const applicationId = quest.config.application.id;
|
const applicationId = quest.config.application.id;
|
||||||
const applicationName = quest.config.application.name;
|
const applicationName = quest.config.application.name;
|
||||||
const taskName = ["WATCH_VIDEO", "PLAY_ON_DESKTOP", "STREAM_ON_DESKTOP", "PLAY_ACTIVITY"].find(x => quest.config.taskConfig.tasks[x] != null);
|
const taskName = ["WATCH_VIDEO", "PLAY_ON_DESKTOP", "STREAM_ON_DESKTOP", "PLAY_ACTIVITY"].find(x => quest.config.taskConfig.tasks[x] != null);
|
||||||
|
const icon = `https://cdn.discordapp.com/quests/${quest.id}/${theme}/${quest.config.assets.gameTile}`;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const secondsNeeded = quest.config.taskConfig.tasks[taskName].target;
|
const secondsNeeded = quest.config.taskConfig.tasks[taskName].target;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const secondsDone = quest.userStatus?.progress?.[taskName]?.value ?? 0;
|
let secondsDone = quest.userStatus?.progress?.[taskName]?.value ?? 0;
|
||||||
const icon = `https://cdn.discordapp.com/assets/quests/${quest.id}/${theme}/${quest.config.assets.gameTile}`;
|
|
||||||
if (taskName === "WATCH_VIDEO") {
|
if (taskName === "WATCH_VIDEO") {
|
||||||
const tolerance = 2, speed = 10;
|
const maxFuture = 10, speed = 7, interval = 1;
|
||||||
const diff = Math.floor((Date.now() - new Date(quest.userStatus.enrolledAt).getTime()) / 1000);
|
const enrolledAt = new Date(quest.userStatus.enrolledAt).getTime();
|
||||||
const startingPoint = Math.min(Math.max(Math.ceil(secondsDone), diff), secondsNeeded);
|
|
||||||
const fn = async () => {
|
const fn = async () => {
|
||||||
for (let i = startingPoint; i <= secondsNeeded; i += speed) {
|
while (true) {
|
||||||
try {
|
const maxAllowed = Math.floor((Date.now() - enrolledAt) / 1000) + maxFuture;
|
||||||
await RestAPI.post({ url: `/quests/${quest.id}/video-progress`, body: { timestamp: Math.min(secondsNeeded, i + Math.random()) } });
|
const diff = maxAllowed - secondsDone;
|
||||||
} catch (ex) {
|
const timestamp = secondsDone + speed;
|
||||||
console.log("Failed to send increment of", i, ex);
|
if (diff >= speed) {
|
||||||
|
await RestAPI.post({ url: `/quests/${quest.id}/video-progress`, body: { timestamp: Math.min(secondsNeeded, timestamp + Math.random()) } });
|
||||||
|
secondsDone = Math.min(secondsNeeded, timestamp);
|
||||||
}
|
}
|
||||||
await new Promise(resolve => setTimeout(resolve, tolerance * 1000));
|
if (timestamp >= secondsNeeded) {
|
||||||
}
|
break;
|
||||||
if ((secondsNeeded - secondsDone) % speed !== 0) {
|
}
|
||||||
await RestAPI.post({ url: `/quests/${quest.id}/video-progress`, body: { timestamp: secondsNeeded } });
|
await new Promise(resolve => setTimeout(resolve, interval * 1000));
|
||||||
showNotification({
|
|
||||||
title: `${applicationName} - Quest Completer`,
|
|
||||||
body: "Quest Completed.",
|
|
||||||
icon: icon,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
showNotification({
|
||||||
|
title: `${applicationName} - Quest Completer`,
|
||||||
|
body: "Quest Completed.",
|
||||||
|
icon: icon,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
fn();
|
fn();
|
||||||
showNotification({
|
showNotification({
|
||||||
title: `${applicationName} - Quest Completer`,
|
title: `${applicationName} - Quest Completer`,
|
||||||
body: `Wait for ${Math.ceil((secondsNeeded - startingPoint) / speed * tolerance)} more seconds.`,
|
body: `Spoofing video for ${applicationName}.`,
|
||||||
icon: icon,
|
icon: icon,
|
||||||
});
|
});
|
||||||
console.log(`Spoofing video for ${applicationName}.`);
|
|
||||||
} else if (taskName === "PLAY_ON_DESKTOP") {
|
} else if (taskName === "PLAY_ON_DESKTOP") {
|
||||||
RestAPI.get({ url: `/applications/public?application_ids=${applicationId}` }).then(res => {
|
RestAPI.get({ url: `/applications/public?application_ids=${applicationId}` }).then(res => {
|
||||||
const appData = res.body[0];
|
const appData = res.body[0];
|
||||||
|
|
81
src/equicordplugins/streamingCodecDisabler/index.ts
Normal file
81
src/equicordplugins/streamingCodecDisabler/index.ts
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2025 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { definePluginSettings, Settings } from "@api/Settings";
|
||||||
|
import { EquicordDevs } from "@utils/constants";
|
||||||
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
|
import { findStoreLazy } from "@webpack";
|
||||||
|
|
||||||
|
let mediaEngine = findStoreLazy("MediaEngineStore");
|
||||||
|
|
||||||
|
const originalCodecStatuses: {
|
||||||
|
AV1: boolean,
|
||||||
|
H265: boolean,
|
||||||
|
H264: boolean;
|
||||||
|
} = {
|
||||||
|
AV1: true,
|
||||||
|
H265: true,
|
||||||
|
H264: true
|
||||||
|
};
|
||||||
|
|
||||||
|
const settings = definePluginSettings({
|
||||||
|
disableAv1Codec: {
|
||||||
|
description: "Make Discord not consider using AV1 for streaming.",
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
disableH265Codec: {
|
||||||
|
description: "Make Discord not consider using H265 for streaming.",
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
disableH264Codec: {
|
||||||
|
description: "Make Discord not consider using H264 for streaming.",
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "StreamingCodecDisabler",
|
||||||
|
description: "Disable codecs for streaming of your choice",
|
||||||
|
authors: [EquicordDevs.davidkra230],
|
||||||
|
settings,
|
||||||
|
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: "setVideoBroadcast(this.shouldConnectionBroadcastVideo",
|
||||||
|
replacement: {
|
||||||
|
match: /setGoLiveSource\(.,.\)\{/,
|
||||||
|
replace: "$&$self.updateDisabledCodecs();"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
async updateDisabledCodecs() {
|
||||||
|
mediaEngine.setAv1Enabled(originalCodecStatuses.AV1 && !Settings.plugins.StreamingCodecDisabler.disableAv1Codec);
|
||||||
|
mediaEngine.setH265Enabled(originalCodecStatuses.H265 && !Settings.plugins.StreamingCodecDisabler.disableH265Codec);
|
||||||
|
mediaEngine.setH264Enabled(originalCodecStatuses.H264 && !Settings.plugins.StreamingCodecDisabler.disableH264Codec);
|
||||||
|
},
|
||||||
|
|
||||||
|
async start() {
|
||||||
|
mediaEngine = mediaEngine.getMediaEngine();
|
||||||
|
const options = Object.keys(originalCodecStatuses);
|
||||||
|
// [{"codec":"","decode":false,"encode":false}]
|
||||||
|
const CodecCapabilities = JSON.parse(await new Promise(res => mediaEngine.getCodecCapabilities(res)));
|
||||||
|
CodecCapabilities.forEach((codec: { codec: string; encode: boolean; }) => {
|
||||||
|
if (options.includes(codec.codec)) {
|
||||||
|
originalCodecStatuses[codec.codec] = codec.encode;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async stop() {
|
||||||
|
mediaEngine.setAv1Enabled(originalCodecStatuses.AV1);
|
||||||
|
mediaEngine.setH265Enabled(originalCodecStatuses.H265);
|
||||||
|
mediaEngine.setH264Enabled(originalCodecStatuses.H264);
|
||||||
|
}
|
||||||
|
});
|
|
@ -8,7 +8,6 @@ import { openModal } from "@utils/index";
|
||||||
import { OAuth2AuthorizeModal, showToast, Toasts } from "@webpack/common";
|
import { OAuth2AuthorizeModal, showToast, Toasts } from "@webpack/common";
|
||||||
|
|
||||||
const databaseTimezones: Record<string, { value: string | null; }> = {};
|
const databaseTimezones: Record<string, { value: string | null; }> = {};
|
||||||
|
|
||||||
const DOMAIN = "https://timezone.creations.works";
|
const DOMAIN = "https://timezone.creations.works";
|
||||||
const REDIRECT_URI = `${DOMAIN}/auth/discord/callback`;
|
const REDIRECT_URI = `${DOMAIN}/auth/discord/callback`;
|
||||||
const CLIENT_ID = "1377021506810417173";
|
const CLIENT_ID = "1377021506810417173";
|
||||||
|
@ -26,7 +25,6 @@ export async function loadDatabaseTimezones(): Promise<boolean> {
|
||||||
const res = await fetch(`${DOMAIN}/list`, {
|
const res = await fetch(`${DOMAIN}/list`, {
|
||||||
headers: { Accept: "application/json" }
|
headers: { Accept: "application/json" }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
for (const id in json) {
|
for (const id in json) {
|
||||||
|
@ -34,10 +32,8 @@ export async function loadDatabaseTimezones(): Promise<boolean> {
|
||||||
value: json[id]?.timezone ?? null
|
value: json[id]?.timezone ?? null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to fetch timezones list:", e);
|
console.error("Failed to fetch timezones list:", e);
|
||||||
|
@ -45,30 +41,93 @@ export async function loadDatabaseTimezones(): Promise<boolean> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setTimezone(timezone: string): Promise<boolean> {
|
async function checkAuthentication(): Promise<boolean> {
|
||||||
const res = await fetch(`${DOMAIN}/set?timezone=${encodeURIComponent(timezone)}`, {
|
try {
|
||||||
method: "POST",
|
const res = await fetch(`${DOMAIN}/me`, {
|
||||||
headers: {
|
credentials: "include",
|
||||||
"Content-Type": "application/json",
|
headers: { Accept: "application/json" }
|
||||||
Accept: "application/json"
|
});
|
||||||
},
|
return res.ok;
|
||||||
credentials: "include"
|
} catch (e) {
|
||||||
});
|
console.error("Failed to check authentication:", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return res.ok;
|
export async function setTimezone(timezone: string): Promise<boolean> {
|
||||||
|
const isAuthenticated = await checkAuthentication();
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
authModal(() => {
|
||||||
|
setTimezoneInternal(timezone).then(resolve);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return setTimezoneInternal(timezone);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setTimezoneInternal(timezone: string): Promise<boolean> {
|
||||||
|
const formData = new URLSearchParams();
|
||||||
|
formData.append("timezone", timezone);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${DOMAIN}/set`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
Accept: "application/json"
|
||||||
|
},
|
||||||
|
credentials: "include",
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const error = await res.json().catch(() => ({ message: "Unknown error" }));
|
||||||
|
showToast(error.message || "Failed to set timezone", Toasts.Type.FAILURE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast("Timezone updated successfully!", Toasts.Type.SUCCESS);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error setting timezone:", e);
|
||||||
|
showToast("Failed to set timezone", Toasts.Type.FAILURE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteTimezone(): Promise<boolean> {
|
export async function deleteTimezone(): Promise<boolean> {
|
||||||
const res = await fetch(`${DOMAIN}/delete`, {
|
const isAuthenticated = await checkAuthentication();
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Accept: "application/json"
|
|
||||||
},
|
|
||||||
credentials: "include"
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.ok;
|
if (!isAuthenticated) {
|
||||||
|
showToast("You must be logged in to delete your timezone", Toasts.Type.FAILURE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${DOMAIN}/delete`, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json"
|
||||||
|
},
|
||||||
|
credentials: "include"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const error = await res.json().catch(() => ({ message: "Unknown error" }));
|
||||||
|
showToast(error.message || "Failed to delete timezone", Toasts.Type.FAILURE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast("Timezone deleted successfully!", Toasts.Type.SUCCESS);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error deleting timezone:", e);
|
||||||
|
showToast("Failed to delete timezone", Toasts.Type.FAILURE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function authModal(callback?: () => void) {
|
export function authModal(callback?: () => void) {
|
||||||
|
@ -83,21 +142,17 @@ export function authModal(callback?: () => void) {
|
||||||
cancelCompletesFlow={false}
|
cancelCompletesFlow={false}
|
||||||
callback={async (res: any) => {
|
callback={async (res: any) => {
|
||||||
if (!res || !res.location) return;
|
if (!res || !res.location) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = new URL(res.location);
|
const url = new URL(res.location);
|
||||||
|
|
||||||
const r = await fetch(url, {
|
const r = await fetch(url, {
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: { Accept: "application/json" }
|
headers: { Accept: "application/json" }
|
||||||
});
|
});
|
||||||
|
|
||||||
const json = await r.json();
|
const json = await r.json();
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
showToast(json.message ?? "Authorization failed", Toasts.Type.FAILURE);
|
showToast(json.message ?? "Authorization failed", Toasts.Type.FAILURE);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
showToast("Authorization successful!", Toasts.Type.SUCCESS);
|
showToast("Authorization successful!", Toasts.Type.SUCCESS);
|
||||||
callback?.();
|
callback?.();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -17,7 +17,7 @@ import { findByPropsLazy } from "@webpack";
|
||||||
import { Button, Menu, showToast, Toasts, Tooltip, useEffect, UserStore, useState } from "@webpack/common";
|
import { Button, Menu, showToast, Toasts, Tooltip, useEffect, UserStore, useState } from "@webpack/common";
|
||||||
import { Message, User } from "discord-types/general";
|
import { Message, User } from "discord-types/general";
|
||||||
|
|
||||||
import { authModal, deleteTimezone, getTimezone, loadDatabaseTimezones, setUserDatabaseTimezone } from "./database";
|
import { deleteTimezone, getTimezone, loadDatabaseTimezones, setUserDatabaseTimezone } from "./database";
|
||||||
import { SetTimezoneModal } from "./TimezoneModal";
|
import { SetTimezoneModal } from "./TimezoneModal";
|
||||||
|
|
||||||
export let timezones: Record<string, string | null> = {};
|
export let timezones: Record<string, string | null> = {};
|
||||||
|
@ -68,9 +68,7 @@ export const settings = definePluginSettings({
|
||||||
type: OptionType.COMPONENT,
|
type: OptionType.COMPONENT,
|
||||||
component: () => (
|
component: () => (
|
||||||
<Button onClick={() => {
|
<Button onClick={() => {
|
||||||
authModal(async () => {
|
openModal(modalProps => <SetTimezoneModal userId={UserStore.getCurrentUser().id} modalProps={modalProps} database={true} />);
|
||||||
openModal(modalProps => <SetTimezoneModal userId={UserStore.getCurrentUser().id} modalProps={modalProps} database={true} />);
|
|
||||||
});
|
|
||||||
}}>
|
}}>
|
||||||
Set Timezone on Database
|
Set Timezone on Database
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -83,11 +81,19 @@ export const settings = definePluginSettings({
|
||||||
component: () => (
|
component: () => (
|
||||||
<Button
|
<Button
|
||||||
color={Button.Colors.RED}
|
color={Button.Colors.RED}
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
authModal(async () => {
|
try {
|
||||||
await setUserDatabaseTimezone(UserStore.getCurrentUser().id, null);
|
await setUserDatabaseTimezone(UserStore.getCurrentUser().id, null);
|
||||||
await deleteTimezone();
|
const success = await deleteTimezone();
|
||||||
});
|
if (success) {
|
||||||
|
showToast("Database timezone reset successfully!", Toasts.Type.SUCCESS);
|
||||||
|
} else {
|
||||||
|
showToast("Failed to reset database timezone", Toasts.Type.FAILURE);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error resetting database timezone:", error);
|
||||||
|
showToast("Failed to reset database timezone", Toasts.Type.FAILURE);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Reset Database Timezones
|
Reset Database Timezones
|
||||||
|
@ -228,9 +234,7 @@ export default definePlugin({
|
||||||
|
|
||||||
toolboxActions: {
|
toolboxActions: {
|
||||||
"Set Database Timezone": () => {
|
"Set Database Timezone": () => {
|
||||||
authModal(async () => {
|
openModal(modalProps => <SetTimezoneModal userId={UserStore.getCurrentUser().id} modalProps={modalProps} database={true} />);
|
||||||
openModal(modalProps => <SetTimezoneModal userId={UserStore.getCurrentUser().id} modalProps={modalProps} database={true} />);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
"Refresh Database Timezones": async () => {
|
"Refresh Database Timezones": async () => {
|
||||||
try {
|
try {
|
||||||
|
@ -265,9 +269,7 @@ export default definePlugin({
|
||||||
<Button
|
<Button
|
||||||
color={Button.Colors.GREEN}
|
color={Button.Colors.GREEN}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
authModal(async () => {
|
openModal(modalProps => <SetTimezoneModal userId={UserStore.getCurrentUser().id} modalProps={modalProps} database={true} />);
|
||||||
openModal(modalProps => <SetTimezoneModal userId={UserStore.getCurrentUser().id} modalProps={modalProps} database={true} />);
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Want to save your timezone to the database? Click here to set it.
|
Want to save your timezone to the database? Click here to set it.
|
||||||
|
|
|
@ -29,7 +29,7 @@ export default definePlugin({
|
||||||
authors: [Devs.AutumnVN],
|
authors: [Devs.AutumnVN],
|
||||||
start() {
|
start() {
|
||||||
(function connect() {
|
(function connect() {
|
||||||
ws = new WebSocket("ws://localhost:24050/websocket/v2");
|
ws = new WebSocket("ws://127.0.0.1:24050/websocket/v2");
|
||||||
ws.addEventListener("error", () => ws.close());
|
ws.addEventListener("error", () => ws.close());
|
||||||
ws.addEventListener("close", () => wsReconnect = setTimeout(connect, 5000));
|
ws.addEventListener("close", () => wsReconnect = setTimeout(connect, 5000));
|
||||||
ws.addEventListener("message", ({ data }) => throttledOnMessage(data));
|
ws.addEventListener("message", ({ data }) => throttledOnMessage(data));
|
||||||
|
|
|
@ -8,8 +8,7 @@ import { NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||||
import { openModal } from "@utils/modal";
|
import { openModal } from "@utils/modal";
|
||||||
import { ChannelStore, FluxDispatcher, Menu } from "@webpack/common";
|
import { ChannelStore, FluxDispatcher, Menu } from "@webpack/common";
|
||||||
|
|
||||||
import { SetCustomWallpaperModal, SetDiscordWallpaperModal } from "./modal";
|
import { SetWallpaperModal } from "./modal";
|
||||||
import { ChatWallpaperStore, fetchWallpapers } from "./util";
|
|
||||||
|
|
||||||
|
|
||||||
const addWallpaperMenu = (channelId?: string, guildId?: string) => {
|
const addWallpaperMenu = (channelId?: string, guildId?: string) => {
|
||||||
|
@ -22,25 +21,18 @@ const addWallpaperMenu = (channelId?: string, guildId?: string) => {
|
||||||
url,
|
url,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu.MenuItem label="Wallpaper Free" key="vc-wpfree-menu" id="vc-wpfree-menu">
|
<Menu.MenuItem label="Wallpaper Free" key="vc-wpfree-menu" id="vc-wpfree-menu">
|
||||||
<Menu.MenuItem
|
<Menu.MenuItem
|
||||||
label="Set custom wallpaper"
|
label="Set Wallpaper"
|
||||||
id="vc-wpfree-set-custom"
|
id="vc-wpfree-set-wallpaper"
|
||||||
action={() => openModal(props => <SetCustomWallpaperModal props={props} onSelect={setWallpaper} />)}
|
action={() => openModal(props => <SetWallpaperModal props={props} onSelect={setWallpaper} />)}
|
||||||
/>
|
|
||||||
<Menu.MenuItem
|
|
||||||
label="Set a Discord wallpaper"
|
|
||||||
id="vc-wpfree-set-discord"
|
|
||||||
action={async () => {
|
|
||||||
ChatWallpaperStore.shouldFetchWallpapers && await fetchWallpapers();
|
|
||||||
openModal(props => <SetDiscordWallpaperModal props={props} onSelect={setWallpaper} />);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<Menu.MenuSeparator />
|
<Menu.MenuSeparator />
|
||||||
<Menu.MenuItem
|
<Menu.MenuItem
|
||||||
label="Remove Custom Wallpaper"
|
label="Remove Wallpaper"
|
||||||
id="vc-wpfree-remove"
|
id="vc-wpfree-remove-wallpaper"
|
||||||
color="danger"
|
color="danger"
|
||||||
action={() => setWallpaper(void 0)}
|
action={() => setWallpaper(void 0)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -5,38 +5,37 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
|
import { ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
|
||||||
import { Button, lodash, Text, TextInput, useState, useStateFromStores } from "@webpack/common";
|
import { Button, Text, TextInput, useState } from "@webpack/common";
|
||||||
|
|
||||||
import { ChatWallpaperStore, Wallpaper } from "./util";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
props: ModalProps;
|
props: ModalProps;
|
||||||
onSelect: (url: string) => void;
|
onSelect: (url: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SetCustomWallpaperModal({ props, onSelect }: Props) {
|
export function SetWallpaperModal({ props, onSelect }: Props) {
|
||||||
const [url, setUrl] = useState("");
|
const [url, setUrl] = useState("");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalRoot {...props} size={ModalSize.SMALL}>
|
<ModalRoot {...props} size={ModalSize.SMALL}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
<Text variant="heading-lg/normal" style={{ marginBottom: 8 }}>
|
<Text variant="heading-lg/normal" style={{ marginBottom: 8 }}>
|
||||||
Set a custom wallpaper
|
Set wallpaper
|
||||||
</Text>
|
</Text>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
||||||
|
<Text>The image url</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
placeholder="The image url"
|
|
||||||
value={url}
|
value={url}
|
||||||
onChange={setUrl}
|
onChange={u => {
|
||||||
|
setUrl(u);
|
||||||
|
}}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
{url && (
|
{url && (
|
||||||
<img
|
<img
|
||||||
|
alt=""
|
||||||
src={url}
|
src={url}
|
||||||
alt="Wallpaper preview"
|
|
||||||
style={{
|
style={{
|
||||||
display: "block",
|
display: "block",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
@ -60,57 +59,6 @@ export function SetCustomWallpaperModal({ props, onSelect }: Props) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</ModalRoot>
|
</ModalRoot >
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SetDiscordWallpaperModal({ props, onSelect }: Props) {
|
|
||||||
const discordWallpapers: Wallpaper[] = useStateFromStores([ChatWallpaperStore], () => ChatWallpaperStore.wallpapers);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalRoot {...props} size={ModalSize.MEDIUM}>
|
|
||||||
<ModalHeader>
|
|
||||||
<Text variant="heading-lg/normal" style={{ marginBottom: 8 }}>
|
|
||||||
Choose a Discord Wallpaper
|
|
||||||
</Text>
|
|
||||||
</ModalHeader>
|
|
||||||
<ModalContent>
|
|
||||||
<div className="vc-wpfree-discord-wp-modal">
|
|
||||||
{lodash.chunk(discordWallpapers, 2).map(group => {
|
|
||||||
const main = group[0];
|
|
||||||
return (
|
|
||||||
<div key={main.id} className="vc-wpfree-discord-wp-icon-container">
|
|
||||||
<figure style={{ margin: 0, textAlign: "center" }}>
|
|
||||||
<img
|
|
||||||
className="vc-wpfree-discord-wp-icon-img"
|
|
||||||
src={`https://cdn.discordapp.com/assets/content/${main.default.icon}`}
|
|
||||||
alt={main.label}
|
|
||||||
/>
|
|
||||||
<figcaption>
|
|
||||||
<Text variant="text-md/normal">{main.label}</Text>
|
|
||||||
</figcaption>
|
|
||||||
</figure>
|
|
||||||
<div className="vc-wpfree-discord-set-buttons">
|
|
||||||
{group.map(wp => (
|
|
||||||
<Button
|
|
||||||
key={wp.id}
|
|
||||||
size={Button.Sizes.SMALL}
|
|
||||||
color={Button.Colors.BRAND}
|
|
||||||
onClick={() => {
|
|
||||||
onSelect(`https://cdn.discordapp.com/assets/content/${wp.default.asset}`);
|
|
||||||
props.onClose();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{wp.isBlurred ? "Blurred" : "Normal"}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</ModalContent>
|
|
||||||
</ModalRoot>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -6,13 +6,9 @@
|
||||||
|
|
||||||
import { openModal } from "@utils/modal";
|
import { openModal } from "@utils/modal";
|
||||||
import { makeCodeblock } from "@utils/text";
|
import { makeCodeblock } from "@utils/text";
|
||||||
import { findByCodeLazy, findStoreLazy } from "@webpack";
|
|
||||||
import { Button, FluxDispatcher, Parser } from "@webpack/common";
|
import { Button, FluxDispatcher, Parser } from "@webpack/common";
|
||||||
|
|
||||||
import { SetCustomWallpaperModal, SetDiscordWallpaperModal } from "./modal";
|
import { SetWallpaperModal } from "./modal";
|
||||||
|
|
||||||
export const ChatWallpaperStore = findStoreLazy("ChatWallpaperStore");
|
|
||||||
export const fetchWallpapers = findByCodeLazy('type:"FETCH_CHAT_WALLPAPERS_SUCCESS"');
|
|
||||||
|
|
||||||
export function GlobalDefaultComponent() {
|
export function GlobalDefaultComponent() {
|
||||||
const setGlobal = (url?: string) => {
|
const setGlobal = (url?: string) => {
|
||||||
|
@ -26,13 +22,8 @@ export function GlobalDefaultComponent() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button onClick={() => {
|
<Button onClick={() => {
|
||||||
openModal(props => <SetCustomWallpaperModal props={props} onSelect={setGlobal} />);
|
openModal(props => <SetWallpaperModal props={props} onSelect={setGlobal} />);
|
||||||
}}>Set a global custom wallpaper</Button>
|
}}>Set a global wallpaper</Button>
|
||||||
|
|
||||||
<Button onClick={async () => {
|
|
||||||
ChatWallpaperStore.shouldFetchWallpapers && await fetchWallpapers();
|
|
||||||
openModal(props => <SetDiscordWallpaperModal props={props} onSelect={setGlobal} />);
|
|
||||||
}}>Set a global Discord wallpaper</Button>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
color={Button.Colors.RED}
|
color={Button.Colors.RED}
|
||||||
|
@ -52,30 +43,10 @@ export function GlobalDefaultComponent() {
|
||||||
|
|
||||||
export function TipsComponent() {
|
export function TipsComponent() {
|
||||||
const tipText = `
|
const tipText = `
|
||||||
[class^=wallpaperContainer] {
|
.vc-wpfree-wp-container {
|
||||||
transform: scaleX(-1); /* flip it horizontally */
|
transform: scaleX(-1); /* flip it horizontally */
|
||||||
filter: blur(4px); /* apply a blur */
|
filter: blur(4px); /* apply a blur */
|
||||||
opacity: 0.7; /* self-explanatory */
|
opacity: 0.7; /* self-explanatory */
|
||||||
}`;
|
}`;
|
||||||
return Parser.parse(makeCodeblock(tipText, "css"));
|
return Parser.parse(makeCodeblock(tipText, "css"));
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Wallpaper {
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
default: Default;
|
|
||||||
variants: Variants;
|
|
||||||
isBlurred: boolean;
|
|
||||||
designGroupId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Default {
|
|
||||||
asset: string;
|
|
||||||
icon: string;
|
|
||||||
thumbhash: string;
|
|
||||||
opacity?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Variants {
|
|
||||||
dark: Default;
|
|
||||||
}
|
|
||||||
|
|
|
@ -7,59 +7,47 @@
|
||||||
import "./styles.css";
|
import "./styles.css";
|
||||||
|
|
||||||
import { definePluginSettings } from "@api/Settings";
|
import { definePluginSettings } from "@api/Settings";
|
||||||
|
import { ErrorBoundary } from "@components/index";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { useStateFromStores } from "@webpack/common";
|
import { useStateFromStores } from "@webpack/common";
|
||||||
import { Channel } from "discord-types/general";
|
import { Channel } from "discord-types/general";
|
||||||
|
|
||||||
import { ChannelContextPatch, GuildContextPatch, UserContextPatch } from "./components/ctxmenu";
|
import { ChannelContextPatch, GuildContextPatch, UserContextPatch } from "./components/ctxmenu";
|
||||||
import { GlobalDefaultComponent, TipsComponent, Wallpaper } from "./components/util";
|
import { GlobalDefaultComponent, TipsComponent } from "./components/util";
|
||||||
import { WallpaperFreeStore } from "./store";
|
import { WallpaperFreeStore } from "./store";
|
||||||
|
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
export const settings = definePluginSettings({
|
||||||
forceReplace: {
|
globalDefault: {
|
||||||
description: "If a dm wallpaper is already set, your custom wallpaper will be used instead.",
|
description: "Set a global default wallpaper for all channels.",
|
||||||
type: OptionType.BOOLEAN,
|
type: OptionType.COMPONENT,
|
||||||
default: false,
|
component: GlobalDefaultComponent
|
||||||
},
|
},
|
||||||
stylingTips: {
|
stylingTips: {
|
||||||
description: "",
|
description: "",
|
||||||
type: OptionType.COMPONENT,
|
type: OptionType.COMPONENT,
|
||||||
component: TipsComponent,
|
component: TipsComponent,
|
||||||
},
|
|
||||||
globalDefault: {
|
|
||||||
description: "Set a global default wallpaper for all channels.",
|
|
||||||
type: OptionType.COMPONENT,
|
|
||||||
component: GlobalDefaultComponent
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "WallpaperFree",
|
name: "WallpaperFree",
|
||||||
authors: [Devs.Joona],
|
authors: [Devs.Joona],
|
||||||
description: "Use the DM wallpapers anywhere or set a custom wallpaper",
|
description: "Recreation of the old DM wallpaper experiment; Set a background image for any channel or server.",
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: ".wallpaperContainer,",
|
find: ".handleSendMessage,onResize",
|
||||||
group: true,
|
group: true,
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /return null==(\i).+?\?null:/,
|
match: /return.{1,150},(?=keyboardModeEnabled)/,
|
||||||
replace: "const vcWpFreeCustom = $self.customWallpaper(arguments[0].channel,$1);return !($1||vcWpFreeCustom)?null:"
|
replace: "const vcWallpaperFreeUrl=$self.WallpaperState(arguments[0].channel);$&vcWallpaperFreeUrl,"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /,{chatWallpaperState:/,
|
match: /}\)]}\)](?=.{1,30}messages-)/,
|
||||||
replace: "$&vcWpFreeCustom||"
|
replace: "$&.toSpliced(0,0,$self.Wallpaper({url:this.props.vcWallpaperFreeUrl}))"
|
||||||
},
|
}
|
||||||
{
|
|
||||||
match: /(\i)=(.{1,50}asset.+?(?=,\i=))(?=.+?concat\(\1)/,
|
|
||||||
replace: "$1=arguments[0].chatWallpaperState.vcWallpaperUrl||($2)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
match: /(\i\.isViewable&&)(null!=\i)/,
|
|
||||||
replace: "$1($2||arguments[0].chatWallpaperState.vcWallpaperUrl)"
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -71,21 +59,17 @@ export default definePlugin({
|
||||||
"guild-context": GuildContextPatch,
|
"guild-context": GuildContextPatch,
|
||||||
"gdm-context": ChannelContextPatch,
|
"gdm-context": ChannelContextPatch,
|
||||||
},
|
},
|
||||||
customWallpaper(channel: Channel, wp: Wallpaper | undefined) {
|
Wallpaper({ url }: { url: string; }) {
|
||||||
const { forceReplace } = settings.use(["forceReplace"]);
|
// no we cant place the hook here
|
||||||
const url = useStateFromStores([WallpaperFreeStore], () => WallpaperFreeStore.getUrl(channel));
|
if (!url) return null;
|
||||||
|
|
||||||
if (!forceReplace && wp?.id)
|
return <ErrorBoundary noop>
|
||||||
return wp;
|
<div className="wallpaperContainer vc-wpfree-wp-container" style={{
|
||||||
|
backgroundImage: `url(${url})`,
|
||||||
if (url) {
|
}}></div>
|
||||||
return {
|
</ErrorBoundary>;
|
||||||
wallpaperId: "id",
|
|
||||||
vcWallpaperUrl: url,
|
|
||||||
isViewable: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return void 0;
|
|
||||||
},
|
},
|
||||||
|
WallpaperState(channel: Channel) {
|
||||||
|
return useStateFromStores([WallpaperFreeStore], () => WallpaperFreeStore.getUrl(channel));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,30 +1,8 @@
|
||||||
.vc-wpfree-discord-wp-modal {
|
.vc-wpfree-wp-container,
|
||||||
display: grid;
|
.wallpaperContainer {
|
||||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
background-position: 100% 100%;
|
||||||
gap: 24px;
|
background-repeat: no-repeat;
|
||||||
padding: 8px 0;
|
background-size: cover;
|
||||||
}
|
inset: 0;
|
||||||
|
position: absolute;
|
||||||
.vc-wpfree-discord-wp-icon-container {
|
|
||||||
border-radius: 10px;
|
|
||||||
box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
|
|
||||||
padding: 16px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-wpfree-discord-wp-icon-img {
|
|
||||||
width: 120px;
|
|
||||||
height: 68px;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: 6px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
border: 1px solid var(--background-modifier-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.vc-wpfree-discord-set-buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
margin-top: 12px
|
|
||||||
}
|
}
|
||||||
|
|
3
src/globals.d.ts
vendored
3
src/globals.d.ts
vendored
|
@ -29,11 +29,12 @@ declare global {
|
||||||
* replace: "IS_WEB?foo:bar"
|
* replace: "IS_WEB?foo:bar"
|
||||||
* // GOOD
|
* // GOOD
|
||||||
* replace: IS_WEB ? "foo" : "bar"
|
* replace: IS_WEB ? "foo" : "bar"
|
||||||
* // also good
|
* // also okay
|
||||||
* replace: `${IS_WEB}?foo:bar`
|
* replace: `${IS_WEB}?foo:bar`
|
||||||
*/
|
*/
|
||||||
export var IS_WEB: boolean;
|
export var IS_WEB: boolean;
|
||||||
export var IS_EXTENSION: boolean;
|
export var IS_EXTENSION: boolean;
|
||||||
|
export var IS_USERSCRIPT: 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_DEV: boolean;
|
||||||
|
|
142
src/main/csp.ts
Normal file
142
src/main/csp.ts
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2025 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { session } from "electron";
|
||||||
|
|
||||||
|
type PolicyMap = Record<string, string[]>;
|
||||||
|
|
||||||
|
export const ConnectSrc = ["connect-src"];
|
||||||
|
export const MediaSrc = [...ConnectSrc, "img-src", "media-src"];
|
||||||
|
export const CssSrc = ["style-src", "font-src"];
|
||||||
|
export const MediaAndCssSrc = [...MediaSrc, ...CssSrc];
|
||||||
|
export const MediaScriptsAndCssSrc = [...MediaAndCssSrc, "script-src", "worker-src"];
|
||||||
|
|
||||||
|
// Plugins can whitelist their own domains by importing this object in their native.ts
|
||||||
|
// script and just adding to it. But generally, you should just edit this file instead
|
||||||
|
|
||||||
|
export const CspPolicies: PolicyMap = {
|
||||||
|
"*.github.io": MediaAndCssSrc, // GitHub pages, used by most themes
|
||||||
|
"github.com": MediaAndCssSrc, // GitHub content (stuff uploaded to markdown forms), used by most themes
|
||||||
|
"raw.githubusercontent.com": MediaAndCssSrc, // GitHub raw, used by some themes
|
||||||
|
"*.gitlab.io": MediaAndCssSrc, // GitLab pages, used by some themes
|
||||||
|
"gitlab.com": MediaAndCssSrc, // GitLab raw, used by some themes
|
||||||
|
"*.codeberg.page": MediaAndCssSrc, // Codeberg pages, used by some themes
|
||||||
|
"codeberg.org": MediaAndCssSrc, // Codeberg raw, used by some themes
|
||||||
|
|
||||||
|
"*.githack.com": MediaAndCssSrc, // githack (namely raw.githack.com), used by some themes
|
||||||
|
"jsdelivr.net": MediaAndCssSrc, // jsDelivr, used by very few themes
|
||||||
|
|
||||||
|
"fonts.googleapis.com": CssSrc, // Google Fonts, used by many themes
|
||||||
|
|
||||||
|
"i.imgur.com": MediaSrc, // Imgur, used by some themes
|
||||||
|
"i.ibb.co": MediaSrc, // ImgBB, used by some themes
|
||||||
|
"i.pinimg.com": MediaSrc, // Pinterest, used by some themes
|
||||||
|
"*.tenor.com": MediaSrc, // Tenor, used by some themes
|
||||||
|
"files.catbox.moe": MediaSrc, // Catbox, used by some themes
|
||||||
|
|
||||||
|
"cdn.discordapp.com": MediaAndCssSrc, // Discord CDN, used by Vencord and some themes to load media
|
||||||
|
"media.discordapp.net": MediaSrc, // Discord media CDN, possible alternative to Discord CDN
|
||||||
|
|
||||||
|
// CDNs used for some things by Vencord.
|
||||||
|
// FIXME: we really should not be using CDNs anymore
|
||||||
|
"cdnjs.cloudflare.com": MediaScriptsAndCssSrc,
|
||||||
|
"cdn.jsdelivr.net": MediaScriptsAndCssSrc,
|
||||||
|
|
||||||
|
// Function Specific
|
||||||
|
"api.github.com": ConnectSrc, // used for updating Vencord itself
|
||||||
|
"ws.audioscrobbler.com": ConnectSrc, // Last.fm API
|
||||||
|
"translate-pa.googleapis.com": ConnectSrc, // Google Translate API
|
||||||
|
"*.vencord.dev": MediaSrc, // VenCloud (api.vencord.dev) and Badges (badges.vencord.dev)
|
||||||
|
"manti.vendicated.dev": MediaSrc, // ReviewDB API
|
||||||
|
"decor.fieryflames.dev": ConnectSrc, // Decor API
|
||||||
|
"ugc.decor.fieryflames.dev": MediaSrc, // Decor CDN
|
||||||
|
"sponsor.ajay.app": ConnectSrc, // Dearrow API
|
||||||
|
"dearrow-thumb.ajay.app": MediaSrc, // Dearrow Thumbnail CDN
|
||||||
|
"usrbg.is-hardly.online": MediaSrc, // USRBG API
|
||||||
|
"icons.duckduckgo.com": MediaSrc, // DuckDuckGo Favicon API (Reverse Image Search)
|
||||||
|
};
|
||||||
|
|
||||||
|
const findHeader = (headers: PolicyMap, headerName: Lowercase<string>) => {
|
||||||
|
return Object.keys(headers).find(h => h.toLowerCase() === headerName);
|
||||||
|
};
|
||||||
|
|
||||||
|
const parsePolicy = (policy: string): PolicyMap => {
|
||||||
|
const result: PolicyMap = {};
|
||||||
|
policy.split(";").forEach(directive => {
|
||||||
|
const [directiveKey, ...directiveValue] = directive.trim().split(/\s+/g);
|
||||||
|
if (directiveKey && !Object.prototype.hasOwnProperty.call(result, directiveKey)) {
|
||||||
|
result[directiveKey] = directiveValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const stringifyPolicy = (policy: PolicyMap): string =>
|
||||||
|
Object.entries(policy)
|
||||||
|
.filter(([, values]) => values?.length)
|
||||||
|
.map(directive => directive.flat().join(" "))
|
||||||
|
.join("; ");
|
||||||
|
|
||||||
|
|
||||||
|
const patchCsp = (headers: PolicyMap) => {
|
||||||
|
const reportOnlyHeader = findHeader(headers, "content-security-policy-report-only");
|
||||||
|
if (reportOnlyHeader)
|
||||||
|
delete headers[reportOnlyHeader];
|
||||||
|
|
||||||
|
const header = findHeader(headers, "content-security-policy");
|
||||||
|
|
||||||
|
if (header) {
|
||||||
|
const csp = parsePolicy(headers[header][0]);
|
||||||
|
|
||||||
|
const pushDirective = (directive: string, ...values: string[]) => {
|
||||||
|
csp[directive] ??= [...(csp["default-src"] ?? [])];
|
||||||
|
csp[directive].push(...values);
|
||||||
|
};
|
||||||
|
|
||||||
|
pushDirective("style-src", "'unsafe-inline'");
|
||||||
|
// we could make unsafe-inline safe by using strict-dynamic with a random nonce on our Vencord loader script https://content-security-policy.com/strict-dynamic/
|
||||||
|
// HOWEVER, at the time of writing (24 Jan 2025), Discord is INSANE and also uses unsafe-inline
|
||||||
|
// Once they stop using it, we also should
|
||||||
|
pushDirective("script-src", "'unsafe-inline'", "'unsafe-eval'");
|
||||||
|
|
||||||
|
for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src", "worker-src"]) {
|
||||||
|
pushDirective(directive, "blob:", "data:", "vencord:");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [host, directives] of Object.entries(CspPolicies)) {
|
||||||
|
for (const directive of directives) {
|
||||||
|
pushDirective(directive, host);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
headers[header] = [stringifyPolicy(csp)];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export function initCsp() {
|
||||||
|
session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => {
|
||||||
|
if (responseHeaders) {
|
||||||
|
if (resourceType === "mainFrame")
|
||||||
|
patchCsp(responseHeaders);
|
||||||
|
|
||||||
|
// Fix hosts that don't properly set the css content type, such as
|
||||||
|
// raw.githubusercontent.com
|
||||||
|
if (resourceType === "stylesheet") {
|
||||||
|
const header = findHeader(responseHeaders, "content-type");
|
||||||
|
if (header)
|
||||||
|
responseHeaders[header] = ["text/css"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cb({ cancel: false, responseHeaders });
|
||||||
|
});
|
||||||
|
|
||||||
|
// assign a noop to onHeadersReceived to prevent other mods from adding their own incompatible ones.
|
||||||
|
// For instance, OpenAsar adds their own that doesn't fix content-type for stylesheets which makes it
|
||||||
|
// impossible to load css from github raw despite our fix above
|
||||||
|
session.defaultSession.webRequest.onHeadersReceived = () => { };
|
||||||
|
}
|
|
@ -16,9 +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 { app, protocol, session } from "electron";
|
import { app, net, protocol } from "electron";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
|
import { pathToFileURL } from "url";
|
||||||
|
|
||||||
|
import { initCsp } from "./csp";
|
||||||
import { ensureSafePath } from "./ipcMain";
|
import { ensureSafePath } from "./ipcMain";
|
||||||
import { RendererSettings } from "./settings";
|
import { RendererSettings } from "./settings";
|
||||||
import { IS_VANILLA, THEMES_DIR } from "./utils/constants";
|
import { IS_VANILLA, THEMES_DIR } from "./utils/constants";
|
||||||
|
@ -26,55 +28,71 @@ import { installExt } from "./utils/extensions";
|
||||||
|
|
||||||
if (!IS_VANILLA && !IS_EXTENSION) {
|
if (!IS_VANILLA && !IS_EXTENSION) {
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
// Source Maps! Maybe there's a better way but since the renderer is executed
|
protocol.handle("vencord", ({ url: unsafeUrl }) => {
|
||||||
// from a string I don't think any other form of sourcemaps would work
|
let url = decodeURI(unsafeUrl).slice("vencord://".length).replace(/\?v=\d+$/, "");
|
||||||
protocol.registerFileProtocol("vencord", ({ url: unsafeUrl }, cb) => {
|
|
||||||
let url = unsafeUrl.slice("vencord://".length);
|
|
||||||
if (url.endsWith("/")) url = url.slice(0, -1);
|
if (url.endsWith("/")) url = url.slice(0, -1);
|
||||||
|
|
||||||
if (url.startsWith("/themes/")) {
|
if (url.startsWith("/themes/")) {
|
||||||
const theme = url.slice("/themes/".length);
|
const theme = url.slice("/themes/".length);
|
||||||
|
|
||||||
const safeUrl = ensureSafePath(THEMES_DIR, theme);
|
const safeUrl = ensureSafePath(THEMES_DIR, theme);
|
||||||
if (!safeUrl) {
|
if (!safeUrl) {
|
||||||
cb({ statusCode: 403 });
|
return new Response(null, {
|
||||||
return;
|
status: 404
|
||||||
|
});
|
||||||
}
|
}
|
||||||
cb(safeUrl.replace(/\?v=\d+$/, ""));
|
|
||||||
return;
|
return net.fetch(pathToFileURL(safeUrl).toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Source Maps! Maybe there's a better way but since the renderer is executed
|
||||||
|
// from a string I don't think any other form of sourcemaps would work
|
||||||
|
|
||||||
switch (url) {
|
switch (url) {
|
||||||
case "renderer.js.map":
|
case "renderer.js.map":
|
||||||
case "preload.js.map":
|
case "preload.js.map":
|
||||||
case "patcher.js.map":
|
case "patcher.js.map":
|
||||||
case "main.js.map":
|
case "main.js.map":
|
||||||
cb(join(__dirname, url));
|
return net.fetch(pathToFileURL(join(__dirname, url)).toString());
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
cb({ statusCode: 403 });
|
return new Response(null, {
|
||||||
|
status: 404
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
protocol.registerFileProtocol("equicord", ({ url: unsafeUrl }, cb) => {
|
protocol.handle("equicord", ({ url: unsafeUrl }) => {
|
||||||
let url = unsafeUrl.slice("equicord://".length);
|
let url = decodeURI(unsafeUrl).slice("equicord://".length).replace(/\?v=\d+$/, "");
|
||||||
|
|
||||||
if (url.endsWith("/")) url = url.slice(0, -1);
|
if (url.endsWith("/")) url = url.slice(0, -1);
|
||||||
|
|
||||||
if (url.startsWith("/themes/")) {
|
if (url.startsWith("/themes/")) {
|
||||||
const theme = url.slice("/themes/".length);
|
const theme = url.slice("/themes/".length);
|
||||||
|
|
||||||
const safeUrl = ensureSafePath(THEMES_DIR, theme);
|
const safeUrl = ensureSafePath(THEMES_DIR, theme);
|
||||||
if (!safeUrl) {
|
if (!safeUrl) {
|
||||||
cb({ statusCode: 403 });
|
return new Response(null, {
|
||||||
return;
|
status: 404
|
||||||
|
});
|
||||||
}
|
}
|
||||||
cb(safeUrl.replace(/\?v=\d+$/, ""));
|
|
||||||
return;
|
return net.fetch(pathToFileURL(safeUrl).toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Source Maps! Maybe there's a better way but since the renderer is executed
|
||||||
|
// from a string I don't think any other form of sourcemaps would work
|
||||||
|
|
||||||
switch (url) {
|
switch (url) {
|
||||||
case "renderer.js.map":
|
case "renderer.js.map":
|
||||||
case "preload.js.map":
|
case "preload.js.map":
|
||||||
case "patcher.js.map":
|
case "patcher.js.map":
|
||||||
case "main.js.map":
|
case "main.js.map":
|
||||||
cb(join(__dirname, url));
|
return net.fetch(pathToFileURL(join(__dirname, url)).toString());
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
cb({ statusCode: 403 });
|
return new Response(null, {
|
||||||
|
status: 404
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -86,70 +104,7 @@ if (!IS_VANILLA && !IS_EXTENSION) {
|
||||||
} catch { }
|
} catch { }
|
||||||
|
|
||||||
|
|
||||||
const findHeader = (headers: Record<string, string[]>, headerName: Lowercase<string>) => {
|
initCsp();
|
||||||
return Object.keys(headers).find(h => h.toLowerCase() === headerName);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Remove CSP
|
|
||||||
type PolicyResult = Record<string, string[]>;
|
|
||||||
|
|
||||||
const parsePolicy = (policy: string): PolicyResult => {
|
|
||||||
const result: PolicyResult = {};
|
|
||||||
policy.split(";").forEach(directive => {
|
|
||||||
const [directiveKey, ...directiveValue] = directive.trim().split(/\s+/g);
|
|
||||||
if (directiveKey && !Object.prototype.hasOwnProperty.call(result, directiveKey)) {
|
|
||||||
result[directiveKey] = directiveValue;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
const stringifyPolicy = (policy: PolicyResult): string =>
|
|
||||||
Object.entries(policy)
|
|
||||||
.filter(([, values]) => values?.length)
|
|
||||||
.map(directive => directive.flat().join(" "))
|
|
||||||
.join("; ");
|
|
||||||
|
|
||||||
const patchCsp = (headers: Record<string, string[]>) => {
|
|
||||||
const header = findHeader(headers, "content-security-policy");
|
|
||||||
|
|
||||||
if (header) {
|
|
||||||
const csp = parsePolicy(headers[header][0]);
|
|
||||||
|
|
||||||
for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src", "worker-src"]) {
|
|
||||||
csp[directive] ??= [];
|
|
||||||
csp[directive].push("*", "blob:", "data:", "vencord:", "'unsafe-inline'");
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Restrict this to only imported packages with fixed version.
|
|
||||||
// Perhaps auto generate with esbuild
|
|
||||||
csp["script-src"] ??= [];
|
|
||||||
csp["script-src"].push("'unsafe-eval'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com");
|
|
||||||
headers[header] = [stringifyPolicy(csp)];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => {
|
|
||||||
if (responseHeaders) {
|
|
||||||
if (resourceType === "mainFrame")
|
|
||||||
patchCsp(responseHeaders);
|
|
||||||
|
|
||||||
// Fix hosts that don't properly set the css content type, such as
|
|
||||||
// raw.githubusercontent.com
|
|
||||||
if (resourceType === "stylesheet") {
|
|
||||||
const header = findHeader(responseHeaders, "content-type");
|
|
||||||
if (header)
|
|
||||||
responseHeaders[header] = ["text/css"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cb({ cancel: false, responseHeaders });
|
|
||||||
});
|
|
||||||
|
|
||||||
// assign a noop to onHeadersReceived to prevent other mods from adding their own incompatible ones.
|
|
||||||
// For instance, OpenAsar adds their own that doesn't fix content-type for stylesheets which makes it
|
|
||||||
// impossible to load css from github raw despite our fix above
|
|
||||||
session.defaultSession.webRequest.onHeadersReceived = () => { };
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,7 @@ import { makeLinksOpenExternally } from "./utils/externalLinks";
|
||||||
mkdirSync(THEMES_DIR, { recursive: true });
|
mkdirSync(THEMES_DIR, { recursive: true });
|
||||||
|
|
||||||
export function ensureSafePath(basePath: string, path: string) {
|
export function ensureSafePath(basePath: string, path: string) {
|
||||||
const normalizedBasePath = normalize(basePath);
|
const normalizedBasePath = normalize(basePath + "/");
|
||||||
const newPath = join(basePath, path);
|
const newPath = join(basePath, path);
|
||||||
const normalizedPath = normalize(newPath);
|
const normalizedPath = normalize(newPath);
|
||||||
return normalizedPath.startsWith(normalizedBasePath) ? normalizedPath : null;
|
return normalizedPath.startsWith(normalizedBasePath) ? normalizedPath : null;
|
||||||
|
|
|
@ -30,7 +30,10 @@ export function serializeErrors(func: (...args: any[]) => any) {
|
||||||
ok: false,
|
ok: false,
|
||||||
error: e instanceof Error ? {
|
error: e instanceof Error ? {
|
||||||
// prototypes get lost, so turn error into plain object
|
// prototypes get lost, so turn error into plain object
|
||||||
...e
|
...e,
|
||||||
|
message: e.message,
|
||||||
|
name: e.name,
|
||||||
|
stack: e.stack
|
||||||
} : e
|
} : 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 { get } from "@main/utils/simpleGet";
|
import { fetchBuffer, fetchJson } from "@main/utils/http";
|
||||||
import { IpcEvents } from "@shared/IpcEvents";
|
import { IpcEvents } from "@shared/IpcEvents";
|
||||||
import { VENCORD_USER_AGENT } from "@shared/vencordUserAgent";
|
import { VENCORD_USER_AGENT } from "@shared/vencordUserAgent";
|
||||||
import { ipcMain } from "electron";
|
import { ipcMain } from "electron";
|
||||||
|
@ -30,8 +30,8 @@ import { ASAR_FILE, serializeErrors } from "./common";
|
||||||
const API_BASE = `https://api.github.com/repos/${gitRemote}`;
|
const API_BASE = `https://api.github.com/repos/${gitRemote}`;
|
||||||
let PendingUpdate: string | null = null;
|
let PendingUpdate: string | null = null;
|
||||||
|
|
||||||
async function githubGet(endpoint: string) {
|
async function githubGet<T = any>(endpoint: string) {
|
||||||
return get(API_BASE + endpoint, {
|
return fetchJson<T>(API_BASE + endpoint, {
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "application/vnd.github+json",
|
Accept: "application/vnd.github+json",
|
||||||
// "All API requests MUST include a valid User-Agent header.
|
// "All API requests MUST include a valid User-Agent header.
|
||||||
|
@ -45,9 +45,8 @@ async function calculateGitChanges() {
|
||||||
const isOutdated = await fetchUpdates();
|
const isOutdated = await fetchUpdates();
|
||||||
if (!isOutdated) return [];
|
if (!isOutdated) return [];
|
||||||
|
|
||||||
const res = await githubGet(`/compare/${gitHash}...HEAD`);
|
const data = await githubGet(`/compare/${gitHash}...HEAD`);
|
||||||
|
|
||||||
const data = JSON.parse(res.toString("utf-8"));
|
|
||||||
return data.commits.map((c: any) => ({
|
return data.commits.map((c: any) => ({
|
||||||
// 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),
|
||||||
|
@ -57,9 +56,8 @@ async function calculateGitChanges() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchUpdates() {
|
async function fetchUpdates() {
|
||||||
const release = await githubGet("/releases/latest");
|
const data = await githubGet("/releases/latest");
|
||||||
|
|
||||||
const data = JSON.parse(release.toString());
|
|
||||||
const hash = data.name.slice(data.name.lastIndexOf(" ") + 1);
|
const hash = data.name.slice(data.name.lastIndexOf(" ") + 1);
|
||||||
if (hash === gitHash)
|
if (hash === gitHash)
|
||||||
return false;
|
return false;
|
||||||
|
@ -74,7 +72,7 @@ async function fetchUpdates() {
|
||||||
async function applyUpdates() {
|
async function applyUpdates() {
|
||||||
if (!PendingUpdate) return true;
|
if (!PendingUpdate) return true;
|
||||||
|
|
||||||
const data = await get(PendingUpdate);
|
const data = await fetchBuffer(PendingUpdate);
|
||||||
originalWriteFileSync(__dirname, data);
|
originalWriteFileSync(__dirname, data);
|
||||||
|
|
||||||
PendingUpdate = null;
|
PendingUpdate = null;
|
||||||
|
|
|
@ -24,7 +24,7 @@ import { join } from "path";
|
||||||
|
|
||||||
import { DATA_DIR } from "./constants";
|
import { DATA_DIR } from "./constants";
|
||||||
import { crxToZip } from "./crxToZip";
|
import { crxToZip } from "./crxToZip";
|
||||||
import { get } from "./simpleGet";
|
import { fetchBuffer } from "./http";
|
||||||
|
|
||||||
const extensionCacheDir = join(DATA_DIR, "ExtensionCache");
|
const extensionCacheDir = join(DATA_DIR, "ExtensionCache");
|
||||||
|
|
||||||
|
@ -69,13 +69,14 @@ export async function installExt(id: string) {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const url = `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${id}%26uc&prodversion=${process.versions.chrome}`;
|
const url = `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${id}%26uc&prodversion=${process.versions.chrome}`;
|
||||||
|
|
||||||
const buf = await get(url, {
|
const buf = await fetchBuffer(url, {
|
||||||
headers: {
|
headers: {
|
||||||
"User-Agent": `Electron ${process.versions.electron} ~ Equicord (https://github.com/Equicord/Equicord)`
|
"User-Agent": `Electron ${process.versions.electron} ~ Equicord (https://github.com/Equicord/Equicord)`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await extract(crxToZip(buf), extDir).catch(console.error);
|
await extract(crxToZip(buf), extDir)
|
||||||
|
.catch(err => console.error(`Failed to extract extension ${id}`, err));
|
||||||
}
|
}
|
||||||
|
|
||||||
session.defaultSession.loadExtension(extDir);
|
session.defaultSession.loadExtension(extDir);
|
||||||
|
|
70
src/main/utils/http.ts
Normal file
70
src/main/utils/http.ts
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
/*
|
||||||
|
* 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 { createWriteStream } from "original-fs";
|
||||||
|
import { Readable } from "stream";
|
||||||
|
import { finished } from "stream/promises";
|
||||||
|
|
||||||
|
type Url = string | URL;
|
||||||
|
|
||||||
|
export async function checkedFetch(url: Url, options?: RequestInit) {
|
||||||
|
try {
|
||||||
|
var res = await fetch(url, options);
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error && err.cause) {
|
||||||
|
err = err.cause;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`${options?.method ?? "GET"} ${url} failed: ${err}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
let message = `${options?.method ?? "GET"} ${url}: ${res.status} ${res.statusText}`;
|
||||||
|
try {
|
||||||
|
const reason = await res.text();
|
||||||
|
message += `\n${reason}`;
|
||||||
|
} catch { }
|
||||||
|
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchJson<T = any>(url: Url, options?: RequestInit) {
|
||||||
|
const res = await checkedFetch(url, options);
|
||||||
|
return res.json() as Promise<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchBuffer(url: Url, options?: RequestInit) {
|
||||||
|
const res = await checkedFetch(url, options);
|
||||||
|
const buf = await res.arrayBuffer();
|
||||||
|
|
||||||
|
return Buffer.from(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downloadToFile(url: Url, path: string, options?: RequestInit) {
|
||||||
|
const res = await checkedFetch(url, options);
|
||||||
|
if (!res.body) {
|
||||||
|
throw new Error(`Download ${url}: response body is empty`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error weird type conflict
|
||||||
|
const body = Readable.fromWeb(res.body);
|
||||||
|
await finished(body.pipe(createWriteStream(path)));
|
||||||
|
}
|
|
@ -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 https from "https";
|
|
||||||
|
|
||||||
export function get(url: string, options: https.RequestOptions = {}) {
|
|
||||||
return new Promise<Buffer>((resolve, reject) => {
|
|
||||||
https.get(url, options, res => {
|
|
||||||
const { statusCode, statusMessage, headers } = res;
|
|
||||||
if (statusCode! >= 400)
|
|
||||||
return void reject(`${statusCode}: ${statusMessage} - ${url}`);
|
|
||||||
if (statusCode! >= 300)
|
|
||||||
return void resolve(get(headers.location!, options));
|
|
||||||
|
|
||||||
const chunks = [] as Buffer[];
|
|
||||||
res.on("error", reject);
|
|
||||||
|
|
||||||
res.on("data", chunk => chunks.push(chunk));
|
|
||||||
res.once("end", () => resolve(Buffer.concat(chunks)));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -31,7 +31,7 @@ import { User } from "discord-types/general";
|
||||||
|
|
||||||
import { EquicordDonorModal, VencordDonorModal } from "./modals";
|
import { EquicordDonorModal, VencordDonorModal } from "./modals";
|
||||||
|
|
||||||
const CONTRIBUTOR_BADGE = "https://vencord.dev/assets/favicon.png";
|
const CONTRIBUTOR_BADGE = "https://cdn.discordapp.com/emojis/1092089799109775453.png?size=64";
|
||||||
const EQUICORD_CONTRIBUTOR_BADGE = "https://i.imgur.com/57ATLZu.png";
|
const EQUICORD_CONTRIBUTOR_BADGE = "https://i.imgur.com/57ATLZu.png";
|
||||||
const EQUICORD_DONOR_BADGE = "https://cdn.nest.rip/uploads/78cb1e77-b7a6-4242-9089-e91f866159bf.png";
|
const EQUICORD_DONOR_BADGE = "https://cdn.nest.rip/uploads/78cb1e77-b7a6-4242-9089-e91f866159bf.png";
|
||||||
|
|
||||||
|
|
|
@ -79,11 +79,17 @@ export default definePlugin({
|
||||||
|
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: 'type:"UPLOAD_START"',
|
find: "async uploadFiles(",
|
||||||
replacement: {
|
replacement: [
|
||||||
match: /await \i\.uploadFiles\((\i),/,
|
{
|
||||||
replace: "$1.forEach($self.anonymise),$&"
|
match: /async uploadFiles\((\i),\i\){/,
|
||||||
},
|
replace: "$&$1.forEach($self.anonymise);"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: /async uploadFilesSimple\((\i)\){/,
|
||||||
|
replace: "$&$1.forEach($self.anonymise);"
|
||||||
|
}
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: "#{intl::ATTACHMENT_UTILITIES_SPOILER}",
|
find: "#{intl::ATTACHMENT_UTILITIES_SPOILER}",
|
||||||
|
|
|
@ -20,7 +20,6 @@ import "./style.css";
|
||||||
|
|
||||||
import { definePluginSettings } from "@api/Settings";
|
import { definePluginSettings } from "@api/Settings";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { getIntlMessage } from "@utils/discord";
|
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { findByPropsLazy, findLazy, findStoreLazy } from "@webpack";
|
import { findByPropsLazy, findLazy, findStoreLazy } from "@webpack";
|
||||||
import { FluxDispatcher } from "@webpack/common";
|
import { FluxDispatcher } from "@webpack/common";
|
||||||
|
@ -350,7 +349,7 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return child?.props?.["aria-label"] === getIntlMessage("SERVERS");
|
return child?.props?.renderTreeNode !== null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
return true;
|
return true;
|
||||||
|
@ -390,14 +389,6 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
makeNewButtonFilter(isBetterFolders: boolean) {
|
|
||||||
return child => {
|
|
||||||
if (!isBetterFolders) return true;
|
|
||||||
|
|
||||||
return !child?.props?.barClassName;
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
shouldShowTransition(props: any) {
|
shouldShowTransition(props: any) {
|
||||||
// Pending guilds
|
// Pending guilds
|
||||||
if (props?.folderNode?.id === 1) return true;
|
if (props?.folderNode?.id === 1) return true;
|
||||||
|
|
|
@ -139,5 +139,5 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
DecorSection: ErrorBoundary.wrap(DecorSection)
|
DecorSection: ErrorBoundary.wrap(DecorSection, { noop: true })
|
||||||
});
|
});
|
||||||
|
|
|
@ -29,7 +29,7 @@ export let socket: WebSocket | undefined;
|
||||||
export function initWs(isManual = false) {
|
export function initWs(isManual = false) {
|
||||||
let wasConnected = isManual;
|
let wasConnected = isManual;
|
||||||
let hasErrored = false;
|
let hasErrored = false;
|
||||||
const ws = socket = new WebSocket(`ws://localhost:${PORT}`);
|
const ws = socket = new WebSocket(`ws://127.0.0.1:${PORT}`);
|
||||||
|
|
||||||
function replyData(data: OutgoingMessage) {
|
function replyData(data: OutgoingMessage) {
|
||||||
ws.send(JSON.stringify(data));
|
ws.send(JSON.stringify(data));
|
||||||
|
|
|
@ -118,7 +118,7 @@ export default definePlugin({
|
||||||
renderSearchBar(instance: Instance, SearchBarComponent: TSearchBarComponent) {
|
renderSearchBar(instance: Instance, SearchBarComponent: TSearchBarComponent) {
|
||||||
this.instance = instance;
|
this.instance = instance;
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary noop={true}>
|
<ErrorBoundary noop>
|
||||||
<SearchBar instance={instance} SearchBarComponent={SearchBarComponent} />
|
<SearchBar instance={instance} SearchBarComponent={SearchBarComponent} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
|
|
|
@ -98,7 +98,7 @@ export default definePlugin({
|
||||||
src={`${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/role-icons/${roleId}/${role.icon}.webp?size=24&quality=lossless`}
|
src={`${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/role-icons/${roleId}/${role.icon}.webp?size=24&quality=lossless`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}),
|
}, { noop: true }),
|
||||||
});
|
});
|
||||||
|
|
||||||
function getUsernameString(username: string) {
|
function getUsernameString(username: string) {
|
||||||
|
|
|
@ -20,7 +20,8 @@ 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 } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
import { FluxDispatcher, PermissionsBits, PermissionStore, UserStore } from "@webpack/common";
|
import { FluxDispatcher, PermissionsBits, PermissionStore, UserStore, WindowStore } from "@webpack/common";
|
||||||
|
import NoReplyMentionPlugin from "plugins/noReplyMention";
|
||||||
|
|
||||||
const MessageActions = findByPropsLazy("deleteMessage", "startEditMessage");
|
const MessageActions = findByPropsLazy("deleteMessage", "startEditMessage");
|
||||||
const EditStore = findByPropsLazy("isEditing", "isEditingAny");
|
const EditStore = findByPropsLazy("isEditing", "isEditingAny");
|
||||||
|
@ -28,6 +29,7 @@ const EditStore = findByPropsLazy("isEditing", "isEditingAny");
|
||||||
let isDeletePressed = false;
|
let isDeletePressed = false;
|
||||||
const keydown = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = true);
|
const keydown = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = true);
|
||||||
const keyup = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = false);
|
const keyup = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = false);
|
||||||
|
const focusChanged = () => !WindowStore.isFocused() && (isDeletePressed = false);
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
const settings = definePluginSettings({
|
||||||
enableDeleteOnClick: {
|
enableDeleteOnClick: {
|
||||||
|
@ -62,11 +64,13 @@ export default definePlugin({
|
||||||
start() {
|
start() {
|
||||||
document.addEventListener("keydown", keydown);
|
document.addEventListener("keydown", keydown);
|
||||||
document.addEventListener("keyup", keyup);
|
document.addEventListener("keyup", keyup);
|
||||||
|
WindowStore.addChangeListener(focusChanged);
|
||||||
},
|
},
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
document.removeEventListener("keydown", keydown);
|
document.removeEventListener("keydown", keydown);
|
||||||
document.removeEventListener("keyup", keyup);
|
document.removeEventListener("keyup", keyup);
|
||||||
|
WindowStore.removeChangeListener(focusChanged);
|
||||||
},
|
},
|
||||||
|
|
||||||
onMessageClick(msg: any, channel, event) {
|
onMessageClick(msg: any, channel, event) {
|
||||||
|
@ -89,9 +93,8 @@ export default definePlugin({
|
||||||
if (msg.hasFlag(EPHEMERAL)) return;
|
if (msg.hasFlag(EPHEMERAL)) return;
|
||||||
|
|
||||||
const isShiftPress = event.shiftKey && !settings.store.requireModifier;
|
const isShiftPress = event.shiftKey && !settings.store.requireModifier;
|
||||||
const NoReplyMention = Vencord.Plugins.plugins.NoReplyMention as any as typeof import("../noReplyMention").default;
|
const shouldMention = Vencord.Plugins.isPluginEnabled(NoReplyMentionPlugin.name)
|
||||||
const shouldMention = Vencord.Plugins.isPluginEnabled("NoReplyMention")
|
? NoReplyMentionPlugin.shouldMention(msg, isShiftPress)
|
||||||
? NoReplyMention.shouldMention(msg, isShiftPress)
|
|
||||||
: !isShiftPress;
|
: !isShiftPress;
|
||||||
|
|
||||||
FluxDispatcher.dispatch({
|
FluxDispatcher.dispatch({
|
||||||
|
|
|
@ -20,7 +20,6 @@ import { addMessageAccessory, removeMessageAccessory } from "@api/MessageAccesso
|
||||||
import { updateMessage } from "@api/MessageUpdater";
|
import { updateMessage } from "@api/MessageUpdater";
|
||||||
import { definePluginSettings } from "@api/Settings";
|
import { definePluginSettings } from "@api/Settings";
|
||||||
import { getUserSettingLazy } from "@api/UserSettings";
|
import { getUserSettingLazy } from "@api/UserSettings";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
|
||||||
import { Devs } from "@utils/constants.js";
|
import { Devs } from "@utils/constants.js";
|
||||||
import { classes } from "@utils/misc";
|
import { classes } from "@utils/misc";
|
||||||
import { Queue } from "@utils/Queue";
|
import { Queue } from "@utils/Queue";
|
||||||
|
@ -373,7 +372,7 @@ export default definePlugin({
|
||||||
settings,
|
settings,
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
addMessageAccessory("messageLinkEmbed", props => {
|
addMessageAccessory("MessageLinkEmbeds", props => {
|
||||||
if (!messageLinkRegex.test(props.message.content))
|
if (!messageLinkRegex.test(props.message.content))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
@ -381,15 +380,13 @@ export default definePlugin({
|
||||||
messageLinkRegex.lastIndex = 0;
|
messageLinkRegex.lastIndex = 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<MessageEmbedAccessory
|
||||||
<MessageEmbedAccessory
|
message={props.message}
|
||||||
message={props.message}
|
/>
|
||||||
/>
|
|
||||||
</ErrorBoundary>
|
|
||||||
);
|
);
|
||||||
}, 4 /* just above rich embeds */);
|
}, 4 /* just above rich embeds */);
|
||||||
},
|
},
|
||||||
stop() {
|
stop() {
|
||||||
removeMessageAccessory("messageLinkEmbed");
|
removeMessageAccessory("MessageLinkEmbeds");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -121,14 +121,9 @@ export default definePlugin({
|
||||||
},
|
},
|
||||||
// Make the gap between each item smaller so our tab can fit.
|
// Make the gap between each item smaller so our tab can fit.
|
||||||
{
|
{
|
||||||
match: /className:\i\.tabBar/,
|
match: /type:"top",/,
|
||||||
replace: '$& + " vc-mutual-gdms-modal-v2-tab-bar"'
|
replace: '$&className:"vc-mutual-gdms-modal-v2-tab-bar",'
|
||||||
},
|
},
|
||||||
// Make the tab bar item text smaller so our tab can fit.
|
|
||||||
{
|
|
||||||
match: /(\.tabBarItem.+?variant:)"heading-md\/normal"/,
|
|
||||||
replace: '$1"heading-sm/normal"'
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -209,5 +204,5 @@ export default definePlugin({
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
})
|
}, { noop: true })
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,5 +3,5 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-mutual-gdms-modal-v2-tab-bar {
|
.vc-mutual-gdms-modal-v2-tab-bar {
|
||||||
gap: 12px;
|
--space-xl: 16px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,5 +75,5 @@ export default definePlugin({
|
||||||
}}> Pause Indefinitely.</a>}
|
}}> Pause Indefinitely.</a>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})
|
}, { noop: true })
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,19 +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 { definePluginSettings, Settings } from "@api/Settings";
|
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 } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
import { ChannelStore, ComponentDispatch, FluxDispatcher as Dispatcher, MessageStore, PermissionsBits, PermissionStore, SelectedChannelStore, UserStore } from "@webpack/common";
|
import { ChannelStore, ComponentDispatch, FluxDispatcher as Dispatcher, MessageActions, MessageStore, PermissionsBits, PermissionStore, SelectedChannelStore, UserStore } from "@webpack/common";
|
||||||
import { Message } from "discord-types/general";
|
import { Message } from "discord-types/general";
|
||||||
|
import NoBlockedMessagesPlugin from "plugins/noBlockedMessages";
|
||||||
|
import NoReplyMentionPlugin from "plugins/noReplyMention";
|
||||||
|
|
||||||
const Kangaroo = findByPropsLazy("jumpToMessage");
|
|
||||||
const RelationshipStore = findByPropsLazy("getRelationships", "isBlocked");
|
const RelationshipStore = findByPropsLazy("getRelationships", "isBlocked");
|
||||||
|
|
||||||
const isMac = navigator.platform.includes("Mac"); // bruh
|
const isMac = navigator.platform.includes("Mac"); // bruh
|
||||||
let replyIdx = -1;
|
let currentlyReplyingId: string | null = null;
|
||||||
let editIdx = -1;
|
let currentlyEditingId: string | null = null;
|
||||||
|
|
||||||
|
|
||||||
const enum MentionOptions {
|
const enum MentionOptions {
|
||||||
|
@ -69,36 +70,29 @@ export default definePlugin({
|
||||||
|
|
||||||
flux: {
|
flux: {
|
||||||
DELETE_PENDING_REPLY() {
|
DELETE_PENDING_REPLY() {
|
||||||
replyIdx = -1;
|
currentlyReplyingId = null;
|
||||||
},
|
},
|
||||||
MESSAGE_END_EDIT() {
|
MESSAGE_END_EDIT() {
|
||||||
editIdx = -1;
|
currentlyEditingId = null;
|
||||||
|
},
|
||||||
|
CHANNEL_SELECT() {
|
||||||
|
currentlyReplyingId = null;
|
||||||
|
currentlyEditingId = null;
|
||||||
},
|
},
|
||||||
MESSAGE_START_EDIT: onStartEdit,
|
MESSAGE_START_EDIT: onStartEdit,
|
||||||
CREATE_PENDING_REPLY: onCreatePendingReply
|
CREATE_PENDING_REPLY: onCreatePendingReply
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function calculateIdx(messages: Message[], id: string) {
|
function onStartEdit({ messageId, _isQuickEdit }: any) {
|
||||||
const idx = messages.findIndex(m => m.id === id);
|
|
||||||
return idx === -1
|
|
||||||
? idx
|
|
||||||
: messages.length - idx - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onStartEdit({ channelId, messageId, _isQuickEdit }: any) {
|
|
||||||
if (_isQuickEdit) return;
|
if (_isQuickEdit) return;
|
||||||
|
currentlyEditingId = messageId;
|
||||||
const meId = UserStore.getCurrentUser().id;
|
|
||||||
|
|
||||||
const messages = MessageStore.getMessages(channelId)._array.filter(m => m.author.id === meId);
|
|
||||||
editIdx = calculateIdx(messages, messageId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCreatePendingReply({ message, _isQuickReply }: { message: Message; _isQuickReply: boolean; }) {
|
function onCreatePendingReply({ message, _isQuickReply }: { message: Message; _isQuickReply: boolean; }) {
|
||||||
if (_isQuickReply) return;
|
if (_isQuickReply) return;
|
||||||
|
|
||||||
replyIdx = calculateIdx(MessageStore.getMessages(message.channel_id)._array, message.id);
|
currentlyReplyingId = message.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isCtrl = (e: KeyboardEvent) => isMac ? e.metaKey : e.ctrlKey;
|
const isCtrl = (e: KeyboardEvent) => isMac ? e.metaKey : e.ctrlKey;
|
||||||
|
@ -123,10 +117,10 @@ function jumpIfOffScreen(channelId: string, messageId: string) {
|
||||||
|
|
||||||
const vh = Math.max(document.documentElement.clientHeight, window.innerHeight);
|
const vh = Math.max(document.documentElement.clientHeight, window.innerHeight);
|
||||||
const rect = element.getBoundingClientRect();
|
const rect = element.getBoundingClientRect();
|
||||||
const isOffscreen = rect.bottom < 200 || rect.top - vh >= -200;
|
const isOffscreen = rect.bottom < 150 || rect.top - vh >= -150;
|
||||||
|
|
||||||
if (isOffscreen) {
|
if (isOffscreen) {
|
||||||
Kangaroo.jumpToMessage({
|
MessageActions.jumpToMessage({
|
||||||
channelId,
|
channelId,
|
||||||
messageId,
|
messageId,
|
||||||
flash: false,
|
flash: false,
|
||||||
|
@ -137,44 +131,48 @@ function jumpIfOffScreen(channelId: string, messageId: string) {
|
||||||
|
|
||||||
function getNextMessage(isUp: boolean, isReply: boolean) {
|
function getNextMessage(isUp: boolean, isReply: boolean) {
|
||||||
let messages: Array<Message & { deleted?: boolean; }> = MessageStore.getMessages(SelectedChannelStore.getChannelId())._array;
|
let messages: Array<Message & { deleted?: boolean; }> = MessageStore.getMessages(SelectedChannelStore.getChannelId())._array;
|
||||||
if (!isReply) {
|
|
||||||
// we are editing so only include own
|
|
||||||
const meId = UserStore.getCurrentUser().id;
|
|
||||||
messages = messages.filter(m => m.author.id === meId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Vencord.Plugins.isPluginEnabled("NoBlockedMessages")) {
|
const meId = UserStore.getCurrentUser().id;
|
||||||
messages = messages.filter(m => !RelationshipStore.isBlocked(m.author.id));
|
const hasNoBlockedMessages = Vencord.Plugins.isPluginEnabled(NoBlockedMessagesPlugin.name);
|
||||||
}
|
|
||||||
|
|
||||||
const mutate = (i: number) => isUp
|
messages = messages.filter(m => {
|
||||||
? Math.min(messages.length - 1, i + 1)
|
if (m.deleted) return false;
|
||||||
: Math.max(-1, i - 1);
|
if (!isReply && m.author.id !== meId) return false; // editing only own messages
|
||||||
|
if (hasNoBlockedMessages && NoBlockedMessagesPlugin.shouldIgnoreMessage(m)) return false;
|
||||||
|
|
||||||
const findNextNonDeleted = (i: number) => {
|
return true;
|
||||||
do {
|
});
|
||||||
i = mutate(i);
|
|
||||||
} while (i !== -1 && messages[messages.length - i - 1]?.deleted === true);
|
const findNextNonDeleted = (id: string | null) => {
|
||||||
return i;
|
if (id === null) return messages[messages.length - 1];
|
||||||
|
|
||||||
|
const idx = messages.findIndex(m => m.id === id);
|
||||||
|
if (idx === -1) return messages[messages.length - 1];
|
||||||
|
|
||||||
|
const i = isUp ? idx - 1 : idx + 1;
|
||||||
|
return messages[i] ?? null;
|
||||||
};
|
};
|
||||||
|
|
||||||
let i: number;
|
if (isReply) {
|
||||||
if (isReply)
|
const msg = findNextNonDeleted(currentlyReplyingId);
|
||||||
replyIdx = i = findNextNonDeleted(replyIdx);
|
currentlyReplyingId = msg?.id ?? null;
|
||||||
else
|
return msg;
|
||||||
editIdx = i = findNextNonDeleted(editIdx);
|
} else {
|
||||||
|
const msg = findNextNonDeleted(currentlyEditingId);
|
||||||
return i === - 1 ? undefined : messages[messages.length - i - 1];
|
currentlyEditingId = msg?.id ?? null;
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldMention(message) {
|
function shouldMention(message: Message) {
|
||||||
const { enabled, userList, shouldPingListed } = Settings.plugins.NoReplyMention;
|
|
||||||
const shouldPing = !enabled || (shouldPingListed === userList.includes(message.author.id));
|
|
||||||
|
|
||||||
switch (settings.store.shouldMention) {
|
switch (settings.store.shouldMention) {
|
||||||
case MentionOptions.NO_REPLY_MENTION_PLUGIN: return shouldPing;
|
case MentionOptions.NO_REPLY_MENTION_PLUGIN:
|
||||||
case MentionOptions.DISABLED: return false;
|
if (!Vencord.Plugins.isPluginEnabled(NoReplyMentionPlugin.name)) return true;
|
||||||
default: return true;
|
return NoReplyMentionPlugin.shouldMention(message, false);
|
||||||
|
case MentionOptions.DISABLED:
|
||||||
|
return false;
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -182,13 +180,16 @@ function shouldMention(message) {
|
||||||
function nextReply(isUp: boolean) {
|
function nextReply(isUp: boolean) {
|
||||||
const currChannel = ChannelStore.getChannel(SelectedChannelStore.getChannelId());
|
const currChannel = ChannelStore.getChannel(SelectedChannelStore.getChannelId());
|
||||||
if (currChannel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, currChannel)) return;
|
if (currChannel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, currChannel)) return;
|
||||||
|
|
||||||
const message = getNextMessage(isUp, true);
|
const message = getNextMessage(isUp, true);
|
||||||
|
|
||||||
if (!message)
|
if (!message) {
|
||||||
return void Dispatcher.dispatch({
|
return void Dispatcher.dispatch({
|
||||||
type: "DELETE_PENDING_REPLY",
|
type: "DELETE_PENDING_REPLY",
|
||||||
channelId: SelectedChannelStore.getChannelId(),
|
channelId: SelectedChannelStore.getChannelId(),
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const channel = ChannelStore.getChannel(message.channel_id);
|
const channel = ChannelStore.getChannel(message.channel_id);
|
||||||
const meId = UserStore.getCurrentUser().id;
|
const meId = UserStore.getCurrentUser().id;
|
||||||
|
|
||||||
|
@ -200,6 +201,7 @@ function nextReply(isUp: boolean) {
|
||||||
showMentionToggle: !channel.isPrivate() && message.author.id !== meId,
|
showMentionToggle: !channel.isPrivate() && message.author.id !== meId,
|
||||||
_isQuickReply: true
|
_isQuickReply: true
|
||||||
});
|
});
|
||||||
|
|
||||||
ComponentDispatch.dispatchToLastSubscribed("TEXTAREA_FOCUS");
|
ComponentDispatch.dispatchToLastSubscribed("TEXTAREA_FOCUS");
|
||||||
jumpIfOffScreen(channel.id, message.id);
|
jumpIfOffScreen(channel.id, message.id);
|
||||||
}
|
}
|
||||||
|
@ -210,11 +212,13 @@ function nextEdit(isUp: boolean) {
|
||||||
if (currChannel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, currChannel)) return;
|
if (currChannel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, currChannel)) return;
|
||||||
const message = getNextMessage(isUp, false);
|
const message = getNextMessage(isUp, false);
|
||||||
|
|
||||||
if (!message)
|
if (!message) {
|
||||||
return Dispatcher.dispatch({
|
return Dispatcher.dispatch({
|
||||||
type: "MESSAGE_END_EDIT",
|
type: "MESSAGE_END_EDIT",
|
||||||
channelId: SelectedChannelStore.getChannelId()
|
channelId: SelectedChannelStore.getChannelId()
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Dispatcher.dispatch({
|
Dispatcher.dispatch({
|
||||||
type: "MESSAGE_START_EDIT",
|
type: "MESSAGE_START_EDIT",
|
||||||
channelId: message.channel_id,
|
channelId: message.channel_id,
|
||||||
|
@ -222,5 +226,6 @@ function nextEdit(isUp: boolean) {
|
||||||
content: message.content,
|
content: message.content,
|
||||||
_isQuickEdit: true
|
_isQuickEdit: true
|
||||||
});
|
});
|
||||||
|
|
||||||
jumpIfOffScreen(message.channel_id, message.id);
|
jumpIfOffScreen(message.channel_id, message.id);
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,14 +53,12 @@ function makeSearchItem(src: string) {
|
||||||
<Flex style={{ alignItems: "center", gap: "0.5em" }}>
|
<Flex style={{ alignItems: "center", gap: "0.5em" }}>
|
||||||
<img
|
<img
|
||||||
style={{
|
style={{
|
||||||
borderRadius: i >= 3 // Do not round Google, Yandex & SauceNAO
|
borderRadius: "50%",
|
||||||
? "50%"
|
|
||||||
: void 0
|
|
||||||
}}
|
}}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
height={16}
|
height={16}
|
||||||
width={16}
|
width={16}
|
||||||
src={new URL("/favicon.ico", Engines[engine]).toString().replace("lens.", "")}
|
src={`https://icons.duckduckgo.com/ip3/${new URL(Engines[engine]).host}.ico`}
|
||||||
/>
|
/>
|
||||||
{engine}
|
{engine}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
|
@ -530,6 +530,7 @@ export default definePlugin({
|
||||||
|
|
||||||
if (channel.channelId != null) channel = ChannelStore.getChannel(channel.channelId);
|
if (channel.channelId != null) channel = ChannelStore.getChannel(channel.channelId);
|
||||||
if (channel == null || channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM()) return false;
|
if (channel == null || channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM()) return false;
|
||||||
|
if (["browse", "customize", "guide"].includes(channel.id)) return false;
|
||||||
|
|
||||||
return !PermissionStore.can(PermissionsBits.VIEW_CHANNEL, channel) || checkConnect && !PermissionStore.can(PermissionsBits.CONNECT, channel);
|
return !PermissionStore.can(PermissionsBits.VIEW_CHANNEL, channel) || checkConnect && !PermissionStore.can(PermissionsBits.CONNECT, channel);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -86,5 +86,5 @@ export default definePlugin({
|
||||||
</TooltipContainer>
|
</TooltipContainer>
|
||||||
)}
|
)}
|
||||||
</div>;
|
</div>;
|
||||||
})
|
}, { noop: true })
|
||||||
});
|
});
|
||||||
|
|
|
@ -111,7 +111,7 @@ export default definePlugin({
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Changes the indicator to keep the user object when creating the list of typing users
|
// Changes the indicator to keep the user object when creating the list of typing users
|
||||||
match: /\.map\((\i)=>\i\.\i\.getName\(\i,\i\.id,\1\)\)/,
|
match: /\.map\((\i)=>\i\.\i\.getName\(\i(?:\.guild_id)?,\i\.id,\1\)\)/,
|
||||||
replace: ""
|
replace: ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -132,10 +132,16 @@ export default definePlugin({
|
||||||
|
|
||||||
{
|
{
|
||||||
find: "Copy image not supported",
|
find: "Copy image not supported",
|
||||||
replacement: {
|
replacement: [
|
||||||
match: /(?<=(?:canSaveImage|canCopyImage)\((\i,\i)?\)\{.{0,150})!\i\.isPlatformEmbedded/g,
|
{
|
||||||
replace: "false"
|
match: /(?<=(?:canSaveImage|canCopyImage)\(.{0,120}?)!\i\.isPlatformEmbedded/g,
|
||||||
}
|
replace: "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: /canCopyImage\(.+?(?=return"function"==typeof \i\.clipboard\.copyImage)/,
|
||||||
|
replace: "$&return true;"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
// Add back Copy & Save Image
|
// Add back Copy & Save Image
|
||||||
{
|
{
|
||||||
|
@ -147,7 +153,7 @@ export default definePlugin({
|
||||||
replace: "false"
|
replace: "false"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /return\s*?\[.{0,50}?(?=\?.{0,25}?id:"copy-image")/,
|
match: /return\s*?\[.{0,50}?(?=\?\(0,\i\.jsxs?.{0,100}?id:"copy-image")/,
|
||||||
replace: "return [true"
|
replace: "return [true"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -25,8 +25,8 @@ export default definePlugin({
|
||||||
replace: ";b=AS:800000;level-asymmetry-allowed=1"
|
replace: ";b=AS:800000;level-asymmetry-allowed=1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: "useinbandfec=1",
|
match: /;usedtx=".concat\((\i)\?"0":"1"\)/,
|
||||||
replace: "useinbandfec=1;stereo=1;sprop-stereo=1"
|
replace: '$&.concat($1?";stereo=1;sprop-stereo=1":"")'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,7 +57,7 @@ export interface Dev {
|
||||||
*/
|
*/
|
||||||
export const Devs = /* #__PURE__*/ Object.freeze({
|
export const Devs = /* #__PURE__*/ Object.freeze({
|
||||||
Ven: {
|
Ven: {
|
||||||
name: "Vee",
|
name: "V",
|
||||||
id: 343383572805058560n
|
id: 343383572805058560n
|
||||||
},
|
},
|
||||||
Arjix: {
|
Arjix: {
|
||||||
|
@ -211,7 +211,7 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
||||||
},
|
},
|
||||||
axyie: {
|
axyie: {
|
||||||
name: "'ax",
|
name: "'ax",
|
||||||
id: 273562710745284628n
|
id: 929877747151548487n,
|
||||||
},
|
},
|
||||||
pointy: {
|
pointy: {
|
||||||
name: "pointy",
|
name: "pointy",
|
||||||
|
@ -604,7 +604,11 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
||||||
},
|
},
|
||||||
samsam: {
|
samsam: {
|
||||||
name: "samsam",
|
name: "samsam",
|
||||||
id: 836452332387565589n,
|
id: 400482410279469056n,
|
||||||
|
},
|
||||||
|
Cootshk: {
|
||||||
|
name: "Cootshk",
|
||||||
|
id: 921605971577548820n
|
||||||
},
|
},
|
||||||
} satisfies Record<string, Dev>);
|
} satisfies Record<string, Dev>);
|
||||||
|
|
||||||
|
@ -1078,6 +1082,10 @@ export const EquicordDevs = Object.freeze({
|
||||||
name: "bbgaming25k",
|
name: "bbgaming25k",
|
||||||
id: 851222385528274964n,
|
id: 851222385528274964n,
|
||||||
},
|
},
|
||||||
|
davidkra230: {
|
||||||
|
name: "davidkra230",
|
||||||
|
id: 652699312631054356n,
|
||||||
|
},
|
||||||
GroupXyz: {
|
GroupXyz: {
|
||||||
name: "GroupXyz",
|
name: "GroupXyz",
|
||||||
id: 950033410229944331n
|
id: 950033410229944331n
|
||||||
|
|
34
src/utils/cspViolations.ts
Normal file
34
src/utils/cspViolations.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2025 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useLayoutEffect } from "@webpack/common";
|
||||||
|
|
||||||
|
import { useForceUpdater } from "./react";
|
||||||
|
|
||||||
|
const cssRelevantDirectives = ["style-src", "img-src", "font-src"] as const;
|
||||||
|
|
||||||
|
export const CspBlockedUrls = new Set<string>();
|
||||||
|
const CspErrorListeners = new Set<() => void>();
|
||||||
|
|
||||||
|
document.addEventListener("securitypolicyviolation", ({ effectiveDirective, blockedURI }) => {
|
||||||
|
if (!blockedURI || !cssRelevantDirectives.includes(effectiveDirective as any)) return;
|
||||||
|
|
||||||
|
CspBlockedUrls.add(blockedURI);
|
||||||
|
|
||||||
|
CspErrorListeners.forEach(listener => listener());
|
||||||
|
});
|
||||||
|
|
||||||
|
export function useCspErrors() {
|
||||||
|
const forceUpdate = useForceUpdater();
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
CspErrorListeners.add(forceUpdate);
|
||||||
|
|
||||||
|
return () => void CspErrorListeners.delete(forceUpdate);
|
||||||
|
}, [forceUpdate]);
|
||||||
|
|
||||||
|
return [...CspBlockedUrls] as const;
|
||||||
|
}
|
|
@ -21,6 +21,7 @@ export * from "../shared/onceDefined";
|
||||||
export * from "./ChangeList";
|
export * from "./ChangeList";
|
||||||
export * from "./clipboard";
|
export * from "./clipboard";
|
||||||
export * from "./constants";
|
export * from "./constants";
|
||||||
|
export * from "./cspViolations";
|
||||||
export * from "./discord";
|
export * from "./discord";
|
||||||
export * from "./guards";
|
export * from "./guards";
|
||||||
export * from "./intlHash";
|
export * from "./intlHash";
|
||||||
|
|
|
@ -39,7 +39,7 @@ async function initSystemValues() {
|
||||||
createStyle("vencord-os-theme-values").textContent = `:root{${variables}}`;
|
createStyle("vencord-os-theme-values").textContent = `:root{${variables}}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function toggle(isEnabled: boolean) {
|
async function toggle(isEnabled: boolean) {
|
||||||
if (!style) {
|
if (!style) {
|
||||||
if (isEnabled) {
|
if (isEnabled) {
|
||||||
style = createStyle("vencord-custom-css");
|
style = createStyle("vencord-custom-css");
|
||||||
|
@ -92,6 +92,8 @@ async function initThemes() {
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
if (IS_USERSCRIPT) return;
|
||||||
|
|
||||||
initSystemValues();
|
initSystemValues();
|
||||||
initThemes();
|
initThemes();
|
||||||
|
|
||||||
|
@ -104,9 +106,11 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
if (!IS_WEB) {
|
if (!IS_WEB) {
|
||||||
VencordNative.quickCss.addThemeChangeListener(initThemes);
|
VencordNative.quickCss.addThemeChangeListener(initThemes);
|
||||||
}
|
}
|
||||||
});
|
}, { once: true });
|
||||||
|
|
||||||
export function initQuickCssThemeStore() {
|
export function initQuickCssThemeStore() {
|
||||||
|
if (IS_USERSCRIPT) return;
|
||||||
|
|
||||||
initThemes();
|
initThemes();
|
||||||
|
|
||||||
let currentTheme = ThemeStore.theme;
|
let currentTheme = ThemeStore.theme;
|
||||||
|
|
|
@ -53,3 +53,11 @@ export function chooseFile(mimeTypes: string) {
|
||||||
setImmediate(() => document.body.removeChild(input));
|
setImmediate(() => document.body.removeChild(input));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getStylusWebStoreUrl() {
|
||||||
|
const isChromium = (navigator as any).userAgentData?.brands?.some(b => b.brand === "Chromium");
|
||||||
|
|
||||||
|
return isChromium
|
||||||
|
? "https://chromewebstore.google.com/detail/stylus/clngdbkpkpeebahjckkjfobafhncgmne"
|
||||||
|
: "https://addons.mozilla.org/firefox/addon/styl-us/";
|
||||||
|
}
|
||||||
|
|
|
@ -141,7 +141,7 @@ export const UserUtils = {
|
||||||
|
|
||||||
export const UploadManager = findByPropsLazy("clearAll", "addFile");
|
export const UploadManager = findByPropsLazy("clearAll", "addFile");
|
||||||
export const UploadHandler = {
|
export const UploadHandler = {
|
||||||
promptToUpload: findByCodeLazy("#{intl::ATTACHMENT_TOO_MANY_ERROR_TITLE}") as (files: File[], channel: Channel, draftType: Number) => void
|
promptToUpload: findByCodeLazy("=!0,showLargeMessageDialog:") as (files: File[], channel: Channel, draftType: Number) => void
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ApplicationAssetUtils = mapMangledModuleLazy("getAssetImage: size must === [", {
|
export const ApplicationAssetUtils = mapMangledModuleLazy("getAssetImage: size must === [", {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue