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.
|
||||
|
||||
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
|
||||
|
||||
<details>
|
||||
<summary>183 additional plugins</summary>
|
||||
<summary>186 additional plugins</summary>
|
||||
|
||||
### All Platforms
|
||||
|
||||
|
@ -20,6 +20,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
|
|||
- AlwaysExpandProfile by thororen
|
||||
- AmITyping by MrDiamond
|
||||
- Anammox by Kyuuhachi
|
||||
- AudiobookShelfRPC by vMohammad
|
||||
- AtSomeone by Joona
|
||||
- BannersEverywhere by ImLvna & 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
|
||||
- InstantScreenshare by HAHALOSAH & thororen
|
||||
- IRememberYou by zoodogood
|
||||
- JellyfinRichPresence by vMohammad
|
||||
- Jumpscare by Surgedevs
|
||||
- JumpToStart by Samwich
|
||||
- KeyboardSounds by HypedDomi
|
||||
|
@ -160,6 +162,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
|
|||
- StatusPresets by iamme
|
||||
- SteamStatusSync by niko
|
||||
- StickerBlocker by Samwich
|
||||
- StreamingCodecDisabler by davidkra230
|
||||
- TalkInReverse by Tolgchu
|
||||
- TeX by Kyuuhachi
|
||||
- TextToSpeech by Samwich
|
||||
|
|
|
@ -20,15 +20,12 @@
|
|||
/// <reference path="../src/globals.d.ts" />
|
||||
|
||||
import monacoHtmlLocal from "file://monacoWin.html?minify";
|
||||
import monacoHtmlCdn from "file://../src/main/monacoWin.html?minify";
|
||||
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 { getTheme, Theme } from "../src/utils/discord";
|
||||
import { Settings } from "../src/Vencord";
|
||||
|
||||
// Discord deletes this so need to store in variable
|
||||
const { localStorage } = window;
|
||||
import { getStylusWebStoreUrl } from "@utils/web";
|
||||
|
||||
// listeners for ipc.on
|
||||
const cssListeners = new Set<(css: string) => void>();
|
||||
|
@ -76,6 +73,14 @@ window.VencordNative = {
|
|||
addThemeChangeListener: NOOP,
|
||||
openFile: NOOP_ASYNC,
|
||||
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 win = open("about:blank", "VencordQuickCss", features);
|
||||
if (!win) {
|
||||
|
@ -91,7 +96,7 @@ window.VencordNative = {
|
|||
? "vs-light"
|
||||
: "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",
|
||||
"private": "true",
|
||||
"version": "1.12.2",
|
||||
"description": "The other cutest Discord client mod",
|
||||
"homepage": "https://github.com/Equicord/Equicord#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/Equicord/Equicord/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/Equicord/Equicord.git"
|
||||
},
|
||||
"license": "GPL-3.0-or-later",
|
||||
"author": "Equicord",
|
||||
"scripts": {
|
||||
"build": "bun run scripts/build/build.mjs",
|
||||
"buildStandalone": "bun run build --standalone",
|
||||
"buildWeb": "bun run scripts/build/buildWeb.mjs",
|
||||
"buildWebStandalone": "bun run buildWeb --standalone",
|
||||
"buildReporter": "bun run buildWebStandalone --reporter --skip-extension",
|
||||
"buildReporterDesktop": "bun run build --reporter",
|
||||
"watch": "bun run build --watch",
|
||||
"dev": "bun run watch",
|
||||
"watchWeb": "bun run buildWeb --watch",
|
||||
"generatePluginJson": "bun run scripts/generatePluginList.ts",
|
||||
"generateEquicordPluginJson": "bun run scripts/generateEquicordPluginList.ts",
|
||||
"generateTypes": "tspc --emitDeclarationOnly --declaration --outDir packages/vencord-types --allowJs false",
|
||||
"inject": "bun run scripts/runInstaller.mjs --install",
|
||||
"uninject": "bun run scripts/runInstaller.mjs --uninstall",
|
||||
"lint": "eslint",
|
||||
"lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins",
|
||||
"lint:fix": "bun run lint --fix",
|
||||
"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",
|
||||
"testTsc": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ffmpeg/ffmpeg": "^0.12.10",
|
||||
"@ffmpeg/util": "^0.12.1",
|
||||
"@intrnl/xxhash64": "^0.1.2",
|
||||
"@sapphi-red/web-noise-suppressor": "0.3.5",
|
||||
"@types/less": "^3.0.6",
|
||||
"@types/stylus": "^0.48.42",
|
||||
"@types/tinycolor2": "^1.4.6",
|
||||
"@vap/core": "0.0.12",
|
||||
"@vap/shiki": "0.10.5",
|
||||
"fflate": "^0.8.2",
|
||||
"gifenc": "github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3",
|
||||
"jsqr": "1.4.0",
|
||||
"idb": "8.0.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"nanoid": "^5.1.5",
|
||||
"socket.io": "^4.8.1",
|
||||
"usercss-meta": "^0.12.0",
|
||||
"openai": "^4.30.0",
|
||||
"virtual-merge": "^1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron/asar": "^3.2.10",
|
||||
"@stylistic/eslint-plugin": "^4.2.0",
|
||||
"@types/chrome": "^0.0.312",
|
||||
"@types/diff": "^7.0.2",
|
||||
"@types/lodash": "^4.17.14",
|
||||
"@types/node": "^22.13.13",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@types/yazl": "^2.4.5",
|
||||
"diff": "^7.0.0",
|
||||
"discord-types": "^1.3.26",
|
||||
"esbuild": "^0.25.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-import-resolver-alias": "^1.1.2",
|
||||
"eslint-plugin-react": "^7.37.3",
|
||||
"eslint-plugin-simple-header": "^1.2.1",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"highlight.js": "11.11.1",
|
||||
"html-minifier-terser": "^7.2.0",
|
||||
"moment": "^2.22.2",
|
||||
"puppeteer-core": "^24.4.0",
|
||||
"standalone-electron-types": "^34.2.0",
|
||||
"stylelint": "^16.17.0",
|
||||
"stylelint-config-standard": "^37.0.0",
|
||||
"ts-patch": "^3.3.0",
|
||||
"ts-pattern": "^5.6.0",
|
||||
"type-fest": "^4.41.0",
|
||||
"typescript": "^5.8.2",
|
||||
"typescript-eslint": "^8.28.0",
|
||||
"typescript-transform-paths": "^3.5.5",
|
||||
"zip-local": "^0.3.5",
|
||||
"zustand": "^3.7.2"
|
||||
},
|
||||
"packageManager": "bun@1.1.0",
|
||||
"trustedDependencies": ["esbuild"],
|
||||
"engines": {
|
||||
"node": ">=18",
|
||||
"bun": ">=1.0.0"
|
||||
}
|
||||
}
|
||||
"name": "equicord",
|
||||
"private": "true",
|
||||
"version": "1.12.4",
|
||||
"description": "The other cutest Discord client mod",
|
||||
"homepage": "https://github.com/Equicord/Equicord#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/Equicord/Equicord/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/Equicord/Equicord.git"
|
||||
},
|
||||
"license": "GPL-3.0-or-later",
|
||||
"author": "Equicord",
|
||||
"scripts": {
|
||||
"build": "bun run scripts/build/build.mjs",
|
||||
"buildStandalone": "bun run build --standalone",
|
||||
"buildWeb": "bun run scripts/build/buildWeb.mjs",
|
||||
"buildWebStandalone": "bun run buildWeb --standalone",
|
||||
"buildReporter": "bun run buildWebStandalone --reporter --skip-extension",
|
||||
"buildReporterDesktop": "bun run build --reporter",
|
||||
"watch": "bun run build --watch",
|
||||
"dev": "bun run watch",
|
||||
"watchWeb": "bun run buildWeb --watch",
|
||||
"generatePluginJson": "bun run scripts/generatePluginList.ts",
|
||||
"generateEquicordPluginJson": "bun run scripts/generateEquicordPluginList.ts",
|
||||
"generateTypes": "tspc --emitDeclarationOnly --declaration --outDir packages/vencord-types --allowJs false",
|
||||
"inject": "bun run scripts/runInstaller.mjs --install",
|
||||
"uninject": "bun run scripts/runInstaller.mjs --uninstall",
|
||||
"lint": "eslint",
|
||||
"lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins",
|
||||
"lint:fix": "bun run lint --fix",
|
||||
"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",
|
||||
"testTsc": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ffmpeg/ffmpeg": "^0.12.10",
|
||||
"@ffmpeg/util": "^0.12.1",
|
||||
"@intrnl/xxhash64": "^0.1.2",
|
||||
"@sapphi-red/web-noise-suppressor": "0.3.5",
|
||||
"@types/less": "^3.0.6",
|
||||
"@types/stylus": "^0.48.42",
|
||||
"@types/tinycolor2": "^1.4.6",
|
||||
"@vap/core": "0.0.12",
|
||||
"@vap/shiki": "0.10.5",
|
||||
"fflate": "^0.8.2",
|
||||
"gifenc": "github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3",
|
||||
"jsqr": "1.4.0",
|
||||
"idb": "8.0.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"nanoid": "^5.1.5",
|
||||
"socket.io": "^4.8.1",
|
||||
"usercss-meta": "^0.12.0",
|
||||
"openai": "^4.30.0",
|
||||
"virtual-merge": "^1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron/asar": "^3.2.10",
|
||||
"@stylistic/eslint-plugin": "^4.2.0",
|
||||
"@types/chrome": "^0.0.312",
|
||||
"@types/diff": "^7.0.2",
|
||||
"@types/lodash": "^4.17.14",
|
||||
"@types/node": "^22.13.13",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@types/yazl": "^2.4.5",
|
||||
"diff": "^7.0.0",
|
||||
"discord-types": "^1.3.26",
|
||||
"esbuild": "^0.25.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-import-resolver-alias": "^1.1.2",
|
||||
"eslint-plugin-react": "^7.37.3",
|
||||
"eslint-plugin-simple-header": "^1.2.1",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"highlight.js": "11.11.1",
|
||||
"html-minifier-terser": "^7.2.0",
|
||||
"moment": "^2.22.2",
|
||||
"puppeteer-core": "^24.4.0",
|
||||
"standalone-electron-types": "^34.2.0",
|
||||
"stylelint": "^16.17.0",
|
||||
"stylelint-config-standard": "^37.0.0",
|
||||
"ts-patch": "^3.3.0",
|
||||
"ts-pattern": "^5.6.0",
|
||||
"type-fest": "^4.41.0",
|
||||
"typescript": "^5.8.2",
|
||||
"typescript-eslint": "^8.28.0",
|
||||
"typescript-transform-paths": "^3.5.5",
|
||||
"zip-local": "^0.3.5",
|
||||
"zustand": "^3.7.2"
|
||||
},
|
||||
"packageManager": "bun@1.1.0",
|
||||
"trustedDependencies": [
|
||||
"esbuild"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18",
|
||||
"bun": ">=1.0.0"
|
||||
}
|
||||
}
|
|
@ -34,6 +34,7 @@ const defines = stringifyValues({
|
|||
IS_UPDATER_DISABLED,
|
||||
IS_WEB: false,
|
||||
IS_EXTENSION: false,
|
||||
IS_USERSCRIPT: false,
|
||||
VERSION,
|
||||
BUILD_TIMESTAMP
|
||||
});
|
||||
|
|
|
@ -43,6 +43,7 @@ const commonOptions = {
|
|||
define: stringifyValues({
|
||||
IS_WEB: true,
|
||||
IS_EXTENSION: false,
|
||||
IS_USERSCRIPT: false,
|
||||
IS_STANDALONE: true,
|
||||
IS_DEV,
|
||||
IS_REPORTER,
|
||||
|
@ -108,6 +109,7 @@ const buildConfigs = [
|
|||
inject: ["browser/GMPolyfill.js", ...(commonOptions?.inject || [])],
|
||||
define: {
|
||||
...commonOptions.define,
|
||||
IS_USERSCRIPT: "true",
|
||||
window: "unsafeWindow",
|
||||
},
|
||||
outfile: "dist/Vencord.user.js",
|
||||
|
|
|
@ -146,7 +146,7 @@ export const globPlugins = kind => ({
|
|||
});
|
||||
|
||||
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 pluginsCode = "\n";
|
||||
let metaCode = "\n";
|
||||
|
|
|
@ -264,7 +264,7 @@ function isPluginFile({ name }: { name: string; }) {
|
|||
|
||||
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 })
|
||||
.filter(isPluginFile)
|
||||
.map(async dirent => {
|
||||
|
|
|
@ -153,7 +153,11 @@ async function init() {
|
|||
|
||||
if (!IS_WEB && !IS_UPDATER_DISABLED) {
|
||||
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) {
|
||||
|
|
|
@ -48,7 +48,7 @@ export function _modifyAccessories(
|
|||
) {
|
||||
for (const [key, accessory] of accessories.entries()) {
|
||||
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} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
|
|
@ -11,6 +11,10 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.visual-refresh .vc-notification-root {
|
||||
background-color: var(--background-base-low);
|
||||
}
|
||||
|
||||
.vc-notification-root:not(.vc-notification-log-wrapper > .vc-notification-root) {
|
||||
position: absolute;
|
||||
z-index: 2147483647;
|
||||
|
|
|
@ -75,10 +75,15 @@ const ErrorBoundary = LazyComponent(() => {
|
|||
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() {
|
||||
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)
|
||||
return (
|
||||
|
|
|
@ -4,4 +4,8 @@
|
|||
border: 1px solid #e78284;
|
||||
border-radius: 5px;
|
||||
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["aria-disabled"] = true;
|
||||
}
|
||||
|
||||
props.rel ??= "noreferrer";
|
||||
|
||||
return (
|
||||
<a role="link" target="_blank" {...props}>
|
||||
{props.children}
|
||||
|
|
|
@ -270,7 +270,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||
{!!plugin.settingsAboutComponent && (
|
||||
<div className={classes(Margins.bottom8, "vc-text-selectable")}>
|
||||
<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} />
|
||||
</ErrorBoundary>
|
||||
</Forms.FormSection>
|
||||
|
|
|
@ -244,7 +244,7 @@ export default function PluginSettings() {
|
|||
}));
|
||||
}, []);
|
||||
|
||||
const depMap = React.useMemo(() => {
|
||||
const depMap = useMemo(() => {
|
||||
const o = {} as Record<string, string[]>;
|
||||
for (const plugin in Plugins) {
|
||||
const deps = Plugins[plugin].dependencies;
|
||||
|
|
|
@ -34,6 +34,7 @@ import { useAwaiter } from "@utils/react";
|
|||
import type { ThemeHeader } from "@utils/themes";
|
||||
import { getThemeInfo, stripBOM, type UserThemeHeader } from "@utils/themes/bd";
|
||||
import { usercssParse } from "@utils/themes/usercss";
|
||||
import { getStylusWebStoreUrl } from "@utils/web";
|
||||
import { findLazy } from "@webpack";
|
||||
import { Button, Card, Forms, React, showToast, TabBar, TextInput, Tooltip, useEffect, useMemo, useRef, useState } from "@webpack/common";
|
||||
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: [
|
||||
{
|
||||
// Patch activity icons
|
||||
find: "isBlockedOrIgnored(null",
|
||||
find: '"ActivityStatus"',
|
||||
replacement: {
|
||||
match: /(?<=hideTooltip:.{0,4}}=(\i).*?{}\))\]/,
|
||||
replace: ",$self.patchActivityList($1)]"
|
||||
},
|
||||
predicate: () => settings.store.memberList,
|
||||
all: true
|
||||
},
|
||||
{
|
||||
// Show all activities in the user popout/sidebar
|
||||
find: "hasAvatarForGuild(null",
|
||||
find: '"UserProfilePopoutBody"',
|
||||
replacement: {
|
||||
match: /(?<=(\i)\.id\)\}\)\),(\i).*?)\(0,.{0,100}\i\.id,onClose:\i\}\)/,
|
||||
replace: "$self.showAllActivitiesComponent({ activity: $2, user: $1 })"
|
||||
|
|
|
@ -10,14 +10,13 @@ import { React, Tooltip } from "@webpack/common";
|
|||
import { JSX } from "react";
|
||||
|
||||
import { ActivityTooltip } from "../components/ActivityTooltip";
|
||||
import { SpotifyIcon } from "../components/SpotifyIcon";
|
||||
import { TwitchIcon } from "../components/TwitchIcon";
|
||||
import { settings } from "../settings";
|
||||
import { ActivityListIcon, ActivityListProps, ApplicationIcon, IconCSSProperties } from "../types";
|
||||
import { cl, getApplicationIcons } from "../utils";
|
||||
|
||||
// if discord one day decides to change their icon this needs to be updated
|
||||
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");
|
||||
// Discord no longer shows an icon here by default but we use the one from the popout now here
|
||||
const DefaultActivityIcon = findComponentByCodeLazy("M5 2a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3V5a3 3 0 0 0-3-3H5Zm6.81 7c-.54 0-1 .26-1.23.61A1 1 0 0 1 8.92 8.5 3.49 3.49 0 0 1 11.82 7c1.81 0 3.43 1.38 3.43 3.25 0 1.45-.98 2.61-2.27 3.06a1 1 0 0 1-1.96.37l-.19-1a1 1 0 0 1 .98-1.18c.87 0 1.44-.63 1.44-1.25S12.68 9 11.81 9ZM13 16a1 1 0 1 1-2 0 1 1 0 0 1 2 0Zm7-10.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0ZM18.5 20a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM7 18.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0ZM5.5 7a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z");
|
||||
|
||||
export function patchActivityList({ activities, user, hideTooltip }: ActivityListProps): JSX.Element | null {
|
||||
const icons: ActivityListIcon[] = [];
|
||||
|
@ -57,7 +56,6 @@ export function patchActivityList({ activities, user, hideTooltip }: ActivityLis
|
|||
}
|
||||
};
|
||||
addActivityIcon("Twitch", TwitchIcon);
|
||||
addActivityIcon("Spotify", SpotifyIcon);
|
||||
|
||||
if (icons.length) {
|
||||
const iconStyle: IconCSSProperties = {
|
||||
|
@ -86,7 +84,7 @@ export function patchActivityList({ activities, user, hideTooltip }: ActivityLis
|
|||
// We need to filter out custom statuses
|
||||
const shouldShow = activities.filter(a => a.type !== 4).length !== icons.length;
|
||||
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[] {
|
||||
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) {
|
||||
const { assets, application_id, platform } = activity;
|
||||
if (!application_id && !platform) continue;
|
||||
const { assets, application_id, platform, id } = activity;
|
||||
if (!application_id && !platform && !id.startsWith("spotify:")) continue;
|
||||
|
||||
if (assets) {
|
||||
const { small_image, small_text, large_image, large_text } = assets;
|
||||
|
@ -59,6 +59,12 @@ export function getApplicationIcons(activities: Activity[], preferSmall = false)
|
|||
activity
|
||||
});
|
||||
}
|
||||
} else if (image.startsWith("spotify:")) {
|
||||
const url = `https://i.scdn.co/image/${image.split(":")[1]}`;
|
||||
applicationIcons.push({
|
||||
image: { src: url, alt },
|
||||
activity
|
||||
});
|
||||
} else {
|
||||
const src = `https://cdn.discordapp.com/app-assets/${application_id}/${image}.png`;
|
||||
applicationIcons.push({
|
||||
|
|
|
@ -7,4 +7,4 @@
|
|||
pointer-events: none;
|
||||
z-index: 1;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import definePlugin from "@utils/types";
|
|||
|
||||
export default definePlugin({
|
||||
name: "BypassPinPrompt",
|
||||
description: "Bypass the pin prompt when pinning messages",
|
||||
description: "Bypass the pin prompt when using the pin functions",
|
||||
authors: [EquicordDevs.thororen],
|
||||
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[]; }) {
|
||||
children.splice(
|
||||
children.length - 1, 0,
|
||||
<ErrorBoundary noop={true}>
|
||||
<ErrorBoundary noop>
|
||||
<VencordPopoutButton />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
|
|
@ -24,8 +24,6 @@ export const reverseExtensionMap = Object.entries(extensionMap).reduce((acc, [ta
|
|||
return acc;
|
||||
}, {} as Record<string, string>);
|
||||
|
||||
type ExtUpload = Upload & { fixExtension?: boolean; };
|
||||
|
||||
export default definePlugin({
|
||||
name: "FixFileExtensions",
|
||||
authors: [EquicordDevs.thororen],
|
||||
|
@ -34,15 +32,21 @@ export default definePlugin({
|
|||
patches: [
|
||||
// Taken from AnonymiseFileNames
|
||||
{
|
||||
find: 'type:"UPLOAD_START"',
|
||||
replacement: {
|
||||
match: /await \i\.uploadFiles\((\i),/,
|
||||
replace: "$1.forEach($self.fixExt),$&"
|
||||
},
|
||||
find: "async uploadFiles(",
|
||||
replacement: [
|
||||
{
|
||||
match: /async uploadFiles\((\i),\i\){/,
|
||||
replace: "$&$1.forEach($self.fixExt);"
|
||||
},
|
||||
{
|
||||
match: /async uploadFilesSimple\((\i)\){/,
|
||||
replace: "$&$1.forEach($self.fixExt);"
|
||||
}
|
||||
],
|
||||
predicate: () => !Settings.plugins.AnonymiseFileNames.enabled,
|
||||
},
|
||||
],
|
||||
fixExt(upload: ExtUpload) {
|
||||
fixExt(upload: Upload) {
|
||||
const file = upload.filename;
|
||||
const tarMatch = tarExtMatcher.exec(file);
|
||||
const extIdx = tarMatch?.index ?? file.lastIndexOf(".");
|
||||
|
|
|
@ -24,7 +24,7 @@ export default async (
|
|||
}: FurudoSettings,
|
||||
repliedMessage?: Message
|
||||
): 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",
|
||||
headers: {
|
||||
"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 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 icon = `https://cdn.discordapp.com/quests/${quest.id}/${theme}/${quest.config.assets.gameTile}`;
|
||||
// @ts-ignore
|
||||
const secondsNeeded = quest.config.taskConfig.tasks[taskName].target;
|
||||
// @ts-ignore
|
||||
const secondsDone = quest.userStatus?.progress?.[taskName]?.value ?? 0;
|
||||
const icon = `https://cdn.discordapp.com/assets/quests/${quest.id}/${theme}/${quest.config.assets.gameTile}`;
|
||||
let secondsDone = quest.userStatus?.progress?.[taskName]?.value ?? 0;
|
||||
if (taskName === "WATCH_VIDEO") {
|
||||
const tolerance = 2, speed = 10;
|
||||
const diff = Math.floor((Date.now() - new Date(quest.userStatus.enrolledAt).getTime()) / 1000);
|
||||
const startingPoint = Math.min(Math.max(Math.ceil(secondsDone), diff), secondsNeeded);
|
||||
const maxFuture = 10, speed = 7, interval = 1;
|
||||
const enrolledAt = new Date(quest.userStatus.enrolledAt).getTime();
|
||||
const fn = async () => {
|
||||
for (let i = startingPoint; i <= secondsNeeded; i += speed) {
|
||||
try {
|
||||
await RestAPI.post({ url: `/quests/${quest.id}/video-progress`, body: { timestamp: Math.min(secondsNeeded, i + Math.random()) } });
|
||||
} catch (ex) {
|
||||
console.log("Failed to send increment of", i, ex);
|
||||
while (true) {
|
||||
const maxAllowed = Math.floor((Date.now() - enrolledAt) / 1000) + maxFuture;
|
||||
const diff = maxAllowed - secondsDone;
|
||||
const timestamp = secondsDone + speed;
|
||||
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 ((secondsNeeded - secondsDone) % speed !== 0) {
|
||||
await RestAPI.post({ url: `/quests/${quest.id}/video-progress`, body: { timestamp: secondsNeeded } });
|
||||
showNotification({
|
||||
title: `${applicationName} - Quest Completer`,
|
||||
body: "Quest Completed.",
|
||||
icon: icon,
|
||||
});
|
||||
if (timestamp >= secondsNeeded) {
|
||||
break;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, interval * 1000));
|
||||
}
|
||||
showNotification({
|
||||
title: `${applicationName} - Quest Completer`,
|
||||
body: "Quest Completed.",
|
||||
icon: icon,
|
||||
});
|
||||
};
|
||||
fn();
|
||||
showNotification({
|
||||
title: `${applicationName} - Quest Completer`,
|
||||
body: `Wait for ${Math.ceil((secondsNeeded - startingPoint) / speed * tolerance)} more seconds.`,
|
||||
body: `Spoofing video for ${applicationName}.`,
|
||||
icon: icon,
|
||||
});
|
||||
console.log(`Spoofing video for ${applicationName}.`);
|
||||
} else if (taskName === "PLAY_ON_DESKTOP") {
|
||||
RestAPI.get({ url: `/applications/public?application_ids=${applicationId}` }).then(res => {
|
||||
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";
|
||||
|
||||
const databaseTimezones: Record<string, { value: string | null; }> = {};
|
||||
|
||||
const DOMAIN = "https://timezone.creations.works";
|
||||
const REDIRECT_URI = `${DOMAIN}/auth/discord/callback`;
|
||||
const CLIENT_ID = "1377021506810417173";
|
||||
|
@ -26,7 +25,6 @@ export async function loadDatabaseTimezones(): Promise<boolean> {
|
|||
const res = await fetch(`${DOMAIN}/list`, {
|
||||
headers: { Accept: "application/json" }
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
for (const id in json) {
|
||||
|
@ -34,10 +32,8 @@ export async function loadDatabaseTimezones(): Promise<boolean> {
|
|||
value: json[id]?.timezone ?? null
|
||||
};
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (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> {
|
||||
const res = await fetch(`${DOMAIN}/set?timezone=${encodeURIComponent(timezone)}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json"
|
||||
},
|
||||
credentials: "include"
|
||||
});
|
||||
async function checkAuthentication(): Promise<boolean> {
|
||||
try {
|
||||
const res = await fetch(`${DOMAIN}/me`, {
|
||||
credentials: "include",
|
||||
headers: { Accept: "application/json" }
|
||||
});
|
||||
return res.ok;
|
||||
} 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> {
|
||||
const res = await fetch(`${DOMAIN}/delete`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json"
|
||||
},
|
||||
credentials: "include"
|
||||
});
|
||||
const isAuthenticated = await checkAuthentication();
|
||||
|
||||
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) {
|
||||
|
@ -83,21 +142,17 @@ export function authModal(callback?: () => void) {
|
|||
cancelCompletesFlow={false}
|
||||
callback={async (res: any) => {
|
||||
if (!res || !res.location) return;
|
||||
|
||||
try {
|
||||
const url = new URL(res.location);
|
||||
|
||||
const r = await fetch(url, {
|
||||
credentials: "include",
|
||||
headers: { Accept: "application/json" }
|
||||
});
|
||||
|
||||
const json = await r.json();
|
||||
if (!r.ok) {
|
||||
showToast(json.message ?? "Authorization failed", Toasts.Type.FAILURE);
|
||||
return;
|
||||
}
|
||||
|
||||
showToast("Authorization successful!", Toasts.Type.SUCCESS);
|
||||
callback?.();
|
||||
} catch (e) {
|
||||
|
|
|
@ -17,7 +17,7 @@ import { findByPropsLazy } from "@webpack";
|
|||
import { Button, Menu, showToast, Toasts, Tooltip, useEffect, UserStore, useState } from "@webpack/common";
|
||||
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";
|
||||
|
||||
export let timezones: Record<string, string | null> = {};
|
||||
|
@ -68,9 +68,7 @@ export const settings = definePluginSettings({
|
|||
type: OptionType.COMPONENT,
|
||||
component: () => (
|
||||
<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
|
||||
</Button>
|
||||
|
@ -83,11 +81,19 @@ export const settings = definePluginSettings({
|
|||
component: () => (
|
||||
<Button
|
||||
color={Button.Colors.RED}
|
||||
onClick={() => {
|
||||
authModal(async () => {
|
||||
onClick={async () => {
|
||||
try {
|
||||
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
|
||||
|
@ -228,9 +234,7 @@ export default definePlugin({
|
|||
|
||||
toolboxActions: {
|
||||
"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 () => {
|
||||
try {
|
||||
|
@ -265,9 +269,7 @@ export default definePlugin({
|
|||
<Button
|
||||
color={Button.Colors.GREEN}
|
||||
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.
|
||||
|
|
|
@ -29,7 +29,7 @@ export default definePlugin({
|
|||
authors: [Devs.AutumnVN],
|
||||
start() {
|
||||
(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("close", () => wsReconnect = setTimeout(connect, 5000));
|
||||
ws.addEventListener("message", ({ data }) => throttledOnMessage(data));
|
||||
|
|
|
@ -8,8 +8,7 @@ import { NavContextMenuPatchCallback } from "@api/ContextMenu";
|
|||
import { openModal } from "@utils/modal";
|
||||
import { ChannelStore, FluxDispatcher, Menu } from "@webpack/common";
|
||||
|
||||
import { SetCustomWallpaperModal, SetDiscordWallpaperModal } from "./modal";
|
||||
import { ChatWallpaperStore, fetchWallpapers } from "./util";
|
||||
import { SetWallpaperModal } from "./modal";
|
||||
|
||||
|
||||
const addWallpaperMenu = (channelId?: string, guildId?: string) => {
|
||||
|
@ -22,25 +21,18 @@ const addWallpaperMenu = (channelId?: string, guildId?: string) => {
|
|||
url,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu.MenuItem label="Wallpaper Free" key="vc-wpfree-menu" id="vc-wpfree-menu">
|
||||
<Menu.MenuItem
|
||||
label="Set custom wallpaper"
|
||||
id="vc-wpfree-set-custom"
|
||||
action={() => openModal(props => <SetCustomWallpaperModal 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} />);
|
||||
}}
|
||||
label="Set Wallpaper"
|
||||
id="vc-wpfree-set-wallpaper"
|
||||
action={() => openModal(props => <SetWallpaperModal props={props} onSelect={setWallpaper} />)}
|
||||
/>
|
||||
<Menu.MenuSeparator />
|
||||
<Menu.MenuItem
|
||||
label="Remove Custom Wallpaper"
|
||||
id="vc-wpfree-remove"
|
||||
label="Remove Wallpaper"
|
||||
id="vc-wpfree-remove-wallpaper"
|
||||
color="danger"
|
||||
action={() => setWallpaper(void 0)}
|
||||
/>
|
||||
|
|
|
@ -5,38 +5,37 @@
|
|||
*/
|
||||
|
||||
import { ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
|
||||
import { Button, lodash, Text, TextInput, useState, useStateFromStores } from "@webpack/common";
|
||||
|
||||
import { ChatWallpaperStore, Wallpaper } from "./util";
|
||||
import { Button, Text, TextInput, useState } from "@webpack/common";
|
||||
|
||||
interface Props {
|
||||
props: ModalProps;
|
||||
onSelect: (url: string) => void;
|
||||
}
|
||||
|
||||
export function SetCustomWallpaperModal({ props, onSelect }: Props) {
|
||||
export function SetWallpaperModal({ props, onSelect }: Props) {
|
||||
const [url, setUrl] = useState("");
|
||||
|
||||
return (
|
||||
<ModalRoot {...props} size={ModalSize.SMALL}>
|
||||
<ModalHeader>
|
||||
<Text variant="heading-lg/normal" style={{ marginBottom: 8 }}>
|
||||
Set a custom wallpaper
|
||||
Set wallpaper
|
||||
</Text>
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
||||
|
||||
<Text>The image url</Text>
|
||||
<TextInput
|
||||
placeholder="The image url"
|
||||
value={url}
|
||||
onChange={setUrl}
|
||||
onChange={u => {
|
||||
setUrl(u);
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
{url && (
|
||||
<img
|
||||
alt=""
|
||||
src={url}
|
||||
alt="Wallpaper preview"
|
||||
style={{
|
||||
display: "block",
|
||||
width: "100%",
|
||||
|
@ -60,57 +59,6 @@ export function SetCustomWallpaperModal({ props, onSelect }: Props) {
|
|||
</div>
|
||||
</div>
|
||||
</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 { makeCodeblock } from "@utils/text";
|
||||
import { findByCodeLazy, findStoreLazy } from "@webpack";
|
||||
import { Button, FluxDispatcher, Parser } from "@webpack/common";
|
||||
|
||||
import { SetCustomWallpaperModal, SetDiscordWallpaperModal } from "./modal";
|
||||
|
||||
export const ChatWallpaperStore = findStoreLazy("ChatWallpaperStore");
|
||||
export const fetchWallpapers = findByCodeLazy('type:"FETCH_CHAT_WALLPAPERS_SUCCESS"');
|
||||
import { SetWallpaperModal } from "./modal";
|
||||
|
||||
export function GlobalDefaultComponent() {
|
||||
const setGlobal = (url?: string) => {
|
||||
|
@ -26,13 +22,8 @@ export function GlobalDefaultComponent() {
|
|||
return (
|
||||
<>
|
||||
<Button onClick={() => {
|
||||
openModal(props => <SetCustomWallpaperModal props={props} onSelect={setGlobal} />);
|
||||
}}>Set a global custom wallpaper</Button>
|
||||
|
||||
<Button onClick={async () => {
|
||||
ChatWallpaperStore.shouldFetchWallpapers && await fetchWallpapers();
|
||||
openModal(props => <SetDiscordWallpaperModal props={props} onSelect={setGlobal} />);
|
||||
}}>Set a global Discord wallpaper</Button>
|
||||
openModal(props => <SetWallpaperModal props={props} onSelect={setGlobal} />);
|
||||
}}>Set a global wallpaper</Button>
|
||||
|
||||
<Button
|
||||
color={Button.Colors.RED}
|
||||
|
@ -52,30 +43,10 @@ export function GlobalDefaultComponent() {
|
|||
|
||||
export function TipsComponent() {
|
||||
const tipText = `
|
||||
[class^=wallpaperContainer] {
|
||||
.vc-wpfree-wp-container {
|
||||
transform: scaleX(-1); /* flip it horizontally */
|
||||
filter: blur(4px); /* apply a blur */
|
||||
opacity: 0.7; /* self-explanatory */
|
||||
}`;
|
||||
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 { definePluginSettings } from "@api/Settings";
|
||||
import { ErrorBoundary } from "@components/index";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { useStateFromStores } from "@webpack/common";
|
||||
import { Channel } from "discord-types/general";
|
||||
|
||||
import { ChannelContextPatch, GuildContextPatch, UserContextPatch } from "./components/ctxmenu";
|
||||
import { GlobalDefaultComponent, TipsComponent, Wallpaper } from "./components/util";
|
||||
import { GlobalDefaultComponent, TipsComponent } from "./components/util";
|
||||
import { WallpaperFreeStore } from "./store";
|
||||
|
||||
|
||||
const settings = definePluginSettings({
|
||||
forceReplace: {
|
||||
description: "If a dm wallpaper is already set, your custom wallpaper will be used instead.",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: false,
|
||||
export const settings = definePluginSettings({
|
||||
globalDefault: {
|
||||
description: "Set a global default wallpaper for all channels.",
|
||||
type: OptionType.COMPONENT,
|
||||
component: GlobalDefaultComponent
|
||||
},
|
||||
stylingTips: {
|
||||
description: "",
|
||||
type: OptionType.COMPONENT,
|
||||
component: TipsComponent,
|
||||
},
|
||||
globalDefault: {
|
||||
description: "Set a global default wallpaper for all channels.",
|
||||
type: OptionType.COMPONENT,
|
||||
component: GlobalDefaultComponent
|
||||
}
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "WallpaperFree",
|
||||
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: [
|
||||
{
|
||||
find: ".wallpaperContainer,",
|
||||
find: ".handleSendMessage,onResize",
|
||||
group: true,
|
||||
replacement: [
|
||||
{
|
||||
match: /return null==(\i).+?\?null:/,
|
||||
replace: "const vcWpFreeCustom = $self.customWallpaper(arguments[0].channel,$1);return !($1||vcWpFreeCustom)?null:"
|
||||
match: /return.{1,150},(?=keyboardModeEnabled)/,
|
||||
replace: "const vcWallpaperFreeUrl=$self.WallpaperState(arguments[0].channel);$&vcWallpaperFreeUrl,"
|
||||
},
|
||||
{
|
||||
match: /,{chatWallpaperState:/,
|
||||
replace: "$&vcWpFreeCustom||"
|
||||
},
|
||||
{
|
||||
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)"
|
||||
},
|
||||
match: /}\)]}\)](?=.{1,30}messages-)/,
|
||||
replace: "$&.toSpliced(0,0,$self.Wallpaper({url:this.props.vcWallpaperFreeUrl}))"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
@ -71,21 +59,17 @@ export default definePlugin({
|
|||
"guild-context": GuildContextPatch,
|
||||
"gdm-context": ChannelContextPatch,
|
||||
},
|
||||
customWallpaper(channel: Channel, wp: Wallpaper | undefined) {
|
||||
const { forceReplace } = settings.use(["forceReplace"]);
|
||||
const url = useStateFromStores([WallpaperFreeStore], () => WallpaperFreeStore.getUrl(channel));
|
||||
Wallpaper({ url }: { url: string; }) {
|
||||
// no we cant place the hook here
|
||||
if (!url) return null;
|
||||
|
||||
if (!forceReplace && wp?.id)
|
||||
return wp;
|
||||
|
||||
if (url) {
|
||||
return {
|
||||
wallpaperId: "id",
|
||||
vcWallpaperUrl: url,
|
||||
isViewable: true,
|
||||
};
|
||||
}
|
||||
|
||||
return void 0;
|
||||
return <ErrorBoundary noop>
|
||||
<div className="wallpaperContainer vc-wpfree-wp-container" style={{
|
||||
backgroundImage: `url(${url})`,
|
||||
}}></div>
|
||||
</ErrorBoundary>;
|
||||
},
|
||||
WallpaperState(channel: Channel) {
|
||||
return useStateFromStores([WallpaperFreeStore], () => WallpaperFreeStore.getUrl(channel));
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,30 +1,8 @@
|
|||
.vc-wpfree-discord-wp-modal {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 24px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.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
|
||||
.vc-wpfree-wp-container,
|
||||
.wallpaperContainer {
|
||||
background-position: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
inset: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
|
3
src/globals.d.ts
vendored
3
src/globals.d.ts
vendored
|
@ -29,11 +29,12 @@ declare global {
|
|||
* replace: "IS_WEB?foo:bar"
|
||||
* // GOOD
|
||||
* replace: IS_WEB ? "foo" : "bar"
|
||||
* // also good
|
||||
* // also okay
|
||||
* replace: `${IS_WEB}?foo:bar`
|
||||
*/
|
||||
export var IS_WEB: boolean;
|
||||
export var IS_EXTENSION: boolean;
|
||||
export var IS_USERSCRIPT: boolean;
|
||||
export var IS_STANDALONE: boolean;
|
||||
export var IS_UPDATER_DISABLED: 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/>.
|
||||
*/
|
||||
|
||||
import { app, protocol, session } from "electron";
|
||||
import { app, net, protocol } from "electron";
|
||||
import { join } from "path";
|
||||
import { pathToFileURL } from "url";
|
||||
|
||||
import { initCsp } from "./csp";
|
||||
import { ensureSafePath } from "./ipcMain";
|
||||
import { RendererSettings } from "./settings";
|
||||
import { IS_VANILLA, THEMES_DIR } from "./utils/constants";
|
||||
|
@ -26,55 +28,71 @@ import { installExt } from "./utils/extensions";
|
|||
|
||||
if (!IS_VANILLA && !IS_EXTENSION) {
|
||||
app.whenReady().then(() => {
|
||||
// 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
|
||||
protocol.registerFileProtocol("vencord", ({ url: unsafeUrl }, cb) => {
|
||||
let url = unsafeUrl.slice("vencord://".length);
|
||||
protocol.handle("vencord", ({ url: unsafeUrl }) => {
|
||||
let url = decodeURI(unsafeUrl).slice("vencord://".length).replace(/\?v=\d+$/, "");
|
||||
|
||||
if (url.endsWith("/")) url = url.slice(0, -1);
|
||||
|
||||
if (url.startsWith("/themes/")) {
|
||||
const theme = url.slice("/themes/".length);
|
||||
|
||||
const safeUrl = ensureSafePath(THEMES_DIR, theme);
|
||||
if (!safeUrl) {
|
||||
cb({ statusCode: 403 });
|
||||
return;
|
||||
return new Response(null, {
|
||||
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) {
|
||||
case "renderer.js.map":
|
||||
case "preload.js.map":
|
||||
case "patcher.js.map":
|
||||
case "main.js.map":
|
||||
cb(join(__dirname, url));
|
||||
break;
|
||||
return net.fetch(pathToFileURL(join(__dirname, url)).toString());
|
||||
default:
|
||||
cb({ statusCode: 403 });
|
||||
return new Response(null, {
|
||||
status: 404
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
protocol.registerFileProtocol("equicord", ({ url: unsafeUrl }, cb) => {
|
||||
let url = unsafeUrl.slice("equicord://".length);
|
||||
protocol.handle("equicord", ({ url: unsafeUrl }) => {
|
||||
let url = decodeURI(unsafeUrl).slice("equicord://".length).replace(/\?v=\d+$/, "");
|
||||
|
||||
if (url.endsWith("/")) url = url.slice(0, -1);
|
||||
|
||||
if (url.startsWith("/themes/")) {
|
||||
const theme = url.slice("/themes/".length);
|
||||
|
||||
const safeUrl = ensureSafePath(THEMES_DIR, theme);
|
||||
if (!safeUrl) {
|
||||
cb({ statusCode: 403 });
|
||||
return;
|
||||
return new Response(null, {
|
||||
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) {
|
||||
case "renderer.js.map":
|
||||
case "preload.js.map":
|
||||
case "patcher.js.map":
|
||||
case "main.js.map":
|
||||
cb(join(__dirname, url));
|
||||
break;
|
||||
return net.fetch(pathToFileURL(join(__dirname, url)).toString());
|
||||
default:
|
||||
cb({ statusCode: 403 });
|
||||
return new Response(null, {
|
||||
status: 404
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -86,70 +104,7 @@ if (!IS_VANILLA && !IS_EXTENSION) {
|
|||
} catch { }
|
||||
|
||||
|
||||
const findHeader = (headers: Record<string, string[]>, headerName: Lowercase<string>) => {
|
||||
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 = () => { };
|
||||
initCsp();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ import { makeLinksOpenExternally } from "./utils/externalLinks";
|
|||
mkdirSync(THEMES_DIR, { recursive: true });
|
||||
|
||||
export function ensureSafePath(basePath: string, path: string) {
|
||||
const normalizedBasePath = normalize(basePath);
|
||||
const normalizedBasePath = normalize(basePath + "/");
|
||||
const newPath = join(basePath, path);
|
||||
const normalizedPath = normalize(newPath);
|
||||
return normalizedPath.startsWith(normalizedBasePath) ? normalizedPath : null;
|
||||
|
|
|
@ -30,7 +30,10 @@ export function serializeErrors(func: (...args: any[]) => any) {
|
|||
ok: false,
|
||||
error: e instanceof Error ? {
|
||||
// prototypes get lost, so turn error into plain object
|
||||
...e
|
||||
...e,
|
||||
message: e.message,
|
||||
name: e.name,
|
||||
stack: e.stack
|
||||
} : e
|
||||
};
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
* 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 { VENCORD_USER_AGENT } from "@shared/vencordUserAgent";
|
||||
import { ipcMain } from "electron";
|
||||
|
@ -30,8 +30,8 @@ import { ASAR_FILE, serializeErrors } from "./common";
|
|||
const API_BASE = `https://api.github.com/repos/${gitRemote}`;
|
||||
let PendingUpdate: string | null = null;
|
||||
|
||||
async function githubGet(endpoint: string) {
|
||||
return get(API_BASE + endpoint, {
|
||||
async function githubGet<T = any>(endpoint: string) {
|
||||
return fetchJson<T>(API_BASE + endpoint, {
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
// "All API requests MUST include a valid User-Agent header.
|
||||
|
@ -45,9 +45,8 @@ async function calculateGitChanges() {
|
|||
const isOutdated = await fetchUpdates();
|
||||
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) => ({
|
||||
// github api only sends the long sha
|
||||
hash: c.sha.slice(0, 7),
|
||||
|
@ -57,9 +56,8 @@ async function calculateGitChanges() {
|
|||
}
|
||||
|
||||
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);
|
||||
if (hash === gitHash)
|
||||
return false;
|
||||
|
@ -74,7 +72,7 @@ async function fetchUpdates() {
|
|||
async function applyUpdates() {
|
||||
if (!PendingUpdate) return true;
|
||||
|
||||
const data = await get(PendingUpdate);
|
||||
const data = await fetchBuffer(PendingUpdate);
|
||||
originalWriteFileSync(__dirname, data);
|
||||
|
||||
PendingUpdate = null;
|
||||
|
|
|
@ -24,7 +24,7 @@ import { join } from "path";
|
|||
|
||||
import { DATA_DIR } from "./constants";
|
||||
import { crxToZip } from "./crxToZip";
|
||||
import { get } from "./simpleGet";
|
||||
import { fetchBuffer } from "./http";
|
||||
|
||||
const extensionCacheDir = join(DATA_DIR, "ExtensionCache");
|
||||
|
||||
|
@ -69,13 +69,14 @@ export async function installExt(id: string) {
|
|||
} 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 buf = await get(url, {
|
||||
const buf = await fetchBuffer(url, {
|
||||
headers: {
|
||||
"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);
|
||||
|
|
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";
|
||||
|
||||
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_DONOR_BADGE = "https://cdn.nest.rip/uploads/78cb1e77-b7a6-4242-9089-e91f866159bf.png";
|
||||
|
||||
|
|
|
@ -79,11 +79,17 @@ export default definePlugin({
|
|||
|
||||
patches: [
|
||||
{
|
||||
find: 'type:"UPLOAD_START"',
|
||||
replacement: {
|
||||
match: /await \i\.uploadFiles\((\i),/,
|
||||
replace: "$1.forEach($self.anonymise),$&"
|
||||
},
|
||||
find: "async uploadFiles(",
|
||||
replacement: [
|
||||
{
|
||||
match: /async uploadFiles\((\i),\i\){/,
|
||||
replace: "$&$1.forEach($self.anonymise);"
|
||||
},
|
||||
{
|
||||
match: /async uploadFilesSimple\((\i)\){/,
|
||||
replace: "$&$1.forEach($self.anonymise);"
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
find: "#{intl::ATTACHMENT_UTILITIES_SPOILER}",
|
||||
|
|
|
@ -20,7 +20,6 @@ import "./style.css";
|
|||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { getIntlMessage } from "@utils/discord";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByPropsLazy, findLazy, findStoreLazy } from "@webpack";
|
||||
import { FluxDispatcher } from "@webpack/common";
|
||||
|
@ -350,7 +349,7 @@ export default definePlugin({
|
|||
}
|
||||
|
||||
try {
|
||||
return child?.props?.["aria-label"] === getIntlMessage("SERVERS");
|
||||
return child?.props?.renderTreeNode !== null;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
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) {
|
||||
// Pending guilds
|
||||
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) {
|
||||
let wasConnected = isManual;
|
||||
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) {
|
||||
ws.send(JSON.stringify(data));
|
||||
|
|
|
@ -118,7 +118,7 @@ export default definePlugin({
|
|||
renderSearchBar(instance: Instance, SearchBarComponent: TSearchBarComponent) {
|
||||
this.instance = instance;
|
||||
return (
|
||||
<ErrorBoundary noop={true}>
|
||||
<ErrorBoundary noop>
|
||||
<SearchBar instance={instance} SearchBarComponent={SearchBarComponent} />
|
||||
</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`}
|
||||
/>
|
||||
);
|
||||
}),
|
||||
}, { noop: true }),
|
||||
});
|
||||
|
||||
function getUsernameString(username: string) {
|
||||
|
|
|
@ -20,7 +20,8 @@ import { definePluginSettings } from "@api/Settings";
|
|||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
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 EditStore = findByPropsLazy("isEditing", "isEditingAny");
|
||||
|
@ -28,6 +29,7 @@ const EditStore = findByPropsLazy("isEditing", "isEditingAny");
|
|||
let isDeletePressed = false;
|
||||
const keydown = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = true);
|
||||
const keyup = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = false);
|
||||
const focusChanged = () => !WindowStore.isFocused() && (isDeletePressed = false);
|
||||
|
||||
const settings = definePluginSettings({
|
||||
enableDeleteOnClick: {
|
||||
|
@ -62,11 +64,13 @@ export default definePlugin({
|
|||
start() {
|
||||
document.addEventListener("keydown", keydown);
|
||||
document.addEventListener("keyup", keyup);
|
||||
WindowStore.addChangeListener(focusChanged);
|
||||
},
|
||||
|
||||
stop() {
|
||||
document.removeEventListener("keydown", keydown);
|
||||
document.removeEventListener("keyup", keyup);
|
||||
WindowStore.removeChangeListener(focusChanged);
|
||||
},
|
||||
|
||||
onMessageClick(msg: any, channel, event) {
|
||||
|
@ -89,9 +93,8 @@ export default definePlugin({
|
|||
if (msg.hasFlag(EPHEMERAL)) return;
|
||||
|
||||
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("NoReplyMention")
|
||||
? NoReplyMention.shouldMention(msg, isShiftPress)
|
||||
const shouldMention = Vencord.Plugins.isPluginEnabled(NoReplyMentionPlugin.name)
|
||||
? NoReplyMentionPlugin.shouldMention(msg, isShiftPress)
|
||||
: !isShiftPress;
|
||||
|
||||
FluxDispatcher.dispatch({
|
||||
|
|
|
@ -20,7 +20,6 @@ import { addMessageAccessory, removeMessageAccessory } from "@api/MessageAccesso
|
|||
import { updateMessage } from "@api/MessageUpdater";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { getUserSettingLazy } from "@api/UserSettings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants.js";
|
||||
import { classes } from "@utils/misc";
|
||||
import { Queue } from "@utils/Queue";
|
||||
|
@ -373,7 +372,7 @@ export default definePlugin({
|
|||
settings,
|
||||
|
||||
start() {
|
||||
addMessageAccessory("messageLinkEmbed", props => {
|
||||
addMessageAccessory("MessageLinkEmbeds", props => {
|
||||
if (!messageLinkRegex.test(props.message.content))
|
||||
return null;
|
||||
|
||||
|
@ -381,15 +380,13 @@ export default definePlugin({
|
|||
messageLinkRegex.lastIndex = 0;
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<MessageEmbedAccessory
|
||||
message={props.message}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
<MessageEmbedAccessory
|
||||
message={props.message}
|
||||
/>
|
||||
);
|
||||
}, 4 /* just above rich embeds */);
|
||||
},
|
||||
stop() {
|
||||
removeMessageAccessory("messageLinkEmbed");
|
||||
removeMessageAccessory("MessageLinkEmbeds");
|
||||
}
|
||||
});
|
||||
|
|
|
@ -121,14 +121,9 @@ export default definePlugin({
|
|||
},
|
||||
// Make the gap between each item smaller so our tab can fit.
|
||||
{
|
||||
match: /className:\i\.tabBar/,
|
||||
replace: '$& + " vc-mutual-gdms-modal-v2-tab-bar"'
|
||||
match: /type:"top",/,
|
||||
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 {
|
||||
gap: 12px;
|
||||
--space-xl: 16px;
|
||||
}
|
||||
|
|
|
@ -75,5 +75,5 @@ export default definePlugin({
|
|||
}}> Pause Indefinitely.</a>}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}, { noop: true })
|
||||
});
|
||||
|
|
|
@ -16,19 +16,20 @@
|
|||
* 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 definePlugin, { OptionType } from "@utils/types";
|
||||
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 NoBlockedMessagesPlugin from "plugins/noBlockedMessages";
|
||||
import NoReplyMentionPlugin from "plugins/noReplyMention";
|
||||
|
||||
const Kangaroo = findByPropsLazy("jumpToMessage");
|
||||
const RelationshipStore = findByPropsLazy("getRelationships", "isBlocked");
|
||||
|
||||
const isMac = navigator.platform.includes("Mac"); // bruh
|
||||
let replyIdx = -1;
|
||||
let editIdx = -1;
|
||||
let currentlyReplyingId: string | null = null;
|
||||
let currentlyEditingId: string | null = null;
|
||||
|
||||
|
||||
const enum MentionOptions {
|
||||
|
@ -69,36 +70,29 @@ export default definePlugin({
|
|||
|
||||
flux: {
|
||||
DELETE_PENDING_REPLY() {
|
||||
replyIdx = -1;
|
||||
currentlyReplyingId = null;
|
||||
},
|
||||
MESSAGE_END_EDIT() {
|
||||
editIdx = -1;
|
||||
currentlyEditingId = null;
|
||||
},
|
||||
CHANNEL_SELECT() {
|
||||
currentlyReplyingId = null;
|
||||
currentlyEditingId = null;
|
||||
},
|
||||
MESSAGE_START_EDIT: onStartEdit,
|
||||
CREATE_PENDING_REPLY: onCreatePendingReply
|
||||
}
|
||||
});
|
||||
|
||||
function calculateIdx(messages: Message[], id: string) {
|
||||
const idx = messages.findIndex(m => m.id === id);
|
||||
return idx === -1
|
||||
? idx
|
||||
: messages.length - idx - 1;
|
||||
}
|
||||
|
||||
function onStartEdit({ channelId, messageId, _isQuickEdit }: any) {
|
||||
function onStartEdit({ messageId, _isQuickEdit }: any) {
|
||||
if (_isQuickEdit) return;
|
||||
|
||||
const meId = UserStore.getCurrentUser().id;
|
||||
|
||||
const messages = MessageStore.getMessages(channelId)._array.filter(m => m.author.id === meId);
|
||||
editIdx = calculateIdx(messages, messageId);
|
||||
currentlyEditingId = messageId;
|
||||
}
|
||||
|
||||
function onCreatePendingReply({ message, _isQuickReply }: { message: Message; _isQuickReply: boolean; }) {
|
||||
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;
|
||||
|
@ -123,10 +117,10 @@ function jumpIfOffScreen(channelId: string, messageId: string) {
|
|||
|
||||
const vh = Math.max(document.documentElement.clientHeight, window.innerHeight);
|
||||
const rect = element.getBoundingClientRect();
|
||||
const isOffscreen = rect.bottom < 200 || rect.top - vh >= -200;
|
||||
const isOffscreen = rect.bottom < 150 || rect.top - vh >= -150;
|
||||
|
||||
if (isOffscreen) {
|
||||
Kangaroo.jumpToMessage({
|
||||
MessageActions.jumpToMessage({
|
||||
channelId,
|
||||
messageId,
|
||||
flash: false,
|
||||
|
@ -137,44 +131,48 @@ function jumpIfOffScreen(channelId: string, messageId: string) {
|
|||
|
||||
function getNextMessage(isUp: boolean, isReply: boolean) {
|
||||
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")) {
|
||||
messages = messages.filter(m => !RelationshipStore.isBlocked(m.author.id));
|
||||
}
|
||||
const meId = UserStore.getCurrentUser().id;
|
||||
const hasNoBlockedMessages = Vencord.Plugins.isPluginEnabled(NoBlockedMessagesPlugin.name);
|
||||
|
||||
const mutate = (i: number) => isUp
|
||||
? Math.min(messages.length - 1, i + 1)
|
||||
: Math.max(-1, i - 1);
|
||||
messages = messages.filter(m => {
|
||||
if (m.deleted) return false;
|
||||
if (!isReply && m.author.id !== meId) return false; // editing only own messages
|
||||
if (hasNoBlockedMessages && NoBlockedMessagesPlugin.shouldIgnoreMessage(m)) return false;
|
||||
|
||||
const findNextNonDeleted = (i: number) => {
|
||||
do {
|
||||
i = mutate(i);
|
||||
} while (i !== -1 && messages[messages.length - i - 1]?.deleted === true);
|
||||
return i;
|
||||
return true;
|
||||
});
|
||||
|
||||
const findNextNonDeleted = (id: string | null) => {
|
||||
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)
|
||||
replyIdx = i = findNextNonDeleted(replyIdx);
|
||||
else
|
||||
editIdx = i = findNextNonDeleted(editIdx);
|
||||
|
||||
return i === - 1 ? undefined : messages[messages.length - i - 1];
|
||||
if (isReply) {
|
||||
const msg = findNextNonDeleted(currentlyReplyingId);
|
||||
currentlyReplyingId = msg?.id ?? null;
|
||||
return msg;
|
||||
} else {
|
||||
const msg = findNextNonDeleted(currentlyEditingId);
|
||||
currentlyEditingId = msg?.id ?? null;
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
|
||||
function shouldMention(message) {
|
||||
const { enabled, userList, shouldPingListed } = Settings.plugins.NoReplyMention;
|
||||
const shouldPing = !enabled || (shouldPingListed === userList.includes(message.author.id));
|
||||
|
||||
function shouldMention(message: Message) {
|
||||
switch (settings.store.shouldMention) {
|
||||
case MentionOptions.NO_REPLY_MENTION_PLUGIN: return shouldPing;
|
||||
case MentionOptions.DISABLED: return false;
|
||||
default: return true;
|
||||
case MentionOptions.NO_REPLY_MENTION_PLUGIN:
|
||||
if (!Vencord.Plugins.isPluginEnabled(NoReplyMentionPlugin.name)) 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) {
|
||||
const currChannel = ChannelStore.getChannel(SelectedChannelStore.getChannelId());
|
||||
if (currChannel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, currChannel)) return;
|
||||
|
||||
const message = getNextMessage(isUp, true);
|
||||
|
||||
if (!message)
|
||||
if (!message) {
|
||||
return void Dispatcher.dispatch({
|
||||
type: "DELETE_PENDING_REPLY",
|
||||
channelId: SelectedChannelStore.getChannelId(),
|
||||
});
|
||||
}
|
||||
|
||||
const channel = ChannelStore.getChannel(message.channel_id);
|
||||
const meId = UserStore.getCurrentUser().id;
|
||||
|
||||
|
@ -200,6 +201,7 @@ function nextReply(isUp: boolean) {
|
|||
showMentionToggle: !channel.isPrivate() && message.author.id !== meId,
|
||||
_isQuickReply: true
|
||||
});
|
||||
|
||||
ComponentDispatch.dispatchToLastSubscribed("TEXTAREA_FOCUS");
|
||||
jumpIfOffScreen(channel.id, message.id);
|
||||
}
|
||||
|
@ -210,11 +212,13 @@ function nextEdit(isUp: boolean) {
|
|||
if (currChannel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, currChannel)) return;
|
||||
const message = getNextMessage(isUp, false);
|
||||
|
||||
if (!message)
|
||||
if (!message) {
|
||||
return Dispatcher.dispatch({
|
||||
type: "MESSAGE_END_EDIT",
|
||||
channelId: SelectedChannelStore.getChannelId()
|
||||
});
|
||||
}
|
||||
|
||||
Dispatcher.dispatch({
|
||||
type: "MESSAGE_START_EDIT",
|
||||
channelId: message.channel_id,
|
||||
|
@ -222,5 +226,6 @@ function nextEdit(isUp: boolean) {
|
|||
content: message.content,
|
||||
_isQuickEdit: true
|
||||
});
|
||||
|
||||
jumpIfOffScreen(message.channel_id, message.id);
|
||||
}
|
||||
|
|
|
@ -53,14 +53,12 @@ function makeSearchItem(src: string) {
|
|||
<Flex style={{ alignItems: "center", gap: "0.5em" }}>
|
||||
<img
|
||||
style={{
|
||||
borderRadius: i >= 3 // Do not round Google, Yandex & SauceNAO
|
||||
? "50%"
|
||||
: void 0
|
||||
borderRadius: "50%",
|
||||
}}
|
||||
aria-hidden="true"
|
||||
height={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}
|
||||
</Flex>
|
||||
|
|
|
@ -530,6 +530,7 @@ export default definePlugin({
|
|||
|
||||
if (channel.channelId != null) channel = ChannelStore.getChannel(channel.channelId);
|
||||
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);
|
||||
} catch (e) {
|
||||
|
|
|
@ -86,5 +86,5 @@ export default definePlugin({
|
|||
</TooltipContainer>
|
||||
)}
|
||||
</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
|
||||
match: /\.map\((\i)=>\i\.\i\.getName\(\i,\i\.id,\1\)\)/,
|
||||
match: /\.map\((\i)=>\i\.\i\.getName\(\i(?:\.guild_id)?,\i\.id,\1\)\)/,
|
||||
replace: ""
|
||||
},
|
||||
{
|
||||
|
|
|
@ -132,10 +132,16 @@ export default definePlugin({
|
|||
|
||||
{
|
||||
find: "Copy image not supported",
|
||||
replacement: {
|
||||
match: /(?<=(?:canSaveImage|canCopyImage)\((\i,\i)?\)\{.{0,150})!\i\.isPlatformEmbedded/g,
|
||||
replace: "false"
|
||||
}
|
||||
replacement: [
|
||||
{
|
||||
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
|
||||
{
|
||||
|
@ -147,7 +153,7 @@ export default definePlugin({
|
|||
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"
|
||||
},
|
||||
{
|
||||
|
|
|
@ -25,8 +25,8 @@ export default definePlugin({
|
|||
replace: ";b=AS:800000;level-asymmetry-allowed=1"
|
||||
},
|
||||
{
|
||||
match: "useinbandfec=1",
|
||||
replace: "useinbandfec=1;stereo=1;sprop-stereo=1"
|
||||
match: /;usedtx=".concat\((\i)\?"0":"1"\)/,
|
||||
replace: '$&.concat($1?";stereo=1;sprop-stereo=1":"")'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@ export interface Dev {
|
|||
*/
|
||||
export const Devs = /* #__PURE__*/ Object.freeze({
|
||||
Ven: {
|
||||
name: "Vee",
|
||||
name: "V",
|
||||
id: 343383572805058560n
|
||||
},
|
||||
Arjix: {
|
||||
|
@ -211,7 +211,7 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
|||
},
|
||||
axyie: {
|
||||
name: "'ax",
|
||||
id: 273562710745284628n
|
||||
id: 929877747151548487n,
|
||||
},
|
||||
pointy: {
|
||||
name: "pointy",
|
||||
|
@ -604,7 +604,11 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
|||
},
|
||||
samsam: {
|
||||
name: "samsam",
|
||||
id: 836452332387565589n,
|
||||
id: 400482410279469056n,
|
||||
},
|
||||
Cootshk: {
|
||||
name: "Cootshk",
|
||||
id: 921605971577548820n
|
||||
},
|
||||
} satisfies Record<string, Dev>);
|
||||
|
||||
|
@ -1078,6 +1082,10 @@ export const EquicordDevs = Object.freeze({
|
|||
name: "bbgaming25k",
|
||||
id: 851222385528274964n,
|
||||
},
|
||||
davidkra230: {
|
||||
name: "davidkra230",
|
||||
id: 652699312631054356n,
|
||||
},
|
||||
GroupXyz: {
|
||||
name: "GroupXyz",
|
||||
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 "./clipboard";
|
||||
export * from "./constants";
|
||||
export * from "./cspViolations";
|
||||
export * from "./discord";
|
||||
export * from "./guards";
|
||||
export * from "./intlHash";
|
||||
|
|
|
@ -39,7 +39,7 @@ async function initSystemValues() {
|
|||
createStyle("vencord-os-theme-values").textContent = `:root{${variables}}`;
|
||||
}
|
||||
|
||||
export async function toggle(isEnabled: boolean) {
|
||||
async function toggle(isEnabled: boolean) {
|
||||
if (!style) {
|
||||
if (isEnabled) {
|
||||
style = createStyle("vencord-custom-css");
|
||||
|
@ -92,6 +92,8 @@ async function initThemes() {
|
|||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
if (IS_USERSCRIPT) return;
|
||||
|
||||
initSystemValues();
|
||||
initThemes();
|
||||
|
||||
|
@ -104,9 +106,11 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||
if (!IS_WEB) {
|
||||
VencordNative.quickCss.addThemeChangeListener(initThemes);
|
||||
}
|
||||
});
|
||||
}, { once: true });
|
||||
|
||||
export function initQuickCssThemeStore() {
|
||||
if (IS_USERSCRIPT) return;
|
||||
|
||||
initThemes();
|
||||
|
||||
let currentTheme = ThemeStore.theme;
|
||||
|
|
|
@ -53,3 +53,11 @@ export function chooseFile(mimeTypes: string) {
|
|||
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 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 === [", {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue