mirror of
https://github.com/Equicord/Equicord.git
synced 2025-02-20 15:18:50 -05:00
Merge branch 'dev' into Combats-Corner
This commit is contained in:
commit
92fccecdc2
48 changed files with 1755 additions and 1275 deletions
22
.github/workflows/build.yml
vendored
22
.github/workflows/build.yml
vendored
|
@ -15,25 +15,9 @@ env:
|
|||
GITHUB_TOKEN: ${{ secrets.ETOKEN }}
|
||||
|
||||
jobs:
|
||||
DetermineRunner:
|
||||
name: Determine Runner
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
runner: ${{ steps.set-runner.outputs.runner }}
|
||||
steps:
|
||||
- name: Determine which runner to use
|
||||
id: set-runner
|
||||
uses: benjaminmichaelis/get-soonest-available-runner@v1.1.0
|
||||
with:
|
||||
primary-runner: "self-hosted"
|
||||
fallback-runner: "ubuntu-latest"
|
||||
min-available-runners: 1
|
||||
github-token: ${{ env.GITHUB_TOKEN }}
|
||||
|
||||
Build:
|
||||
name: Build Equicord
|
||||
needs: DetermineRunner
|
||||
runs-on: ${{ needs.DetermineRunner.outputs.runner}}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
@ -89,12 +73,10 @@ jobs:
|
|||
rm release/*.map
|
||||
|
||||
- name: Upload Equicord
|
||||
if: github.repository == 'Equicord/Equicord'
|
||||
run: |
|
||||
gh release upload latest --clobber dist/release/*
|
||||
|
||||
- name: Upload Plugins JSON to Ignore repo
|
||||
if: github.repository == 'Equicord/Equicord'
|
||||
- name: Upload Plugins JSON to Equibored repo
|
||||
run: |
|
||||
git config --global user.name "$USERNAME"
|
||||
git config --global user.email "78185467+thororen1234@users.noreply.github.com"
|
||||
|
|
21
.github/workflows/codeberg-mirror.yml
vendored
21
.github/workflows/codeberg-mirror.yml
vendored
|
@ -9,28 +9,9 @@ on:
|
|||
- cron: "0 */6 * * *"
|
||||
|
||||
jobs:
|
||||
DetermineRunner:
|
||||
name: Determine Runner
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
runner: ${{ steps.set-runner.outputs.runner }}
|
||||
steps:
|
||||
- name: Determine which runner to use
|
||||
id: set-runner
|
||||
uses: benjaminmichaelis/get-soonest-available-runner@v1.1.0
|
||||
with:
|
||||
primary-runner: "self-hosted"
|
||||
fallback-runner: "ubuntu-latest"
|
||||
min-available-runners: 1
|
||||
github-token: ${{ env.GITHUB_TOKEN }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ETOKEN }}
|
||||
|
||||
codeberg:
|
||||
name: Sync Codeberg and Github
|
||||
if: github.repository == 'Equicord/Equicord'
|
||||
needs: DetermineRunner
|
||||
runs-on: ${{ needs.DetermineRunner.outputs.runner }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
|
|
1
.github/workflows/reportBrokenPlugins.yml
vendored
1
.github/workflows/reportBrokenPlugins.yml
vendored
|
@ -8,7 +8,6 @@ on:
|
|||
jobs:
|
||||
TestPlugins:
|
||||
name: Test Patches
|
||||
if: github.repository == 'Equicord/Equicord'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
|
36
.github/workflows/syncDev.yml
vendored
36
.github/workflows/syncDev.yml
vendored
|
@ -1,25 +1,25 @@
|
|||
name: Sync Vencord Dev
|
||||
|
||||
env:
|
||||
WORKFLOW_TOKEN: ${{ secrets.ETOKEN }}
|
||||
UPSTREAM_URL: "https://github.com/Vendicated/Vencord.git"
|
||||
UPSTREAM_BRANCH: "dev"
|
||||
DOWNSTREAM_BRANCH: "dev"
|
||||
WORKFLOW_TOKEN: ${{ secrets.ETOKEN }}
|
||||
UPSTREAM_URL: "https://github.com/Vendicated/Vencord.git"
|
||||
UPSTREAM_BRANCH: "dev"
|
||||
DOWNSTREAM_BRANCH: "dev"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 * * * *"
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 * * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Sync Vencord Dev
|
||||
id: sync
|
||||
uses: verticalsync/sync-upstream-repo@master
|
||||
with:
|
||||
upstream_repo: ${{ env.UPSTREAM_URL }}
|
||||
upstream_branch: ${{ env.UPSTREAM_BRANCH }}
|
||||
downstream_branch: ${{ env.DOWNSTREAM_BRANCH }}
|
||||
token: ${{ env.WORKFLOW_TOKEN }}
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Sync Vencord Dev
|
||||
id: sync
|
||||
uses: verticalsync/sync-upstream-repo@master
|
||||
with:
|
||||
upstream_repo: ${{ env.UPSTREAM_URL }}
|
||||
upstream_branch: ${{ env.UPSTREAM_BRANCH }}
|
||||
downstream_branch: ${{ env.DOWNSTREAM_BRANCH }}
|
||||
token: ${{ env.WORKFLOW_TOKEN }}
|
||||
|
|
36
.github/workflows/syncMain.yml
vendored
36
.github/workflows/syncMain.yml
vendored
|
@ -1,25 +1,25 @@
|
|||
name: Sync Vencord Main
|
||||
|
||||
env:
|
||||
WORKFLOW_TOKEN: ${{ secrets.ETOKEN }}
|
||||
UPSTREAM_URL: "https://github.com/Vendicated/Vencord.git"
|
||||
UPSTREAM_BRANCH: "main"
|
||||
DOWNSTREAM_BRANCH: "main"
|
||||
WORKFLOW_TOKEN: ${{ secrets.ETOKEN }}
|
||||
UPSTREAM_URL: "https://github.com/Vendicated/Vencord.git"
|
||||
UPSTREAM_BRANCH: "main"
|
||||
DOWNSTREAM_BRANCH: "main"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 * * * *"
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 * * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Sync Vencord Main
|
||||
id: sync
|
||||
uses: verticalsync/sync-upstream-repo@master
|
||||
with:
|
||||
upstream_repo: ${{ env.UPSTREAM_URL }}
|
||||
upstream_branch: ${{ env.UPSTREAM_BRANCH }}
|
||||
downstream_branch: ${{ env.DOWNSTREAM_BRANCH }}
|
||||
token: ${{ env.WORKFLOW_TOKEN }}
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Sync Vencord Main
|
||||
id: sync
|
||||
uses: verticalsync/sync-upstream-repo@master
|
||||
with:
|
||||
upstream_repo: ${{ env.UPSTREAM_URL }}
|
||||
upstream_branch: ${{ env.UPSTREAM_BRANCH }}
|
||||
downstream_branch: ${{ env.DOWNSTREAM_BRANCH }}
|
||||
token: ${{ env.WORKFLOW_TOKEN }}
|
||||
|
|
21
.github/workflows/test.yml
vendored
21
.github/workflows/test.yml
vendored
|
@ -10,29 +10,10 @@ on:
|
|||
- dev
|
||||
|
||||
jobs:
|
||||
DetermineRunner:
|
||||
name: Determine Runner
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
runner: ${{ steps.set-runner.outputs.runner }}
|
||||
steps:
|
||||
- name: Determine which runner to use
|
||||
id: set-runner
|
||||
uses: benjaminmichaelis/get-soonest-available-runner@v1.1.0
|
||||
with:
|
||||
primary-runner: "self-hosted"
|
||||
fallback-runner: "ubuntu-latest"
|
||||
min-available-runners: 1
|
||||
github-token: ${{ env.GITHUB_TOKEN }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ETOKEN }}
|
||||
|
||||
Test:
|
||||
name: Test For Pushes
|
||||
needs: DetermineRunner
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
runs-on: ${{ needs.DetermineRunner.outputs.runner}}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
|
13
README.md
13
README.md
|
@ -23,7 +23,7 @@ An enhanced version of [Vencord](https://github.com/Vendicated/Vencord) by [Vend
|
|||
- Request for plugins from Discord.
|
||||
|
||||
<details>
|
||||
<summary>Extra included plugins (119 additional plugins)</summary>
|
||||
<summary>Extra included plugins (122 additional plugins)</summary>
|
||||
|
||||
- AllCallTimers by MaxHerbold and D3SOX
|
||||
- AltKrispSwitch by newwares
|
||||
|
@ -82,27 +82,29 @@ An enhanced version of [Vencord](https://github.com/Vendicated/Vencord) by [Vend
|
|||
- InRole by nin0dev
|
||||
- IrcColors by Grzesiek11
|
||||
- IRememberYou by zoodogood
|
||||
- Jumpscare by Surgedevs
|
||||
- JumpToStart by Samwich
|
||||
- KeyboardSounds by HypedDomi
|
||||
- KeywordNotify by camila314 (maintained by thororen)
|
||||
- LoginWithQR by nexpid
|
||||
- MediaDownloader by Colorman
|
||||
- MediaPlaybackSpeed by D3SOX
|
||||
- Meow by Samwich
|
||||
- MessageColors by Hen
|
||||
- MessageLinkTooltip by Kyuuhachi
|
||||
- MessageLoggerEnhanced by Aria
|
||||
- MessageTranslate by Samwich
|
||||
- ModalFade by Kyuuhachi
|
||||
- MusicTitleRPC by Blackilykay
|
||||
- NewPluginsManager by Sqaaakoi
|
||||
- noAppsAllowed by kvba
|
||||
- NoBulletPoints by Samwich
|
||||
- NoDefaultEmojis by Samwich
|
||||
- NoDeleteSafety by Samwich
|
||||
- NoMirroredCamera by Nyx
|
||||
- NoModalAnimation by AutumnVN
|
||||
- NoNitroUpsell by thororen
|
||||
- NoRoleHeaders by Samwich
|
||||
- NotificationTitle by Kyuuhachi
|
||||
- NotifyUserChanges by D3SOX
|
||||
- OnePingPerDM by ProffDea
|
||||
- PlatformSpoofer by Drag
|
||||
- PurgeMessages by bhop and nyx
|
||||
|
@ -117,7 +119,7 @@ An enhanced version of [Vencord](https://github.com/Vendicated/Vencord) by [Vend
|
|||
- SearchFix by Jaxx
|
||||
- SekaiStickers by MaiKokain
|
||||
- ServerSearch by camila314
|
||||
- Shakespearean by vmohammad
|
||||
- Shakespearean by vmohammad (Dev build only)
|
||||
- ShowBadgesInChat by Inbestigator and KrystalSkull
|
||||
- Slap by Korbo
|
||||
- SoundBoardLogger by Moxxie, fres, echo (maintained by thororen)
|
||||
|
@ -132,13 +134,14 @@ An enhanced version of [Vencord](https://github.com/Vendicated/Vencord) by [Vend
|
|||
- Translate+ by Prince527 (Using Translate by Ven)
|
||||
- UnitConverter by sadan
|
||||
- UnlimitedAccounts by thororen
|
||||
- UnreadCountBadge by Joona
|
||||
- UserPFP by nexpid (maintained by thororen)
|
||||
- UwUifier by echo
|
||||
- VCSupport by thororen
|
||||
- VencordRPC by AutumnVN
|
||||
- VideoSpeed by Samwich
|
||||
- ViewRaw2 by Kyuuhachi
|
||||
- VoiceChatUtilities by Dams and D3SOX
|
||||
- VoiceChatUtilities by D3SOX
|
||||
- WebpackTarball by Kyuuhachi
|
||||
- WhosWatching by fres
|
||||
- WigglyText by nexpid
|
||||
|
|
|
@ -36,7 +36,7 @@ const commonOptions: esbuild.BuildOptions = {
|
|||
external: ["~plugins", "~git-hash", "/assets/*"],
|
||||
plugins: [
|
||||
globPlugins("web"),
|
||||
...commonOpts.plugins,
|
||||
...commonOpts.plugins
|
||||
],
|
||||
target: ["esnext"],
|
||||
define: {
|
||||
|
|
|
@ -35,21 +35,6 @@ for (const variable of ["DISCORD_TOKEN", "CHROMIUM_BIN"]) {
|
|||
|
||||
const CANARY = process.env.USE_CANARY === "true";
|
||||
|
||||
const browser = await pup.launch({
|
||||
headless: true,
|
||||
executablePath: process.env.CHROMIUM_BIN
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36");
|
||||
await page.setBypassCSP(true);
|
||||
|
||||
async function maybeGetError(handle: JSHandle): Promise<string | undefined> {
|
||||
return await (handle as JSHandle<Error>)?.getProperty("message")
|
||||
.then(m => m?.jsonValue())
|
||||
.catch(() => undefined);
|
||||
}
|
||||
|
||||
const report = {
|
||||
badPatches: [] as {
|
||||
plugin: string;
|
||||
|
@ -181,123 +166,11 @@ async function printReport() {
|
|||
}
|
||||
}
|
||||
|
||||
page.on("console", async e => {
|
||||
const level = e.type();
|
||||
const rawArgs = e.args();
|
||||
|
||||
async function getText() {
|
||||
try {
|
||||
return await Promise.all(
|
||||
e.args().map(async a => {
|
||||
return await maybeGetError(a) || await a.jsonValue();
|
||||
})
|
||||
).then(a => a.join(" ").trim());
|
||||
} catch {
|
||||
return e.text();
|
||||
}
|
||||
}
|
||||
|
||||
const firstArg = await rawArgs[0]?.jsonValue();
|
||||
|
||||
const isEquicord = firstArg === "[Equicord]";
|
||||
const isDebug = firstArg === "[PUP_DEBUG]";
|
||||
|
||||
outer:
|
||||
if (isEquicord) {
|
||||
try {
|
||||
var args = await Promise.all(e.args().map(a => a.jsonValue()));
|
||||
} catch {
|
||||
break outer;
|
||||
}
|
||||
|
||||
const [, tag, message, otherMessage] = args as Array<string>;
|
||||
|
||||
switch (tag) {
|
||||
case "WebpackInterceptor:":
|
||||
const patchFailMatch = message.match(/Patch by (.+?) (had no effect|errored|found no module) \(Module id is (.+?)\): (.+)/)!;
|
||||
if (!patchFailMatch) break;
|
||||
|
||||
console.error(await getText());
|
||||
process.exitCode = 1;
|
||||
|
||||
const [, plugin, type, id, regex] = patchFailMatch;
|
||||
report.badPatches.push({
|
||||
plugin,
|
||||
type,
|
||||
id,
|
||||
match: regex.replace(/\[A-Za-z_\$\]\[\\w\$\]\*/g, "\\i"),
|
||||
error: await maybeGetError(e.args()[3])
|
||||
});
|
||||
|
||||
break;
|
||||
case "PluginManager:":
|
||||
const failedToStartMatch = message.match(/Failed to start (.+)/);
|
||||
if (!failedToStartMatch) break;
|
||||
|
||||
console.error(await getText());
|
||||
process.exitCode = 1;
|
||||
|
||||
const [, name] = failedToStartMatch;
|
||||
report.badStarts.push({
|
||||
plugin: name,
|
||||
error: await maybeGetError(e.args()[3]) ?? "Unknown error"
|
||||
});
|
||||
|
||||
break;
|
||||
case "LazyChunkLoader:":
|
||||
console.error(await getText());
|
||||
|
||||
switch (message) {
|
||||
case "A fatal error occurred:":
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
break;
|
||||
case "Reporter:":
|
||||
console.error(await getText());
|
||||
|
||||
switch (message) {
|
||||
case "A fatal error occurred:":
|
||||
process.exit(1);
|
||||
case "Webpack Find Fail:":
|
||||
process.exitCode = 1;
|
||||
report.badWebpackFinds.push(otherMessage);
|
||||
break;
|
||||
case "Finished test":
|
||||
await browser.close();
|
||||
await printReport();
|
||||
process.exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isDebug) {
|
||||
console.error(await getText());
|
||||
} else if (level === "error") {
|
||||
const text = await getText();
|
||||
|
||||
if (text.length && !text.startsWith("Failed to load resource: the server responded with a status of") && !text.includes("Webpack")) {
|
||||
if (IGNORED_DISCORD_ERRORS.some(regex => text.match(regex))) {
|
||||
report.ignoredErrors.push(text);
|
||||
} else {
|
||||
console.error("[Unexpected Error]", text);
|
||||
report.otherErrors.push(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
page.on("error", e => console.error("[Error]", e.message));
|
||||
page.on("pageerror", e => {
|
||||
if (e.message.includes("Sentry successfully disabled")) return;
|
||||
|
||||
if (!e.message.startsWith("Object") && !e.message.includes("Cannot find module")) {
|
||||
console.error("[Page Error]", e.message);
|
||||
report.otherErrors.push(e.message);
|
||||
} else {
|
||||
report.ignoredErrors.push(e.message);
|
||||
}
|
||||
});
|
||||
async function maybeGetError(handle: JSHandle): Promise<string | undefined> {
|
||||
return await (handle as JSHandle<Error>)?.getProperty("message")
|
||||
.then(m => m?.jsonValue())
|
||||
.catch(() => undefined);
|
||||
}
|
||||
|
||||
async function reporterRuntime(token: string) {
|
||||
Vencord.Webpack.waitFor(
|
||||
|
@ -309,11 +182,144 @@ async function reporterRuntime(token: string) {
|
|||
);
|
||||
}
|
||||
|
||||
await page.evaluateOnNewDocument(`
|
||||
if (location.host.endsWith("discord.com")) {
|
||||
${readFileSync("./dist/browser/browser.js", "utf-8")};
|
||||
(${reporterRuntime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)});
|
||||
}
|
||||
`);
|
||||
try {
|
||||
const browser = await pup.launch({
|
||||
headless: true,
|
||||
executablePath: process.env.CHROMIUM_BIN
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
await page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36");
|
||||
await page.setBypassCSP(true);
|
||||
|
||||
await page.goto(CANARY ? "https://canary.discord.com/login" : "https://discord.com/login");
|
||||
page.on("console", async e => {
|
||||
const level = e.type();
|
||||
const rawArgs = e.args();
|
||||
|
||||
async function getText() {
|
||||
try {
|
||||
return await Promise.all(
|
||||
e.args().map(async a => {
|
||||
return await maybeGetError(a) || await a.jsonValue();
|
||||
})
|
||||
).then(a => a.join(" ").trim());
|
||||
} catch {
|
||||
return e.text();
|
||||
}
|
||||
}
|
||||
|
||||
const firstArg = await rawArgs[0]?.jsonValue();
|
||||
|
||||
const isEquicord = firstArg === "[Equicord]";
|
||||
const isDebug = firstArg === "[PUP_DEBUG]";
|
||||
|
||||
outer:
|
||||
if (isEquicord) {
|
||||
try {
|
||||
var args = await Promise.all(e.args().map(a => a.jsonValue()));
|
||||
} catch {
|
||||
break outer;
|
||||
}
|
||||
|
||||
const [, tag, message, otherMessage] = args as Array<string>;
|
||||
|
||||
switch (tag) {
|
||||
case "WebpackInterceptor:":
|
||||
const patchFailMatch = message.match(/Patch by (.+?) (had no effect|errored|found no module) \(Module id is (.+?)\): (.+)/)!;
|
||||
if (!patchFailMatch) break;
|
||||
|
||||
console.error(await getText());
|
||||
process.exitCode = 1;
|
||||
|
||||
const [, plugin, type, id, regex] = patchFailMatch;
|
||||
report.badPatches.push({
|
||||
plugin,
|
||||
type,
|
||||
id,
|
||||
match: regex.replace(/\[A-Za-z_\$\]\[\\w\$\]\*/g, "\\i"),
|
||||
error: await maybeGetError(e.args()[3])
|
||||
});
|
||||
|
||||
break;
|
||||
case "PluginManager:":
|
||||
const failedToStartMatch = message.match(/Failed to start (.+)/);
|
||||
if (!failedToStartMatch) break;
|
||||
|
||||
console.error(await getText());
|
||||
process.exitCode = 1;
|
||||
|
||||
const [, name] = failedToStartMatch;
|
||||
report.badStarts.push({
|
||||
plugin: name,
|
||||
error: await maybeGetError(e.args()[3]) ?? "Unknown error"
|
||||
});
|
||||
|
||||
break;
|
||||
case "LazyChunkLoader:":
|
||||
console.error(await getText());
|
||||
|
||||
switch (message) {
|
||||
case "A fatal error occurred:":
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
break;
|
||||
case "Reporter:":
|
||||
console.error(await getText());
|
||||
|
||||
switch (message) {
|
||||
case "A fatal error occurred:":
|
||||
process.exit(1);
|
||||
case "Webpack Find Fail:":
|
||||
process.exitCode = 1;
|
||||
report.badWebpackFinds.push(otherMessage);
|
||||
break;
|
||||
case "Finished test":
|
||||
await browser.close();
|
||||
await printReport();
|
||||
process.exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isDebug) {
|
||||
console.error(await getText());
|
||||
} else if (level === "error") {
|
||||
const text = await getText();
|
||||
|
||||
if (text.length && !text.startsWith("Failed to load resource: the server responded with a status of") && !text.includes("Webpack")) {
|
||||
if (IGNORED_DISCORD_ERRORS.some(regex => text.match(regex))) {
|
||||
report.ignoredErrors.push(text);
|
||||
} else {
|
||||
console.error("[Unexpected Error]", text);
|
||||
report.otherErrors.push(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
page.on("error", e => console.error("[Error]", e.message));
|
||||
page.on("pageerror", e => {
|
||||
if (e.message.includes("Sentry successfully disabled")) return;
|
||||
if (!e.message.startsWith("Object") && !e.message.includes("Cannot find module")) {
|
||||
console.error("[Page Error]", e.message);
|
||||
report.otherErrors.push(e.message);
|
||||
} else {
|
||||
report.ignoredErrors.push(e.message);
|
||||
}
|
||||
});
|
||||
|
||||
await page.evaluateOnNewDocument(`
|
||||
if (location.host.endsWith("discord.com")) {
|
||||
${readFileSync("./dist/browser/browser.js", "utf-8")};
|
||||
(${reporterRuntime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)});
|
||||
}
|
||||
`);
|
||||
|
||||
await page.goto(CANARY ? "https://canary.discord.com/login" : "https://discord.com/login");
|
||||
|
||||
await printReport();
|
||||
await browser.close();
|
||||
} catch (error) {
|
||||
console.error("An error occurred:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
|
|
@ -1,73 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { findComponentByCodeLazy } from "@webpack";
|
||||
import { moment, React, useMemo } from "@webpack/common";
|
||||
import { User } from "discord-types/general";
|
||||
|
||||
import { Activity, Application } from "../types";
|
||||
import {
|
||||
formatElapsedTime,
|
||||
getActivityImage,
|
||||
getApplicationIcons,
|
||||
getValidStartTimeStamp,
|
||||
getValidTimestamps
|
||||
} from "../utils";
|
||||
|
||||
const TimeBar = findComponentByCodeLazy<{
|
||||
start: number;
|
||||
end: number;
|
||||
themed: boolean;
|
||||
className: string;
|
||||
}>("isSingleLine");
|
||||
|
||||
interface ActivityTooltipProps {
|
||||
activity: Activity;
|
||||
application?: Application;
|
||||
user: User;
|
||||
cl: ReturnType<typeof import("@api/Styles").classNameFactory>;
|
||||
}
|
||||
|
||||
export default function ActivityTooltip({ activity, application, user, cl }: Readonly<ActivityTooltipProps>) {
|
||||
const image = useMemo(() => {
|
||||
const activityImage = getActivityImage(activity, application);
|
||||
if (activityImage) {
|
||||
return activityImage;
|
||||
}
|
||||
const icon = getApplicationIcons([activity], true)[0];
|
||||
return icon?.image.src;
|
||||
}, [activity]);
|
||||
const timestamps = useMemo(() => getValidTimestamps(activity), [activity]);
|
||||
const startTime = useMemo(() => getValidStartTimeStamp(activity), [activity]);
|
||||
|
||||
const hasDetails = activity.details ?? activity.state;
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<div className={cl("activity")}>
|
||||
{image && <img className={cl("activity-image")} src={image} alt="Activity logo" />}
|
||||
<div className={cl("activity-title")}>{activity.name}</div>
|
||||
{hasDetails && <div className={cl("activity-divider")} />}
|
||||
<div className={cl("activity-details")}>
|
||||
<div>{activity.details}</div>
|
||||
<div>{activity.state}</div>
|
||||
{!timestamps && startTime &&
|
||||
<div className={cl("activity-time-bar")}>
|
||||
{formatElapsedTime(moment(startTime / 1000), moment())}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
{timestamps && (
|
||||
<TimeBar start={timestamps.start}
|
||||
end={timestamps.end}
|
||||
themed={false}
|
||||
className={cl("activity-time-bar")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
|
@ -1,51 +1,234 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./styles.css";
|
||||
|
||||
import { migratePluginSettings } from "@api/Settings";
|
||||
import { definePluginSettings, migratePluginSettings } from "@api/Settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import { findComponentByCodeLazy } from "@webpack";
|
||||
import { PresenceStore, React, Tooltip, useStateFromStores } from "@webpack/common";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
|
||||
import { PresenceStore, React, Tooltip, useEffect, useMemo, useState, useStateFromStores } from "@webpack/common";
|
||||
import { User } from "discord-types/general";
|
||||
|
||||
import ActivityTooltip from "./components/ActivityTooltip";
|
||||
import { Caret } from "./components/Caret";
|
||||
import { SpotifyIcon } from "./components/SpotifyIcon";
|
||||
import { TwitchIcon } from "./components/TwitchIcon";
|
||||
import settings from "./settings";
|
||||
import { Activity, ActivityListIcon, ActivityViewProps, ApplicationIcon, IconCSSProperties } from "./types";
|
||||
import {
|
||||
getApplicationIcons
|
||||
} from "./utils";
|
||||
import { Activity, ActivityListIcon, Application, ApplicationIcon, IconCSSProperties } from "./types";
|
||||
|
||||
const settings = definePluginSettings({
|
||||
memberList: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Show activity icons in the member list",
|
||||
default: true,
|
||||
restartNeeded: true,
|
||||
},
|
||||
iconSize: {
|
||||
type: OptionType.SLIDER,
|
||||
description: "Size of the activity icons",
|
||||
markers: [10, 15, 20],
|
||||
default: 15,
|
||||
stickToMarkers: false,
|
||||
},
|
||||
specialFirst: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Show special activities first (Currently Spotify and Twitch)",
|
||||
default: true,
|
||||
restartNeeded: false,
|
||||
},
|
||||
renderGifs: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Allow rendering GIFs",
|
||||
default: true,
|
||||
restartNeeded: false,
|
||||
},
|
||||
showAppDescriptions: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Show application descriptions in the activity tooltip",
|
||||
default: true,
|
||||
restartNeeded: false,
|
||||
},
|
||||
divider: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "",
|
||||
component: () => (
|
||||
<div style={{
|
||||
width: "100%",
|
||||
height: 1,
|
||||
borderTop: "thin solid var(--background-modifier-accent)",
|
||||
paddingTop: 5,
|
||||
paddingBottom: 5
|
||||
}} />
|
||||
),
|
||||
},
|
||||
userPopout: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Show all activities in the profile popout/sidebar",
|
||||
default: true,
|
||||
restartNeeded: true,
|
||||
},
|
||||
allActivitiesStyle: {
|
||||
type: OptionType.SELECT,
|
||||
description: "Style for showing all activities",
|
||||
options: [
|
||||
{
|
||||
default: true,
|
||||
label: "Carousel",
|
||||
value: "carousel",
|
||||
},
|
||||
{
|
||||
label: "List",
|
||||
value: "list",
|
||||
},
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const cl = classNameFactory("vc-bactivities-");
|
||||
|
||||
const ActivityView = findComponentByCodeLazy<ActivityViewProps>(",onOpenGameProfileModal:");
|
||||
const ApplicationStore: {
|
||||
getApplication: (id: string) => Application | null;
|
||||
} = findStoreLazy("ApplicationStore");
|
||||
|
||||
const { fetchApplication }: {
|
||||
fetchApplication: (id: string) => Promise<Application | null>;
|
||||
} = findByPropsLazy("fetchApplication");
|
||||
|
||||
const ActivityView = findComponentByCodeLazy<{
|
||||
activity: Activity | null;
|
||||
user: User;
|
||||
application?: Application;
|
||||
type?: string;
|
||||
}>(",onOpenGameProfileModal:");
|
||||
|
||||
// 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");
|
||||
|
||||
const fetchedApplications = new Map<string, Application | null>();
|
||||
|
||||
const xboxUrl = "https://discord.com/assets/9a15d086141be29d9fcd.png"; // TODO: replace with "renderXboxImage"?
|
||||
|
||||
const ActivityTooltip = ({ activity, application, user }: Readonly<{ activity: Activity, application?: Application, user: User; }>) => {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<div className={cl("activity-tooltip")}>
|
||||
<ActivityView
|
||||
activity={activity}
|
||||
user={user}
|
||||
application={application}
|
||||
type="BiteSizePopout"
|
||||
/>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
function getActivityApplication({ application_id }: Activity) {
|
||||
if (!application_id) return undefined;
|
||||
let application = ApplicationStore.getApplication(application_id);
|
||||
if (!application && fetchedApplications.has(application_id)) {
|
||||
application = fetchedApplications.get(application_id) ?? null;
|
||||
}
|
||||
return application ?? undefined;
|
||||
}
|
||||
|
||||
function getApplicationIcons(activities: Activity[], preferSmall = false) {
|
||||
const applicationIcons: ApplicationIcon[] = [];
|
||||
const applications = activities.filter(activity => activity.application_id || activity.platform);
|
||||
|
||||
for (const activity of applications) {
|
||||
const { assets, application_id, platform } = activity;
|
||||
if (!application_id && !platform) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (assets) {
|
||||
const addImage = (image: string, alt: string) => {
|
||||
if (image.startsWith("mp:")) {
|
||||
const discordMediaLink = `https://media.discordapp.net/${image.replace(/mp:/, "")}`;
|
||||
if (settings.store.renderGifs || !discordMediaLink.endsWith(".gif")) {
|
||||
applicationIcons.push({
|
||||
image: { src: discordMediaLink, alt },
|
||||
activity
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const src = `https://cdn.discordapp.com/app-assets/${application_id}/${image}.png`;
|
||||
applicationIcons.push({
|
||||
image: { src, alt },
|
||||
activity
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const smallImage = assets.small_image;
|
||||
const smallText = assets.small_text ?? "Small Text";
|
||||
const largeImage = assets.large_image;
|
||||
const largeText = assets.large_text ?? "Large Text";
|
||||
if (preferSmall) {
|
||||
if (smallImage) {
|
||||
addImage(smallImage, smallText);
|
||||
} else if (largeImage) {
|
||||
addImage(largeImage, largeText);
|
||||
}
|
||||
} else {
|
||||
if (largeImage) {
|
||||
addImage(largeImage, largeText);
|
||||
} else if (smallImage) {
|
||||
addImage(smallImage, smallText);
|
||||
}
|
||||
}
|
||||
} else if (application_id) {
|
||||
let application = ApplicationStore.getApplication(application_id);
|
||||
if (!application) {
|
||||
if (fetchedApplications.has(application_id)) {
|
||||
application = fetchedApplications.get(application_id) as Application | null;
|
||||
} else {
|
||||
fetchedApplications.set(application_id, null);
|
||||
fetchApplication(application_id).then(app => {
|
||||
fetchedApplications.set(application_id, app);
|
||||
}).catch(console.error);
|
||||
}
|
||||
}
|
||||
|
||||
if (application) {
|
||||
if (application.icon) {
|
||||
const src = `https://cdn.discordapp.com/app-icons/${application.id}/${application.icon}.png`;
|
||||
applicationIcons.push({
|
||||
image: { src, alt: application.name },
|
||||
activity,
|
||||
application
|
||||
});
|
||||
} else if (platform === "xbox") {
|
||||
applicationIcons.push({
|
||||
image: { src: xboxUrl, alt: "Xbox" },
|
||||
activity,
|
||||
application
|
||||
});
|
||||
}
|
||||
} else if (platform === "xbox") {
|
||||
applicationIcons.push({
|
||||
image: { src: xboxUrl, alt: "Xbox" },
|
||||
activity
|
||||
});
|
||||
}
|
||||
} else if (platform === "xbox") {
|
||||
applicationIcons.push({
|
||||
image: { src: xboxUrl, alt: "Xbox" },
|
||||
activity
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return applicationIcons;
|
||||
}
|
||||
|
||||
migratePluginSettings("BetterActivities", "MemberListActivities");
|
||||
|
||||
export default definePlugin({
|
||||
name: "BetterActivities",
|
||||
description: "Shows activity icons in the member list and allows showing all activities",
|
||||
|
@ -68,12 +251,7 @@ export default definePlugin({
|
|||
for (const appIcon of uniqueIcons) {
|
||||
icons.push({
|
||||
iconElement: <img {...appIcon.image} />,
|
||||
tooltip: <ActivityTooltip
|
||||
activity={appIcon.activity}
|
||||
application={appIcon.application}
|
||||
user={user}
|
||||
cl={cl}
|
||||
/>
|
||||
tooltip: <ActivityTooltip activity={appIcon.activity} application={appIcon.application} user={user} />
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -84,7 +262,7 @@ export default definePlugin({
|
|||
const activity = activities[activityIndex];
|
||||
const iconObject: ActivityListIcon = {
|
||||
iconElement: <IconComponent />,
|
||||
tooltip: <ActivityTooltip activity={activity} user={user} cl={cl} />
|
||||
tooltip: <ActivityTooltip activity={activity} user={user} />
|
||||
};
|
||||
|
||||
if (settings.store.specialFirst) {
|
||||
|
@ -131,8 +309,8 @@ export default definePlugin({
|
|||
return null;
|
||||
},
|
||||
|
||||
showAllActivitiesComponent({ activity, user, guild, channelId, onClose }: ActivityViewProps) {
|
||||
const [currentActivity, setCurrentActivity] = React.useState<Activity | null>(
|
||||
showAllActivitiesComponent({ activity, user, ...props }: Readonly<{ activity: Activity; user: User; application: Application; type: string; }>) {
|
||||
const [currentActivity, setCurrentActivity] = useState<Activity | null>(
|
||||
activity?.type !== 4 ? activity! : null
|
||||
);
|
||||
|
||||
|
@ -140,7 +318,7 @@ export default definePlugin({
|
|||
[PresenceStore], () => PresenceStore.getActivities(user.id).filter((activity: Activity) => activity.type !== 4)
|
||||
) ?? [];
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (!activities.length) {
|
||||
setCurrentActivity(null);
|
||||
return;
|
||||
|
@ -148,75 +326,92 @@ export default definePlugin({
|
|||
|
||||
if (!currentActivity || !activities.includes(currentActivity))
|
||||
setCurrentActivity(activities[0]);
|
||||
|
||||
}, [activities]);
|
||||
|
||||
// we use these for other activities, it would be better to somehow get the corresponding activity props
|
||||
const generalProps = useMemo(() => Object.keys(props).reduce((acc, key) => {
|
||||
// exclude activity specific props to prevent copying them to all activities (e.g. buttons)
|
||||
if (key !== "renderActions" && key !== "application") acc[key] = props[key];
|
||||
return acc;
|
||||
}, {}), [props]);
|
||||
|
||||
if (!activities.length) return null;
|
||||
|
||||
if (settings.store.allActivitiesStyle === "carousel") {
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||
<ActivityView
|
||||
activity={currentActivity}
|
||||
user={user}
|
||||
guild={guild}
|
||||
channelId={channelId}
|
||||
onClose={onClose} />
|
||||
<div
|
||||
className={cl("controls")}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Tooltip text="Left" tooltipClassName={cl("controls-tooltip")}>{({
|
||||
onMouseEnter,
|
||||
onMouseLeave
|
||||
}) => {
|
||||
return <span
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onClick={() => {
|
||||
const index = activities.indexOf(currentActivity!);
|
||||
if (index - 1 >= 0)
|
||||
setCurrentActivity(activities[index - 1]);
|
||||
}}
|
||||
>
|
||||
<Caret
|
||||
disabled={activities.indexOf(currentActivity!) < 1}
|
||||
direction="left" />
|
||||
</span>;
|
||||
}}</Tooltip>
|
||||
{currentActivity?.id === activity?.id ? (
|
||||
<ActivityView
|
||||
activity={currentActivity}
|
||||
user={user}
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
<ActivityView
|
||||
activity={currentActivity}
|
||||
user={user}
|
||||
// fetch optional application
|
||||
application={getActivityApplication(currentActivity!)}
|
||||
{...generalProps}
|
||||
/>
|
||||
)}
|
||||
{activities.length > 1 &&
|
||||
<div
|
||||
className={cl("controls")}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Tooltip text="Left" tooltipClassName={cl("controls-tooltip")}>{({
|
||||
onMouseEnter,
|
||||
onMouseLeave
|
||||
}) => {
|
||||
return <span
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onClick={() => {
|
||||
const index = activities.indexOf(currentActivity!);
|
||||
if (index - 1 >= 0)
|
||||
setCurrentActivity(activities[index - 1]);
|
||||
}}
|
||||
>
|
||||
<Caret
|
||||
disabled={activities.indexOf(currentActivity!) < 1}
|
||||
direction="left" />
|
||||
</span>;
|
||||
}}</Tooltip>
|
||||
|
||||
<div className="carousell">
|
||||
{activities.map((activity, index) => (
|
||||
<div
|
||||
key={"dot--" + index}
|
||||
onClick={() => setCurrentActivity(activity)}
|
||||
className={`dot ${currentActivity === activity ? "selected" : ""}`} />
|
||||
))}
|
||||
<div className="carousel">
|
||||
{activities.map((activity, index) => (
|
||||
<div
|
||||
key={"dot--" + index}
|
||||
onClick={() => setCurrentActivity(activity)}
|
||||
className={`dot ${currentActivity === activity ? "selected" : ""}`} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Tooltip text="Right" tooltipClassName={cl("controls-tooltip")}>{({
|
||||
onMouseEnter,
|
||||
onMouseLeave
|
||||
}) => {
|
||||
return <span
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onClick={() => {
|
||||
const index = activities.indexOf(currentActivity!);
|
||||
if (index + 1 < activities.length)
|
||||
setCurrentActivity(activities[index + 1]);
|
||||
}}
|
||||
>
|
||||
<Caret
|
||||
disabled={activities.indexOf(currentActivity!) >= activities.length - 1}
|
||||
direction="right" />
|
||||
</span>;
|
||||
}}</Tooltip>
|
||||
</div>
|
||||
|
||||
<Tooltip text="Right" tooltipClassName={cl("controls-tooltip")}>{({
|
||||
onMouseEnter,
|
||||
onMouseLeave
|
||||
}) => {
|
||||
return <span
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onClick={() => {
|
||||
const index = activities.indexOf(currentActivity!);
|
||||
if (index + 1 < activities.length)
|
||||
setCurrentActivity(activities[index + 1]);
|
||||
}}
|
||||
>
|
||||
<Caret
|
||||
disabled={activities.indexOf(currentActivity!) >= activities.length - 1}
|
||||
direction="right" />
|
||||
</span>;
|
||||
}}</Tooltip>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
|
@ -228,16 +423,22 @@ export default definePlugin({
|
|||
gap: "5px",
|
||||
}}
|
||||
>
|
||||
{activities.map((activity, index) => (
|
||||
<ActivityView
|
||||
key={index}
|
||||
activity={activity}
|
||||
user={user}
|
||||
guild={guild}
|
||||
channelId={channelId}
|
||||
onClose={onClose}
|
||||
/>
|
||||
))}
|
||||
{activities.map((activity, index) =>
|
||||
index === 0 ? (
|
||||
<ActivityView
|
||||
key={index}
|
||||
activity={activity}
|
||||
user={user}
|
||||
{...props}
|
||||
/>) : (
|
||||
<ActivityView
|
||||
key={index}
|
||||
activity={activity}
|
||||
user={user}
|
||||
application={getActivityApplication(activity)}
|
||||
{...generalProps}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -254,22 +455,13 @@ export default definePlugin({
|
|||
predicate: () => settings.store.memberList,
|
||||
},
|
||||
{
|
||||
// Show all activities in the profile panel
|
||||
find: "{layout:\"DM_PANEL\",",
|
||||
// Show all activities in the user popout/sidebar
|
||||
find: '"UserActivityContainer"',
|
||||
replacement: {
|
||||
match: /(?<=\(0,\i\.jsx\)\()\i\.\i(?=,{activity:.+?,user:\i,channelId:\i.id,)/,
|
||||
replace: "$self.showAllActivitiesComponent"
|
||||
},
|
||||
predicate: () => settings.store.profileSidebar,
|
||||
},
|
||||
{
|
||||
// Show all activities in the user popout
|
||||
find: "customStatusSection,",
|
||||
replacement: {
|
||||
match: /(?<=\(0,\i\.jsx\)\()\i\.\i(?=,{activity:\i,user:\i,guild:\i,channelId:\i,onClose:\i,)/,
|
||||
replace: "$self.showAllActivitiesComponent"
|
||||
match: /(?<=\(0,\i\.jsx\)\()(\i\.\i)(?=,{...(\i),activity:\i,user:\i,application:\i)/,
|
||||
replace: "$2.type==='BiteSizePopout'?$self.showAllActivitiesComponent:$1"
|
||||
},
|
||||
predicate: () => settings.store.userPopout
|
||||
}
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { OptionType } from "@utils/types";
|
||||
import { React } from "@webpack/common";
|
||||
|
||||
const settings = definePluginSettings({
|
||||
memberList: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Show activity icons in the member list",
|
||||
default: true,
|
||||
restartNeeded: true,
|
||||
},
|
||||
iconSize: {
|
||||
type: OptionType.SLIDER,
|
||||
description: "Size of the activity icons",
|
||||
markers: [10, 15, 20],
|
||||
default: 15,
|
||||
stickToMarkers: false,
|
||||
},
|
||||
specialFirst: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Show special activities first (Currently Spotify and Twitch)",
|
||||
default: true,
|
||||
},
|
||||
renderGifs: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Allow rendering GIFs",
|
||||
default: true,
|
||||
},
|
||||
divider: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "",
|
||||
component: () => (
|
||||
<div style={{
|
||||
width: "100%",
|
||||
height: 1,
|
||||
borderTop: "thin solid var(--background-modifier-accent)",
|
||||
paddingTop: 5,
|
||||
paddingBottom: 5
|
||||
}} />
|
||||
),
|
||||
},
|
||||
profileSidebar: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Show all activities in the profile sidebar",
|
||||
default: true,
|
||||
restartNeeded: true,
|
||||
},
|
||||
userPopout: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Show all activities in the user popout",
|
||||
default: true,
|
||||
restartNeeded: true,
|
||||
},
|
||||
allActivitiesStyle: {
|
||||
type: OptionType.SELECT,
|
||||
description: "Style for showing all activities",
|
||||
options: [
|
||||
{
|
||||
default: true,
|
||||
label: "Carousel",
|
||||
value: "carousel",
|
||||
},
|
||||
{
|
||||
label: "List",
|
||||
value: "list",
|
||||
},
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
export default settings;
|
|
@ -19,44 +19,8 @@
|
|||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.vc-bactivities-activity {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.vc-bactivities-activity-title {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.vc-bactivities-activity-image {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.vc-bactivities-activity-divider {
|
||||
width: 100%;
|
||||
border-top: 1px dotted rgb(255 255 255 / 20%);
|
||||
margin-top: 3px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.vc-bactivities-activity-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: var(--text-muted);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.vc-bactivities-activity-time-bar {
|
||||
width: 100%;
|
||||
margin-top: 3px;
|
||||
margin-bottom: 3px;
|
||||
.vc-bactivities-activity-tooltip {
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
.vc-bactivities-caret-left,
|
||||
|
@ -101,12 +65,12 @@
|
|||
background: var(--background-modifier-accent);
|
||||
}
|
||||
|
||||
.vc-bactivities-controls .carousell {
|
||||
.vc-bactivities-controls .carousel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.vc-bactivities-controls .carousell .dot {
|
||||
.vc-bactivities-controls .carousel .dot {
|
||||
margin: 0 4px;
|
||||
width: 10px;
|
||||
cursor: pointer;
|
||||
|
@ -117,11 +81,11 @@
|
|||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.vc-bactivities-controls .carousell .dot:hover:not(.selected) {
|
||||
.vc-bactivities-controls .carousel .dot:hover:not(.selected) {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.vc-bactivities-controls .carousell .dot.selected {
|
||||
.vc-bactivities-controls .carousel .dot.selected {
|
||||
opacity: 1;
|
||||
background: var(--dot-color, var(--brand-500));
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Guild, User } from "discord-types/general";
|
||||
import { CSSProperties, ImgHTMLAttributes } from "react";
|
||||
|
||||
export interface Timestamp {
|
||||
|
@ -81,11 +80,3 @@ export interface ActivityListIcon {
|
|||
export interface IconCSSProperties extends CSSProperties {
|
||||
"--icon-size": string;
|
||||
}
|
||||
|
||||
export interface ActivityViewProps {
|
||||
activity: Activity | null;
|
||||
user: User;
|
||||
guild: Guild;
|
||||
channelId: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
|
|
@ -1,158 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { findByPropsLazy, findStoreLazy } from "@webpack";
|
||||
import { moment } from "@webpack/common";
|
||||
|
||||
import settings from "./settings";
|
||||
import { Activity, Application, ApplicationIcon, Timestamp } from "./types";
|
||||
|
||||
const ApplicationStore: {
|
||||
getApplication: (id: string) => Application | null;
|
||||
} = findStoreLazy("ApplicationStore");
|
||||
|
||||
const { fetchApplication }: {
|
||||
fetchApplication: (id: string) => Promise<Application | null>;
|
||||
} = findByPropsLazy("fetchApplication");
|
||||
|
||||
export function getActivityImage(activity: Activity, application?: Application): string | undefined {
|
||||
if (activity.type === 2 && activity.name === "Spotify") {
|
||||
// get either from large or small image
|
||||
const image = activity.assets?.large_image ?? activity.assets?.small_image;
|
||||
// image needs to replace 'spotify:'
|
||||
if (image?.startsWith("spotify:")) {
|
||||
// spotify cover art is always https://i.scdn.co/image/ID
|
||||
return image.replace("spotify:", "https://i.scdn.co/image/");
|
||||
}
|
||||
}
|
||||
if (activity.type === 1 && activity.name === "Twitch") {
|
||||
const image = activity.assets?.large_image;
|
||||
// image needs to replace 'twitch:'
|
||||
if (image?.startsWith("twitch:")) {
|
||||
// twitch images are always https://static-cdn.jtvnw.net/previews-ttv/live_user_USERNAME-RESOLTUON.jpg
|
||||
return `${image.replace("twitch:", "https://static-cdn.jtvnw.net/previews-ttv/live_user_")}-108x60.jpg`;
|
||||
}
|
||||
}
|
||||
// TODO: we could support other assets here
|
||||
}
|
||||
|
||||
const fetchedApplications = new Map<string, Application | null>();
|
||||
|
||||
// TODO: replace with "renderXboxImage"?
|
||||
const xboxUrl = "https://discord.com/assets/9a15d086141be29d9fcd.png";
|
||||
|
||||
export function getApplicationIcons(activities: Activity[], preferSmall = false) {
|
||||
const applicationIcons: ApplicationIcon[] = [];
|
||||
const applications = activities.filter(activity => activity.application_id || activity.platform);
|
||||
|
||||
for (const activity of applications) {
|
||||
const { assets, application_id, platform } = activity;
|
||||
if (!application_id && !platform) {
|
||||
continue;
|
||||
}
|
||||
if (assets) {
|
||||
|
||||
const addImage = (image: string, alt: string) => {
|
||||
if (image.startsWith("mp:")) {
|
||||
const discordMediaLink = `https://media.discordapp.net/${image.replace(/mp:/, "")}`;
|
||||
if (settings.store.renderGifs || !discordMediaLink.endsWith(".gif")) {
|
||||
applicationIcons.push({
|
||||
image: { src: discordMediaLink, alt },
|
||||
activity
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const src = `https://cdn.discordapp.com/app-assets/${application_id}/${image}.png`;
|
||||
applicationIcons.push({
|
||||
image: { src, alt },
|
||||
activity
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const smallImage = assets.small_image;
|
||||
const smallText = assets.small_text ?? "Small Text";
|
||||
const largeImage = assets.large_image;
|
||||
const largeText = assets.large_text ?? "Large Text";
|
||||
if (preferSmall) {
|
||||
if (smallImage) {
|
||||
addImage(smallImage, smallText);
|
||||
} else if (largeImage) {
|
||||
addImage(largeImage, largeText);
|
||||
}
|
||||
} else {
|
||||
if (largeImage) {
|
||||
addImage(largeImage, largeText);
|
||||
} else if (smallImage) {
|
||||
addImage(smallImage, smallText);
|
||||
}
|
||||
}
|
||||
} else if (application_id) {
|
||||
let application = ApplicationStore.getApplication(application_id);
|
||||
if (!application) {
|
||||
if (fetchedApplications.has(application_id)) {
|
||||
application = fetchedApplications.get(application_id) as Application | null;
|
||||
} else {
|
||||
fetchedApplications.set(application_id, null);
|
||||
fetchApplication(application_id).then(app => {
|
||||
fetchedApplications.set(application_id, app);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (application) {
|
||||
if (application.icon) {
|
||||
const src = `https://cdn.discordapp.com/app-icons/${application.id}/${application.icon}.png`;
|
||||
applicationIcons.push({
|
||||
image: { src, alt: application.name },
|
||||
activity,
|
||||
application
|
||||
});
|
||||
} else if (platform === "xbox") {
|
||||
applicationIcons.push({
|
||||
image: { src: xboxUrl, alt: "Xbox" },
|
||||
activity,
|
||||
application
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (platform === "xbox") {
|
||||
applicationIcons.push({
|
||||
image: { src: xboxUrl, alt: "Xbox" },
|
||||
activity
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return applicationIcons;
|
||||
}
|
||||
|
||||
export function getValidTimestamps(activity: Activity): Required<Timestamp> | null {
|
||||
if (activity.timestamps?.start !== undefined && activity.timestamps?.end !== undefined) {
|
||||
return activity.timestamps as Required<Timestamp>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getValidStartTimeStamp(activity: Activity): number | null {
|
||||
if (activity.timestamps?.start !== undefined) {
|
||||
return activity.timestamps.start;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const customFormat = (momentObj: moment.Moment): string => {
|
||||
const hours = momentObj.hours();
|
||||
const formattedTime = momentObj.format("mm:ss");
|
||||
return hours > 0 ? `${momentObj.format("HH:")}${formattedTime}` : formattedTime;
|
||||
};
|
||||
|
||||
export function formatElapsedTime(startTime: moment.Moment, endTime: moment.Moment): string {
|
||||
const duration = moment.duration(endTime.diff(startTime));
|
||||
return `${customFormat(moment.utc(duration.asMilliseconds()))} elapsed`;
|
||||
}
|
|
@ -291,7 +291,7 @@ function getCSS(fontName) {
|
|||
` : ""}
|
||||
/*Privacy blur*/
|
||||
${Settings.plugins.Glide.privacyBlur ? `
|
||||
.header_ec86aa,
|
||||
.header_f9f2ca,
|
||||
.container_ee69e0,
|
||||
.title_a7d72e,
|
||||
.layout_f9647d,
|
||||
|
@ -300,7 +300,7 @@ function getCSS(fontName) {
|
|||
transition: filter 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
body:not(:hover) .header_ec86aa,
|
||||
body:not(:hover) .header_f9f2ca,
|
||||
body:not(:hover) .container_ee69e0,
|
||||
body:not(:hover) .title_a7d72e,
|
||||
body:not(:hover) [aria-label="Members"],
|
||||
|
@ -512,8 +512,16 @@ function getCSS(fontName) {
|
|||
{
|
||||
color: var(--mutedtext) !important
|
||||
}
|
||||
|
||||
${settings.store.pastelStatuses ? `
|
||||
.menu_d90b3d
|
||||
{
|
||||
background: var(--accent) !important;
|
||||
}
|
||||
.messageGroupWrapper_ac90a2, .header_ac90a2
|
||||
{
|
||||
background-color: var(--primary);
|
||||
}
|
||||
${settings.store.pastelStatuses ?
|
||||
`
|
||||
/*Pastel statuses*/
|
||||
rect[fill='#23a55a'], svg[fill='#23a55a'] {
|
||||
fill: #80c968 !important;
|
||||
|
@ -612,7 +620,7 @@ function getCSS(fontName) {
|
|||
}
|
||||
|
||||
/*No more useless spotify activity header*/
|
||||
.headerContainer_d5089b
|
||||
.headerContainer_c1d9fd
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
|
@ -634,7 +642,7 @@ function getCSS(fontName) {
|
|||
}
|
||||
|
||||
/*Hide icon on file uploading status*/
|
||||
.icon_a4623d
|
||||
.icon_b52bef
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
|
@ -655,12 +663,12 @@ function getCSS(fontName) {
|
|||
padding: 6px 8px !important;
|
||||
}
|
||||
/*Hide the icon that displays what platform the user is listening with on spotify status*/
|
||||
.platformIcon_d5089b
|
||||
.platformIcon_c1d9fd
|
||||
{
|
||||
display: none !important;
|
||||
}
|
||||
/*hide the album name on spotify statuses (who cares)*/
|
||||
[class="state_d5089b ellipsis_d5089b textRow_d5089b"]
|
||||
[class="state_c1d9fd ellipsis_c1d9fd textRow_c1d9fd"]
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
|
@ -782,5 +790,3 @@ export default definePlugin({
|
|||
// preview thing, kinda low effort but eh
|
||||
settingsAboutComponent: () => <img src="https://files.catbox.moe/j8y2gt.webp" width="568px" border-radius="30px" ></img>
|
||||
});
|
||||
|
||||
|
||||
|
|
95
src/equicordplugins/jumpscare/index.tsx
Normal file
95
src/equicordplugins/jumpscare/index.tsx
Normal file
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./styles.css";
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { FluxDispatcher, ReactDOM, useEffect, useState } from "@webpack/common";
|
||||
import { Root } from "react-dom/client";
|
||||
|
||||
let jumpscareRoot: Root | undefined;
|
||||
|
||||
const settings = definePluginSettings({
|
||||
imageSource: {
|
||||
type: OptionType.STRING,
|
||||
description: "Sets the image url of the jumpscare",
|
||||
default: "https://github.com/Equicord/Equibored/blob/main/misc/troll.gif?raw=true"
|
||||
},
|
||||
audioSource: {
|
||||
type: OptionType.STRING,
|
||||
description: "Sets the audio url of the jumpscare",
|
||||
default: "https://github.com/Equicord/Equibored/raw/main/misc/trollolol.mp3?raw=true"
|
||||
},
|
||||
chance: {
|
||||
type: OptionType.NUMBER,
|
||||
description: "The chance of a jumpscare happening (1 in X so: 100 = 1/100 or 1%, 50 = 1/50 or 2%, etc.)",
|
||||
default: 1000
|
||||
}
|
||||
});
|
||||
|
||||
function getJumpscareRoot(): Root {
|
||||
if (!jumpscareRoot) {
|
||||
const element = document.createElement("div");
|
||||
element.id = "jumpscare-root";
|
||||
element.classList.add("jumpscare-root");
|
||||
document.body.append(element);
|
||||
jumpscareRoot = ReactDOM.createRoot(element);
|
||||
}
|
||||
|
||||
return jumpscareRoot;
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "Jumpscare",
|
||||
description: "Adds a configurable chance of jumpscaring you whenever you open a channel. Inspired by Geometry Dash Mega Hack",
|
||||
authors: [Devs.surgedevs],
|
||||
settings,
|
||||
|
||||
start() {
|
||||
getJumpscareRoot().render(
|
||||
<this.JumpscareComponent />
|
||||
);
|
||||
},
|
||||
|
||||
stop() {
|
||||
jumpscareRoot?.unmount();
|
||||
jumpscareRoot = undefined;
|
||||
},
|
||||
|
||||
JumpscareComponent() {
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
|
||||
const audio = new Audio(settings.store.audioSource);
|
||||
|
||||
const jumpscare = event => {
|
||||
if (isPlaying) return;
|
||||
|
||||
const chance = 1 / settings.store.chance;
|
||||
if (Math.random() > chance) return;
|
||||
|
||||
setIsPlaying(true);
|
||||
audio.play();
|
||||
|
||||
console.log(isPlaying);
|
||||
|
||||
setTimeout(() => {
|
||||
setIsPlaying(false);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
FluxDispatcher.subscribe("CHANNEL_SELECT", jumpscare);
|
||||
|
||||
return () => {
|
||||
FluxDispatcher.unsubscribe("CHANNEL_SELECT", jumpscare);
|
||||
};
|
||||
});
|
||||
|
||||
return <img className={`jumpscare-img ${isPlaying ? "jumpscare-animate" : ""}`} src={settings.store.imageSource} />;
|
||||
}
|
||||
});
|
33
src/equicordplugins/jumpscare/styles.css
Normal file
33
src/equicordplugins/jumpscare/styles.css
Normal file
|
@ -0,0 +1,33 @@
|
|||
.jumpscare-root {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.jumpscare-img {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #000;
|
||||
z-index: 99999;
|
||||
object-fit: contain;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.jumpscare-animate {
|
||||
animation: jumpscare-animation 0.7s;
|
||||
}
|
||||
|
||||
@keyframes jumpscare-animation {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
80% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
|
@ -7,23 +7,37 @@
|
|||
import "./style.css";
|
||||
|
||||
import { DataStore } from "@api/index";
|
||||
import { showNotification } from "@api/Notifications";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { DeleteIcon } from "@components/Icons";
|
||||
import { EquicordDevs } from "@utils/constants";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes } from "@utils/misc";
|
||||
import { useForceUpdater } from "@utils/react";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { Button, ChannelStore, Forms, NavigationRouter, Select, Switch, TextInput, useState } from "@webpack/common";
|
||||
import { Message } from "discord-types/general/index.js";
|
||||
import { findByCodeLazy, findByPropsLazy } from "@webpack";
|
||||
import { Button, ChannelStore, Forms, Select, SelectedChannelStore, Switch, TabBar, TextInput, Tooltip, UserStore, useState } from "@webpack/common";
|
||||
import { Message, User } from "discord-types/general/index.js";
|
||||
import type { PropsWithChildren } from "react";
|
||||
|
||||
|
||||
type IconProps = JSX.IntrinsicElements["svg"];
|
||||
type KeywordEntry = { regex: string, listIds: Array<string>, listType: ListType, ignoreCase: boolean; };
|
||||
|
||||
let keywordEntries: Array<KeywordEntry> = [];
|
||||
let currentUser: User;
|
||||
let keywordLog: Array<any> = [];
|
||||
|
||||
const recentMentionsPopoutClass = findByPropsLazy("recentMentionsPopout");
|
||||
const tabClass = findByPropsLazy("tab");
|
||||
const buttonClass = findByPropsLazy("size36");
|
||||
|
||||
const MenuHeader = findByCodeLazy(".getMessageReminders()).length");
|
||||
const Popout = findByCodeLazy(".Messages.UNBLOCK_TO_JUMP_TITLE", "canCloseAllMessages:");
|
||||
const createMessageRecord = findByCodeLazy(".createFromServer(", ".isBlockedForMessage", "messageReference:");
|
||||
const KEYWORD_ENTRIES_KEY = "KeywordNotify_keywordEntries";
|
||||
const KEYWORD_LOG_KEY = "KeywordNotify_log";
|
||||
|
||||
const cl = classNameFactory("vc-keywordnotify-");
|
||||
|
||||
|
@ -52,6 +66,32 @@ enum ListType {
|
|||
Whitelist = "Whitelist"
|
||||
}
|
||||
|
||||
interface BaseIconProps extends IconProps {
|
||||
viewBox: string;
|
||||
}
|
||||
|
||||
function highlightKeywords(str: string, entries: Array<KeywordEntry>) {
|
||||
let regexes: Array<RegExp>;
|
||||
try {
|
||||
regexes = entries.map(e => new RegExp(e.regex, "g" + (e.ignoreCase ? "i" : "")));
|
||||
} catch (err) {
|
||||
return [str];
|
||||
}
|
||||
|
||||
const matches = regexes.map(r => str.match(r)).flat().filter(e => e != null) as Array<string>;
|
||||
if (matches.length === 0) {
|
||||
return [str];
|
||||
}
|
||||
|
||||
const idx = str.indexOf(matches[0]);
|
||||
|
||||
return [
|
||||
<span>{str.substring(0, idx)}</span>,
|
||||
<span className="highlight">{matches[0]}</span>,
|
||||
<span>{str.substring(idx + matches[0].length)}</span>
|
||||
];
|
||||
}
|
||||
|
||||
function Collapsible({ title, children }) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
|
@ -115,7 +155,7 @@ function ListedIds({ listIds, setListIds }) {
|
|||
);
|
||||
}
|
||||
|
||||
function ListTypeSelector({ listType, setListType }) {
|
||||
function ListTypeSelector({ listType, setListType }: { listType: ListType, setListType: (v: ListType) => void; }) {
|
||||
return (
|
||||
<Select
|
||||
options={[
|
||||
|
@ -185,7 +225,6 @@ function KeywordEntries() {
|
|||
>
|
||||
Ignore Case
|
||||
</Switch>
|
||||
<Forms.FormDivider className={[Margins.top8, Margins.bottom8].join(" ")} />
|
||||
<Forms.FormTitle tag="h5">Whitelist/Blacklist</Forms.FormTitle>
|
||||
<Flex flexDirection="row">
|
||||
<div style={{ flexGrow: 1 }}>
|
||||
|
@ -215,22 +254,60 @@ function KeywordEntries() {
|
|||
);
|
||||
}
|
||||
|
||||
function Icon({ height = 24, width = 24, className, children, viewBox, ...svgProps }: PropsWithChildren<BaseIconProps>) {
|
||||
return (
|
||||
<svg
|
||||
className={classes(className, "vc-icon")}
|
||||
role="img"
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox={viewBox}
|
||||
{...svgProps}
|
||||
>
|
||||
{children}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
// Ideally I would just add this to Icons.tsx, but I cannot as this is a user-plugin :/
|
||||
function DoubleCheckmarkIcon(props: IconProps) {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
className={classes(props.className, "vc-double-checkmark-icon")}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path fill="currentColor"
|
||||
d="M16.7 8.7a1 1 0 0 0-1.4-1.4l-3.26 3.24a1 1 0 0 0 1.42 1.42L16.7 8.7ZM3.7 11.3a1 1 0 0 0-1.4 1.4l4.5 4.5a1 1 0 0 0 1.4-1.4l-4.5-4.5Z"
|
||||
/>
|
||||
<path fill="currentColor"
|
||||
d="M21.7 9.7a1 1 0 0 0-1.4-1.4L13 15.58l-3.3-3.3a1 1 0 0 0-1.4 1.42l4 4a1 1 0 0 0 1.4 0l8-8Z"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
const settings = definePluginSettings({
|
||||
ignoreBots: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Ignore messages from bots",
|
||||
default: true
|
||||
},
|
||||
amountToKeep: {
|
||||
type: OptionType.NUMBER,
|
||||
description: "Amount of messages to keep in the log",
|
||||
default: 50
|
||||
},
|
||||
keywords: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "Keywords to detect",
|
||||
description: "Manage keywords",
|
||||
component: () => <KeywordEntries />
|
||||
}
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "KeywordNotify",
|
||||
authors: [EquicordDevs.camila314, EquicordDevs.thororen],
|
||||
authors: [EquicordDevs.camila314, EquicordDevs.x3rt, EquicordDevs.thororen],
|
||||
description: "Sends a notification if a given message matches certain keywords or regexes",
|
||||
settings,
|
||||
patches: [
|
||||
|
@ -240,20 +317,53 @@ export default definePlugin({
|
|||
match: /}_dispatch\((\i),\i\){/,
|
||||
replace: "$&$1=$self.modify($1);"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: "Messages.UNREADS_TAB_LABEL}",
|
||||
replacement: {
|
||||
match: /\i\?\(0,\i\.jsxs\)\(\i\.TabBar\.Item/,
|
||||
replace: "$self.keywordTabBar(),$&"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: "location:\"RecentsPopout\"})",
|
||||
replacement: {
|
||||
match: /:(\i)===\i\.\i\.MENTIONS\?\(0,.+?setTab:(\i),onJump:(\i),badgeState:\i,closePopout:(\i)/,
|
||||
replace: ": $1 === 8 ? $self.tryKeywordMenu($2, $3, $4) $&"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: ".guildFilter:null",
|
||||
replacement: {
|
||||
match: /function (\i)\(\i\){let{message:\i,gotoMessage/,
|
||||
replace: "$self.renderMsg = $1; $&"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: ".guildFilter:null",
|
||||
replacement: {
|
||||
match: /onClick:\(\)=>(\i\.\i\.deleteRecentMention\((\i)\.id\))/,
|
||||
replace: "onClick: () => $2._keyword ? $self.deleteKeyword($2.id) : $1"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
async start() {
|
||||
this.onUpdate = () => null;
|
||||
currentUser = UserStore.getCurrentUser();
|
||||
keywordEntries = await DataStore.get(KEYWORD_ENTRIES_KEY) ?? [];
|
||||
await DataStore.set(KEYWORD_ENTRIES_KEY, keywordEntries);
|
||||
(await DataStore.get(KEYWORD_LOG_KEY) ?? []).map(e => JSON.parse(e)).forEach(e => {
|
||||
this.addToLog(e);
|
||||
});
|
||||
},
|
||||
|
||||
applyKeywordEntries(m: Message) {
|
||||
let matches = false;
|
||||
let match = "";
|
||||
|
||||
for (const entry of keywordEntries) {
|
||||
if (entry.regex === "") {
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
let listed = entry.listIds.some(id => id === m.channel_id || id === m.author.id);
|
||||
|
@ -267,31 +377,30 @@ export default definePlugin({
|
|||
const whitelistMode = entry.listType === ListType.Whitelist;
|
||||
|
||||
if (!whitelistMode && listed) {
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
if (whitelistMode && !listed) {
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (settings.store.ignoreBots && m.author.bot && (!whitelistMode || !entry.listIds.includes(m.author.id))) {
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
const flags = entry.ignoreCase ? "i" : "";
|
||||
if (safeMatchesRegex(m.content, entry.regex, flags)) {
|
||||
matches = true;
|
||||
match = m.content;
|
||||
}
|
||||
|
||||
for (const embed of m.embeds as any) {
|
||||
if (safeMatchesRegex(embed.description, entry.regex, flags) || safeMatchesRegex(embed.title, entry.regex, flags)) {
|
||||
matches = true;
|
||||
match = m.content;
|
||||
} else if (embed.fields != null) {
|
||||
for (const field of embed.fields as Array<{ name: string, value: string; }>) {
|
||||
if (safeMatchesRegex(field.value, entry.regex, flags) || safeMatchesRegex(field.name, entry.regex, flags)) {
|
||||
matches = true;
|
||||
match = m.content;
|
||||
} else {
|
||||
for (const embed of m.embeds as any) {
|
||||
if (safeMatchesRegex(embed.description, entry.regex, flags) || safeMatchesRegex(embed.title, entry.regex, flags)) {
|
||||
matches = true;
|
||||
break;
|
||||
} else if (embed.fields != null) {
|
||||
for (const field of embed.fields as Array<{ name: string, value: string; }>) {
|
||||
if (safeMatchesRegex(field.value, entry.regex, flags) || safeMatchesRegex(field.name, entry.regex, flags)) {
|
||||
matches = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -299,14 +408,112 @@ export default definePlugin({
|
|||
}
|
||||
|
||||
if (matches) {
|
||||
showNotification({
|
||||
title: "Keyword Notify",
|
||||
body: `${m.author.username} matched the keyword ${match}`,
|
||||
onClick: () => NavigationRouter.transitionTo(`/channels/${ChannelStore.getChannel(m.channel_id)?.guild_id ?? "@me"}/${m.channel_id}${m.id ? "/" + m.id : ""}`)
|
||||
});
|
||||
// @ts-ignore
|
||||
m.mentions.push({ id: currentUser.id });
|
||||
|
||||
if (m.author.id !== currentUser.id)
|
||||
this.addToLog(m);
|
||||
}
|
||||
},
|
||||
|
||||
addToLog(m: Message) {
|
||||
if (m == null || keywordLog.some(e => e.id === m.id))
|
||||
return;
|
||||
|
||||
DataStore.get(KEYWORD_LOG_KEY).then(log => {
|
||||
DataStore.set(KEYWORD_LOG_KEY, [...log, JSON.stringify(m)]);
|
||||
});
|
||||
|
||||
const thing = createMessageRecord(m);
|
||||
keywordLog.push(thing);
|
||||
keywordLog.sort((a, b) => b.timestamp - a.timestamp);
|
||||
|
||||
if (keywordLog.length > settings.store.amountToKeep)
|
||||
keywordLog.pop();
|
||||
|
||||
this.onUpdate();
|
||||
},
|
||||
|
||||
deleteKeyword(id) {
|
||||
keywordLog = keywordLog.filter(e => e.id !== id);
|
||||
this.onUpdate();
|
||||
},
|
||||
|
||||
keywordTabBar() {
|
||||
return (
|
||||
<TabBar.Item className={classes(tabClass.tab, tabClass.expanded)} id={8}>
|
||||
Keywords
|
||||
</TabBar.Item>
|
||||
);
|
||||
},
|
||||
|
||||
tryKeywordMenu(setTab, onJump, closePopout) {
|
||||
const header = (
|
||||
<MenuHeader tab={8} setTab={setTab} closePopout={closePopout} badgeState={{ badgeForYou: false }} children={
|
||||
<Tooltip text="Clear All">
|
||||
{({ onMouseLeave, onMouseEnter }) => (
|
||||
<Button
|
||||
onMouseLeave={onMouseLeave}
|
||||
onMouseEnter={onMouseEnter}
|
||||
look={Button.Looks.BLANK}
|
||||
size={Button.Sizes.ICON}
|
||||
onClick={() => {
|
||||
keywordLog = [];
|
||||
DataStore.set(KEYWORD_LOG_KEY, []);
|
||||
this.onUpdate();
|
||||
}}>
|
||||
<div className={classes(buttonClass.button, buttonClass.secondary, buttonClass.size32)}>
|
||||
<DoubleCheckmarkIcon />
|
||||
</div>
|
||||
</Button>
|
||||
)}
|
||||
</Tooltip>
|
||||
} />
|
||||
);
|
||||
|
||||
const channel = ChannelStore.getChannel(SelectedChannelStore.getChannelId());
|
||||
|
||||
const [tempLogs, setKeywordLog] = useState(keywordLog);
|
||||
this.onUpdate = () => {
|
||||
const newLog = Array.from(keywordLog);
|
||||
setKeywordLog(newLog);
|
||||
};
|
||||
|
||||
const messageRender = (e, t) => {
|
||||
e._keyword = true;
|
||||
|
||||
e.customRenderedContent = {
|
||||
content: highlightKeywords(e.content, keywordEntries)
|
||||
};
|
||||
|
||||
const msg = this.renderMsg({
|
||||
message: e,
|
||||
gotoMessage: t,
|
||||
dismissible: true
|
||||
});
|
||||
|
||||
return [msg];
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popout
|
||||
className={classes(recentMentionsPopoutClass.recentMentionsPopout)}
|
||||
renderHeader={() => header}
|
||||
renderMessage={messageRender}
|
||||
channel={channel}
|
||||
onJump={onJump}
|
||||
onFetch={() => null}
|
||||
onCloseMessage={this.deleteKeyword}
|
||||
loadMore={() => null}
|
||||
messages={tempLogs}
|
||||
renderEmptyState={() => null}
|
||||
canCloseAllMessages={true}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
||||
modify(e) {
|
||||
if (e.type === "MESSAGE_CREATE") {
|
||||
this.applyKeywordEntries(e.message);
|
||||
|
|
|
@ -12,7 +12,7 @@ import { Button, Forms, i18n, Menu, TabBar } from "@webpack/common";
|
|||
import { ReactElement } from "react";
|
||||
|
||||
import { preload, unload } from "./images";
|
||||
import { cl, QrCodeCameraIcon } from "./ui";
|
||||
import { cl, QrCodeIcon } from "./ui";
|
||||
import openQrModal from "./ui/modals/QrModal";
|
||||
|
||||
export default definePlugin({
|
||||
|
@ -106,7 +106,7 @@ export default definePlugin({
|
|||
<Menu.MenuItem
|
||||
id="scan-qr"
|
||||
label={i18n.Messages.USER_SETTINGS_SCAN_QR_CODE}
|
||||
icon={QrCodeCameraIcon}
|
||||
icon={QrCodeIcon}
|
||||
action={openQrModal}
|
||||
showIconFirst
|
||||
focusedClassName={menuItemFocused}
|
||||
|
|
|
@ -37,11 +37,10 @@ export const { Spinner } = proxyLazy(() => Forms as any as {
|
|||
SpinnerTypes: typeof SpinnerTypes;
|
||||
});
|
||||
|
||||
export const { QrCodeCameraIcon } = findByPropsLazy("QrCodeCameraIcon") as {
|
||||
QrCodeCameraIcon: ComponentType<{
|
||||
size: number;
|
||||
}>;
|
||||
};
|
||||
const icons = findByPropsLazy("PencilIcon");
|
||||
export const QrCodeIcon = proxyLazy(() => icons.QrCodeCameraIcon ?? icons.QrCodeIcon) as ComponentType<{
|
||||
size: number;
|
||||
}>;
|
||||
|
||||
export const cl = classNameFactory("qrlogin-");
|
||||
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
function SpeedIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="currentColor"
|
||||
viewBox="0 -960 960 960"
|
||||
>
|
||||
<path d="M418-340q24 24 62 23.5t56-27.5l224-336-336 224q-27 18-28.5 55t22.5 61zm62-460q59 0 113.5 16.5T696-734l-76 48q-33-17-68.5-25.5T480-720q-133 0-226.5 93.5T160-400q0 42 11.5 83t32.5 77h552q23-38 33.5-79t10.5-85q0-36-8.5-70T766-540l48-76q30 47 47.5 100T880-406q1 57-13 109t-41 99q-11 18-30 28t-40 10H204q-21 0-40-10t-30-28q-26-45-40-95.5T80-400q0-83 31.5-155.5t86-127Q252-737 325-768.5T480-800zm7 313z"></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default SpeedIcon;
|
151
src/equicordplugins/mediaPlaybackSpeed/index.tsx
Normal file
151
src/equicordplugins/mediaPlaybackSpeed/index.tsx
Normal file
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./styles.css";
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { makeRange } from "@components/PluginSettings/components";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { ContextMenuApi, FluxDispatcher, Heading, Menu, React, Tooltip, useEffect } from "@webpack/common";
|
||||
import { RefObject } from "react";
|
||||
|
||||
import SpeedIcon from "./components/SpeedIcon";
|
||||
|
||||
const cl = classNameFactory("vc-media-playback-speed-");
|
||||
|
||||
const min = 0.25;
|
||||
const max = 3.5;
|
||||
const speeds = makeRange(min, max, 0.25);
|
||||
|
||||
const settings = definePluginSettings({
|
||||
test: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "",
|
||||
component() {
|
||||
return <Heading variant="heading-lg/bold" selectable={false}>
|
||||
Default playback speeds
|
||||
</Heading>;
|
||||
}
|
||||
},
|
||||
defaultVoiceMessageSpeed: {
|
||||
type: OptionType.SLIDER,
|
||||
default: 1,
|
||||
description: "Voice messages",
|
||||
markers: speeds,
|
||||
},
|
||||
defaultVideoSpeed: {
|
||||
type: OptionType.SLIDER,
|
||||
default: 1,
|
||||
description: "Videos",
|
||||
markers: speeds,
|
||||
},
|
||||
defaultAudioSpeed: {
|
||||
type: OptionType.SLIDER,
|
||||
default: 1,
|
||||
description: "Audios",
|
||||
markers: speeds,
|
||||
},
|
||||
});
|
||||
|
||||
type MediaRef = RefObject<HTMLMediaElement> | undefined;
|
||||
|
||||
export default definePlugin({
|
||||
name: "MediaPlaybackSpeed",
|
||||
description: "Allows changing the (default) playback speed of media embeds",
|
||||
authors: [Devs.D3SOX],
|
||||
|
||||
settings,
|
||||
|
||||
PlaybackSpeedComponent({ mediaRef }: { mediaRef: MediaRef; }) {
|
||||
const changeSpeed = (speed: number) => {
|
||||
const media = mediaRef?.current;
|
||||
if (media) {
|
||||
media.playbackRate = speed;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!mediaRef?.current) return;
|
||||
const media = mediaRef.current;
|
||||
if (media.tagName === "AUDIO") {
|
||||
const isVoiceMessage = media.className.includes("audioElement_");
|
||||
changeSpeed(isVoiceMessage ? settings.store.defaultVoiceMessageSpeed : settings.store.defaultAudioSpeed);
|
||||
} else if (media.tagName === "VIDEO") {
|
||||
changeSpeed(settings.store.defaultVideoSpeed);
|
||||
}
|
||||
}, [mediaRef]);
|
||||
|
||||
return (
|
||||
<Tooltip text="Playback speed">
|
||||
{tooltipProps => (
|
||||
<button
|
||||
{...tooltipProps}
|
||||
className={cl("icon")}
|
||||
onClick={e => {
|
||||
ContextMenuApi.openContextMenu(e, () =>
|
||||
<Menu.Menu
|
||||
navId="vc-playback-speed"
|
||||
onClose={() => FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
|
||||
aria-label="Playback speed control"
|
||||
>
|
||||
<Menu.MenuGroup
|
||||
label="Playback speed"
|
||||
>
|
||||
{speeds.map(speed => (
|
||||
<Menu.MenuItem
|
||||
key={speed}
|
||||
id={"speed-" + speed}
|
||||
label={`${speed}x`}
|
||||
action={() => changeSpeed(speed)}
|
||||
/>
|
||||
))}
|
||||
</Menu.MenuGroup>
|
||||
</Menu.Menu>
|
||||
);
|
||||
}}>
|
||||
<SpeedIcon />
|
||||
</button>
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
|
||||
renderComponent(mediaRef: MediaRef) {
|
||||
return <ErrorBoundary noop>
|
||||
<this.PlaybackSpeedComponent mediaRef={mediaRef} />
|
||||
</ErrorBoundary>;
|
||||
},
|
||||
|
||||
patches: [
|
||||
// voice message embeds
|
||||
{
|
||||
find: "\"--:--\"",
|
||||
replacement: {
|
||||
match: /onVolumeShow:\i,onVolumeHide:\i\}\)(?<=useCallback\(\(\)=>\{let \i=(\i).current;.+?)/,
|
||||
replace: "$&,$self.renderComponent($1)"
|
||||
}
|
||||
},
|
||||
// audio & video embeds
|
||||
{
|
||||
// need to pass media ref via props to make it easily accessible from inside controls
|
||||
find: "renderControls(){",
|
||||
replacement: {
|
||||
match: /onToggleMuted:this.toggleMuted,/,
|
||||
replace: "$&mediaRef:this.mediaRef,"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: "AUDIO:\"AUDIO\"",
|
||||
replacement: {
|
||||
match: /onVolumeHide:\i,iconClassName:\i.controlIcon,iconColor:"currentColor",sliderWrapperClassName:\i.volumeSliderWrapper\}\)\}\),/,
|
||||
replace: "$&$self.renderComponent(this.props.mediaRef),"
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
10
src/equicordplugins/mediaPlaybackSpeed/styles.css
Normal file
10
src/equicordplugins/mediaPlaybackSpeed/styles.css
Normal file
|
@ -0,0 +1,10 @@
|
|||
.vc-media-playback-speed-icon {
|
||||
background-color: transparent;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
color: var(--interactive-normal);
|
||||
}
|
||||
|
||||
.vc-media-playback-speed-icon:hover {
|
||||
color: var(--interactive-active);
|
||||
}
|
63
src/equicordplugins/messageColors/constants.ts
Normal file
63
src/equicordplugins/messageColors/constants.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { OptionType } from "@utils/types";
|
||||
|
||||
export const enum RenderType {
|
||||
BLOCK,
|
||||
FOREGROUND,
|
||||
BACKGROUND,
|
||||
}
|
||||
|
||||
export const settings = definePluginSettings({
|
||||
renderType: {
|
||||
type: OptionType.SELECT,
|
||||
description: "How to render colors",
|
||||
options: [
|
||||
{
|
||||
label: "Text color",
|
||||
value: RenderType.FOREGROUND,
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
label: "Block nearby",
|
||||
value: RenderType.BLOCK,
|
||||
},
|
||||
{
|
||||
label: "Background color",
|
||||
value: RenderType.BACKGROUND
|
||||
},
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
export const enum ColorType {
|
||||
RGB,
|
||||
RGBA,
|
||||
HEX,
|
||||
HSL
|
||||
}
|
||||
|
||||
// It's sooo hard to read regex without this, it makes it at least somewhat bearable
|
||||
export const replaceRegexp = (reg: string) => {
|
||||
const n = new RegExp(reg
|
||||
// \c - 'comma'
|
||||
// \v - 'value'
|
||||
// \f - 'float'
|
||||
.replaceAll("\\f", "[+-]?([0-9]*[.])?[0-9]+")
|
||||
.replaceAll("\\c", "(?:,|\\s)")
|
||||
.replaceAll("\\v", "\\s*?\\d+?\\s*?"), "g");
|
||||
|
||||
return n;
|
||||
};
|
||||
|
||||
export const regex = [
|
||||
{ reg: /rgb\(\v\c\v\c\v\)/g, type: ColorType.RGB },
|
||||
{ reg: /rgba\(\v\c\v\c\v(\c|\/?)\s*\f\)/g, type: ColorType.RGBA },
|
||||
{ reg: /hsl\(\v°?\c\s*?\d+%?\s*?\c\s*?\d+%?\s*?\)/g, type: ColorType.HSL },
|
||||
{ reg: /#(?:[0-9a-fA-F]{3}){1,2}/g, type: ColorType.HEX }
|
||||
].map(v => { v.reg = replaceRegexp(v.reg.source); return v; });
|
203
src/equicordplugins/messageColors/index.tsx
Normal file
203
src/equicordplugins/messageColors/index.tsx
Normal file
|
@ -0,0 +1,203 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./styles.css";
|
||||
|
||||
import { EquicordDevs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import { React } from "@webpack/common";
|
||||
|
||||
import {
|
||||
ColorType,
|
||||
regex,
|
||||
RenderType,
|
||||
replaceRegexp,
|
||||
settings,
|
||||
} from "./constants";
|
||||
|
||||
const source = regex.map(r => r.reg.source).join("|");
|
||||
const matchAllRegExp = new RegExp(`^(${source})`, "i");
|
||||
|
||||
interface ParsedColorInfo {
|
||||
type: "color";
|
||||
color: string;
|
||||
colorType: ColorType;
|
||||
text: string;
|
||||
}
|
||||
|
||||
const requiredFirstCharacters = ["r", "h", "#"].flatMap(v => [
|
||||
v,
|
||||
v.toUpperCase(),
|
||||
]);
|
||||
|
||||
export default definePlugin({
|
||||
authors: [EquicordDevs.Hen],
|
||||
name: "MessageColors",
|
||||
description: "Displays color codes like #FF0042 inside of messages",
|
||||
settings,
|
||||
patches: [
|
||||
// Create a new markdown rule, so it parses just like any other features
|
||||
// Like bolding, spoilers, mentions, etc
|
||||
{
|
||||
find: "roleMention:{order:",
|
||||
group: true,
|
||||
replacement: {
|
||||
match: /roleMention:\{order:(\i\.\i\.order)/,
|
||||
replace: "color:$self.getColor($1),$&",
|
||||
},
|
||||
},
|
||||
// Changes text md rule regex, so it stops right before hsl( | rgb(
|
||||
// Without it discord will try to pass a string without those to color rule
|
||||
{
|
||||
find: ".defaultRules.text,match:",
|
||||
group: true,
|
||||
replacement: {
|
||||
// $)/)
|
||||
match: /\$\)\/\)}/,
|
||||
// hsl(|rgb(|$&
|
||||
replace: requiredFirstCharacters.join("|") + "|$&",
|
||||
},
|
||||
},
|
||||
// Discord just requires it to be here
|
||||
// Or it explodes (bad)
|
||||
{
|
||||
find: "Unknown markdown rule:",
|
||||
group: true,
|
||||
replacement: {
|
||||
match: /roleMention:{type:/,
|
||||
replace: 'color:{type:"inlineObject"},$&',
|
||||
},
|
||||
},
|
||||
],
|
||||
getColor(order: number) {
|
||||
return {
|
||||
order,
|
||||
// Don't even try to match if the message chunk doesn't start with...
|
||||
requiredFirstCharacters,
|
||||
// Match -> Parse -> React
|
||||
// Result of previous action is dropped as a first argument of the next one
|
||||
match(content: string) {
|
||||
return matchAllRegExp.exec(content);
|
||||
},
|
||||
parse(
|
||||
matchedContent: RegExpExecArray,
|
||||
_,
|
||||
parseProps: Record<string, any>,
|
||||
): ParsedColorInfo | { type: "text"; content: string; } {
|
||||
// This check makes sure that it doesn't try to parse color
|
||||
// When typing/editing message
|
||||
//
|
||||
// Discord doesn't know how to deal with color and crashes
|
||||
if (!parseProps.messageId)
|
||||
return {
|
||||
type: "text",
|
||||
content: matchedContent[0],
|
||||
};
|
||||
|
||||
const content = matchedContent[0];
|
||||
try {
|
||||
const type = getColorType(content);
|
||||
|
||||
return {
|
||||
type: "color",
|
||||
colorType: type,
|
||||
color: parseColor(content, type),
|
||||
text: content,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return {
|
||||
type: "text",
|
||||
content: matchedContent[0],
|
||||
};
|
||||
}
|
||||
},
|
||||
// react(args: ReturnType<typeof this.parse>)
|
||||
react({ text, colorType, color }: ParsedColorInfo) {
|
||||
if (settings.store.renderType === RenderType.FOREGROUND) {
|
||||
return <span style={{ color: color }}>{text}</span>;
|
||||
}
|
||||
const styles = {
|
||||
"--color": color,
|
||||
} as React.CSSProperties;
|
||||
|
||||
if (settings.store.renderType === RenderType.BACKGROUND) {
|
||||
const isDark = isColorDark(color, colorType);
|
||||
const className = `vc-color-bg ${!isDark ? "vc-color-bg-invert" : ""}`;
|
||||
return (
|
||||
<span className={className} style={styles}>
|
||||
{text}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{text}
|
||||
<span className="vc-color-block" style={styles}></span>
|
||||
</>
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// https://en.wikipedia.org/wiki/Relative_luminance
|
||||
const calcRGBLightness = (r: number, g: number, b: number) => {
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
};
|
||||
const isColorDark = (color: string, type: ColorType): boolean => {
|
||||
switch (type) {
|
||||
case ColorType.RGBA:
|
||||
case ColorType.RGB: {
|
||||
const match = color.match(/\d+/g)!;
|
||||
const lightness = calcRGBLightness(+match[0], +match[1], +match[2]);
|
||||
return lightness < 140;
|
||||
}
|
||||
case ColorType.HEX: {
|
||||
var rgb = parseInt(color.substring(1), 16);
|
||||
const r = (rgb >> 16) & 0xff;
|
||||
const g = (rgb >> 8) & 0xff;
|
||||
const b = (rgb >> 0) & 0xff;
|
||||
const lightness = calcRGBLightness(r, g, b);
|
||||
return lightness < 140;
|
||||
}
|
||||
case ColorType.HSL: {
|
||||
const match = color.match(/\d+/g)!;
|
||||
const lightness = +match[2];
|
||||
return lightness < 50;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getColorType = (color: string): ColorType => {
|
||||
color = color.toLowerCase().trim();
|
||||
if (color.startsWith("#")) return ColorType.HEX;
|
||||
if (color.startsWith("hsl")) return ColorType.HSL;
|
||||
if (color.startsWith("rgba")) return ColorType.RGBA;
|
||||
if (color.startsWith("rgb")) return ColorType.RGB;
|
||||
|
||||
throw new Error(`Can't resolve color type of ${color}`);
|
||||
};
|
||||
|
||||
function parseColor(str: string, type: ColorType): string {
|
||||
str = str
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replaceAll(/(\s|,)+/g, " ");
|
||||
switch (type) {
|
||||
case ColorType.RGB:
|
||||
return str;
|
||||
case ColorType.RGBA:
|
||||
if (!str.includes("/"))
|
||||
return str.replaceAll(replaceRegexp(/\f(?=\s*?\))/.source), "/$&");
|
||||
return str;
|
||||
case ColorType.HEX:
|
||||
return str[0] === "#" ? str : `#${str}`;
|
||||
case ColorType.HSL:
|
||||
return str.replace("°", "");
|
||||
}
|
||||
}
|
20
src/equicordplugins/messageColors/styles.css
Normal file
20
src/equicordplugins/messageColors/styles.css
Normal file
|
@ -0,0 +1,20 @@
|
|||
.vc-color-block {
|
||||
aspect-ratio: 1/1;
|
||||
background: var(--color);
|
||||
height: 1rem;
|
||||
vertical-align: middle;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
user-select: none;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.vc-color-bg {
|
||||
background: var(--color);
|
||||
}
|
||||
|
||||
/* Light color in dark theme */
|
||||
.theme-dark .vc-color-bg.vc-color-bg-invert,
|
||||
.theme-light .vc-color-bg:not(.vc-color-bg-invert) {
|
||||
color: var(--background-tertiary);
|
||||
}
|
41
src/equicordplugins/noMirroredCamera/index.tsx
Normal file
41
src/equicordplugins/noMirroredCamera/index.tsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
|
||||
export default definePlugin({
|
||||
name: "NoMirroredCamera",
|
||||
description: "Prevents the camera from being mirrored on your screen",
|
||||
authors: [Devs.nyx],
|
||||
|
||||
patches: [
|
||||
// When focused on voice channel or group chat voice call
|
||||
{
|
||||
find: /\i\?\i.\i.SELF_VIDEO/,
|
||||
replacement: {
|
||||
match: /mirror:\i/,
|
||||
replace: "mirror:!1"
|
||||
}
|
||||
},
|
||||
// Popout camera when not focused on voice channel
|
||||
{
|
||||
find: ".mirror]:",
|
||||
replacement: {
|
||||
match: /\[(\i).mirror]:\i/,
|
||||
replace: "[$1.mirror]:!1"
|
||||
}
|
||||
},
|
||||
// Overriding css on Preview Camera/Change Video Background popup
|
||||
{
|
||||
find: ".cameraPreview,",
|
||||
replacement: {
|
||||
match: /className:\i.camera,/,
|
||||
replace: "$&style:{transform: \"scalex(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 NotificationsOffIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}><path fill="currentColor" d="M20 18.69L7.84 6.14L5.27 3.49L4 4.76l2.8 2.8v.01c-.52.99-.8 2.16-.8 3.42v5l-2 2v1h13.73l2 2L21 19.72zM12 22c1.11 0 2-.89 2-2h-4c0 1.11.89 2 2 2m6-7.32V11c0-3.08-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68c-.15.03-.29.08-.42.12c-.1.03-.2.07-.3.11h-.01c-.01 0-.01 0-.02.01c-.23.09-.46.2-.68.31c0 0-.01 0-.01.01z"></path></svg>);
|
||||
}
|
|
@ -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 NotificationsOnIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}><path fill="currentColor" d="M10 20h4c0 1.1-.9 2-2 2s-2-.9-2-2m4-11c0 2.61 1.67 4.83 4 5.66V17h2v2H4v-2h2v-7c0-2.79 1.91-5.14 4.5-5.8v-.7c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5v.7c.71.18 1.36.49 1.95.9A5.902 5.902 0 0 0 14 9m10-1h-3V5h-2v3h-3v2h3v3h2v-3h3z"></path></svg>);
|
||||
}
|
|
@ -1,344 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||
import { showNotification } from "@api/Notifications";
|
||||
import { definePluginSettings, Settings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByPropsLazy, findStoreLazy } from "@webpack";
|
||||
import { Menu, PresenceStore, React, SelectedChannelStore, Tooltip, UserStore } from "@webpack/common";
|
||||
import type { Channel, User } from "discord-types/general";
|
||||
import { CSSProperties } from "react";
|
||||
|
||||
import { NotificationsOffIcon } from "./components/NotificationsOffIcon";
|
||||
import { NotificationsOnIcon } from "./components/NotificationsOnIcon";
|
||||
|
||||
interface PresenceUpdate {
|
||||
user: {
|
||||
id: string;
|
||||
username?: string;
|
||||
global_name?: string;
|
||||
};
|
||||
clientStatus: {
|
||||
desktop?: string;
|
||||
web?: string;
|
||||
mobile?: string;
|
||||
console?: string;
|
||||
};
|
||||
guildId?: string;
|
||||
status: string;
|
||||
broadcast?: any; // what's this?
|
||||
activities: Array<{
|
||||
session_id: string;
|
||||
created_at: number;
|
||||
id: string;
|
||||
name: string;
|
||||
details?: string;
|
||||
type: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface VoiceState {
|
||||
userId: string;
|
||||
channelId?: string;
|
||||
oldChannelId?: string;
|
||||
deaf: boolean;
|
||||
mute: boolean;
|
||||
selfDeaf: boolean;
|
||||
selfMute: boolean;
|
||||
selfStream: boolean;
|
||||
selfVideo: boolean;
|
||||
sessionId: string;
|
||||
suppress: boolean;
|
||||
requestToSpeakTimestamp: string | null;
|
||||
}
|
||||
|
||||
function shouldBeNative() {
|
||||
if (typeof Notification === "undefined") return false;
|
||||
|
||||
const { useNative } = Settings.notifications;
|
||||
if (useNative === "always") return true;
|
||||
if (useNative === "not-focused") return !document.hasFocus();
|
||||
return false;
|
||||
}
|
||||
|
||||
const SessionsStore = findStoreLazy("SessionsStore");
|
||||
|
||||
const StatusUtils = findByPropsLazy("useStatusFillColor", "StatusTypes");
|
||||
|
||||
function Icon(path: string, opts?: { viewBox?: string; width?: number; height?: number; }) {
|
||||
return ({ color, tooltip, small }: { color: string; tooltip: string; small: boolean; }) => (
|
||||
<Tooltip text={tooltip} >
|
||||
{(tooltipProps: any) => (
|
||||
<svg
|
||||
{...tooltipProps}
|
||||
height={(opts?.height ?? 20) - (small ? 3 : 0)}
|
||||
width={(opts?.width ?? 20) - (small ? 3 : 0)}
|
||||
viewBox={opts?.viewBox ?? "0 0 24 24"}
|
||||
fill={color}
|
||||
>
|
||||
<path d={path} />
|
||||
</svg>
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
const Icons = {
|
||||
desktop: Icon("M4 2.5c-1.103 0-2 .897-2 2v11c0 1.104.897 2 2 2h7v2H7v2h10v-2h-4v-2h7c1.103 0 2-.896 2-2v-11c0-1.103-.897-2-2-2H4Zm16 2v9H4v-9h16Z"),
|
||||
web: Icon("M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93Zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39Z"),
|
||||
mobile: Icon("M 187 0 L 813 0 C 916.277 0 1000 83.723 1000 187 L 1000 1313 C 1000 1416.277 916.277 1500 813 1500 L 187 1500 C 83.723 1500 0 1416.277 0 1313 L 0 187 C 0 83.723 83.723 0 187 0 Z M 125 1000 L 875 1000 L 875 250 L 125 250 Z M 500 1125 C 430.964 1125 375 1180.964 375 1250 C 375 1319.036 430.964 1375 500 1375 C 569.036 1375 625 1319.036 625 1250 C 625 1180.964 569.036 1125 500 1125 Z", { viewBox: "0 0 1000 1500", height: 17, width: 17 }),
|
||||
console: Icon("M14.8 2.7 9 3.1V47h3.3c1.7 0 6.2.3 10 .7l6.7.6V2l-4.2.2c-2.4.1-6.9.3-10 .5zm1.8 6.4c1 1.7-1.3 3.6-2.7 2.2C12.7 10.1 13.5 8 15 8c.5 0 1.2.5 1.6 1.1zM16 33c0 6-.4 10-1 10s-1-4-1-10 .4-10 1-10 1 4 1 10zm15-8v23.3l3.8-.7c2-.3 4.7-.6 6-.6H43V3h-2.2c-1.3 0-4-.3-6-.6L31 1.7V25z", { viewBox: "0 0 50 50" }),
|
||||
};
|
||||
type Platform = keyof typeof Icons;
|
||||
|
||||
const PlatformIcon = ({ platform, status, small }: { platform: Platform, status: string; small: boolean; }) => {
|
||||
const tooltip = platform[0].toUpperCase() + platform.slice(1);
|
||||
const Icon = Icons[platform] ?? Icons.desktop;
|
||||
|
||||
return <Icon color={StatusUtils.useStatusFillColor(status)} tooltip={tooltip} small={small} />;
|
||||
};
|
||||
|
||||
interface PlatformIndicatorProps {
|
||||
user: User;
|
||||
wantMargin?: boolean;
|
||||
wantTopMargin?: boolean;
|
||||
small?: boolean;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
const PlatformIndicator = ({ user, wantMargin = true, wantTopMargin = false, small = false, style = {} }: PlatformIndicatorProps) => {
|
||||
if (!user || user.bot) return null;
|
||||
|
||||
if (user.id === UserStore.getCurrentUser().id) {
|
||||
const sessions = SessionsStore.getSessions();
|
||||
if (typeof sessions !== "object") return null;
|
||||
const sortedSessions = Object.values(sessions).sort(({ status: a }: any, { status: b }: any) => {
|
||||
if (a === b) return 0;
|
||||
if (a === "online") return 1;
|
||||
if (b === "online") return -1;
|
||||
if (a === "idle") return 1;
|
||||
if (b === "idle") return -1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
const ownStatus = Object.values(sortedSessions).reduce((acc: any, curr: any) => {
|
||||
if (curr.clientInfo.client !== "unknown")
|
||||
acc[curr.clientInfo.client] = curr.status;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const { clientStatuses } = PresenceStore.getState();
|
||||
clientStatuses[UserStore.getCurrentUser().id] = ownStatus;
|
||||
}
|
||||
|
||||
const status = PresenceStore.getState()?.clientStatuses?.[user.id] as Record<Platform, string>;
|
||||
if (!status) return null;
|
||||
|
||||
const icons = Object.entries(status).map(([platform, status]) => (
|
||||
<PlatformIcon
|
||||
key={platform}
|
||||
platform={platform as Platform}
|
||||
status={status}
|
||||
small={small}
|
||||
/>
|
||||
));
|
||||
|
||||
if (!icons.length) return null;
|
||||
|
||||
return (
|
||||
<span
|
||||
className="vc-platform-indicator"
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginLeft: wantMargin ? 4 : 0,
|
||||
verticalAlign: "top",
|
||||
position: "relative",
|
||||
top: wantTopMargin ? 2 : 0,
|
||||
padding: !wantMargin ? 1 : 0,
|
||||
gap: 2,
|
||||
...style
|
||||
}}
|
||||
|
||||
>
|
||||
{icons}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export const settings = definePluginSettings({
|
||||
notifyStatus: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Notify on status changes",
|
||||
restartNeeded: false,
|
||||
default: true,
|
||||
},
|
||||
notifyVoice: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Notify on voice channel changes",
|
||||
restartNeeded: false,
|
||||
default: false,
|
||||
},
|
||||
persistNotifications: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Persist notifications",
|
||||
restartNeeded: false,
|
||||
default: false,
|
||||
},
|
||||
userIds: {
|
||||
type: OptionType.STRING,
|
||||
description: "User IDs (comma separated)",
|
||||
restartNeeded: false,
|
||||
default: "",
|
||||
}
|
||||
});
|
||||
|
||||
function getUserIdList() {
|
||||
try {
|
||||
return settings.store.userIds.split(",").filter(Boolean);
|
||||
} catch (e) {
|
||||
settings.store.userIds = "";
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// show rich body with user avatar
|
||||
const getRichBody = (user: User, text: string | React.ReactNode) => <div
|
||||
style={{ display: "flex", flexWrap: "wrap", alignItems: "center", gap: "10px" }}>
|
||||
<div style={{ position: "relative" }}>
|
||||
<img src={user.getAvatarURL(void 0, 80, true)}
|
||||
style={{ width: "80px", height: "80px", borderRadius: "15%" }} alt={`${user.username}'s avatar`} />
|
||||
<PlatformIndicator user={user} style={{ position: "absolute", top: "-8px", right: "-10px" }} />
|
||||
</div>
|
||||
<span>{text}</span>
|
||||
</div>;
|
||||
|
||||
function triggerVoiceNotification(userId: string, userChannelId: string | null) {
|
||||
const user = UserStore.getUser(userId);
|
||||
const myChanId = SelectedChannelStore.getVoiceChannelId();
|
||||
|
||||
const name = user.username;
|
||||
|
||||
const title = shouldBeNative() ? `User ${name} changed voice status` : "User voice status change";
|
||||
if (userChannelId) {
|
||||
if (userChannelId !== myChanId) {
|
||||
showNotification({
|
||||
title,
|
||||
body: "joined a new voice channel",
|
||||
noPersist: !settings.store.persistNotifications,
|
||||
richBody: getRichBody(user, `${name} joined a new voice channel`),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
showNotification({
|
||||
title,
|
||||
body: "left their voice channel",
|
||||
noPersist: !settings.store.persistNotifications,
|
||||
richBody: getRichBody(user, `${name} left their voice channel`),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function toggleUserNotify(userId: string) {
|
||||
const userIds = getUserIdList();
|
||||
if (userIds.includes(userId)) {
|
||||
userIds.splice(userIds.indexOf(userId), 1);
|
||||
} else {
|
||||
userIds.push(userId);
|
||||
}
|
||||
settings.store.userIds = userIds.join(",");
|
||||
}
|
||||
|
||||
interface UserContextProps {
|
||||
channel?: Channel;
|
||||
guildId?: string;
|
||||
user: User;
|
||||
}
|
||||
|
||||
const UserContext: NavContextMenuPatchCallback = (children, { user }: UserContextProps) => {
|
||||
if (!user || user.id === UserStore.getCurrentUser().id) return;
|
||||
const isNotifyOn = getUserIdList().includes(user.id);
|
||||
const label = isNotifyOn ? "Don't notify on changes" : "Notify on changes";
|
||||
const icon = isNotifyOn ? NotificationsOffIcon : NotificationsOnIcon;
|
||||
|
||||
children.splice(-1, 0, (
|
||||
<Menu.MenuGroup>
|
||||
<Menu.MenuItem
|
||||
id="toggle-notify-user"
|
||||
label={label}
|
||||
action={() => toggleUserNotify(user.id)}
|
||||
icon={icon}
|
||||
/>
|
||||
</Menu.MenuGroup>
|
||||
));
|
||||
};
|
||||
|
||||
const lastStatuses = new Map<string, string>();
|
||||
|
||||
export default definePlugin({
|
||||
name: "NotifyUserChanges",
|
||||
description: "Adds a notify option in the user context menu to get notified when a user changes voice channels or online status",
|
||||
authors: [Devs.D3SOX],
|
||||
|
||||
settings,
|
||||
|
||||
contextMenus: {
|
||||
"user-context": UserContext
|
||||
},
|
||||
|
||||
flux: {
|
||||
VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceState[]; }) {
|
||||
if (!settings.store.notifyVoice || !settings.store.userIds) {
|
||||
return;
|
||||
}
|
||||
for (const { userId, channelId, oldChannelId } of voiceStates) {
|
||||
if (channelId !== oldChannelId) {
|
||||
const isFollowed = getUserIdList().includes(userId);
|
||||
if (!isFollowed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (channelId) {
|
||||
// move or join new channel
|
||||
triggerVoiceNotification(userId, channelId);
|
||||
} else if (oldChannelId) {
|
||||
// leave
|
||||
triggerVoiceNotification(userId, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
PRESENCE_UPDATES({ updates }: { updates: PresenceUpdate[]; }) {
|
||||
if (!settings.store.notifyStatus || !settings.store.userIds) {
|
||||
return;
|
||||
}
|
||||
for (const { user: { id: userId, username }, status } of updates) {
|
||||
const isFollowed = getUserIdList().includes(userId);
|
||||
if (!isFollowed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// this is also triggered for multiple guilds and when only the activities change, so we have to check if the status actually changed
|
||||
if (lastStatuses.has(userId) && lastStatuses.get(userId) !== status) {
|
||||
const user = UserStore.getUser(userId);
|
||||
const name = username ?? user.username;
|
||||
|
||||
showNotification({
|
||||
title: shouldBeNative() ? `User ${name} changed status` : "User status change",
|
||||
body: `is now ${status}`,
|
||||
noPersist: !settings.store.persistNotifications,
|
||||
richBody: getRichBody(user, `${name}'s status is now ${status}`),
|
||||
});
|
||||
}
|
||||
lastStatuses.set(userId, status);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
});
|
|
@ -11,7 +11,7 @@
|
|||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--background-secondary);
|
||||
background-color: var(--background-primary);
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
border-radius: 100%;
|
||||
|
|
|
@ -4,14 +4,14 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { definePluginSettings, migratePluginSettings } from "@api/Settings";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { EquicordDevs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByCodeLazy } from "@webpack";
|
||||
import { FluxDispatcher, PresenceStore, UserStore } from "@webpack/common";
|
||||
import { PresenceStore, UserStore } from "@webpack/common";
|
||||
|
||||
let savedStatus = "";
|
||||
const updateAsync = findByCodeLazy("updateAsync", "status");
|
||||
|
||||
const settings = definePluginSettings({
|
||||
statusToSet: {
|
||||
type: OptionType.SELECT,
|
||||
|
@ -38,26 +38,23 @@ const settings = definePluginSettings({
|
|||
}
|
||||
});
|
||||
|
||||
migratePluginSettings("StatusWhilePlaying", "DNDWhilePlaying");
|
||||
export default definePlugin({
|
||||
name: "StatusWhilePlaying",
|
||||
description: "Automatically updates your status when playing games",
|
||||
description: "Automatically updates your online status (online, idle, dnd) when launching games",
|
||||
authors: [EquicordDevs.thororen],
|
||||
settings,
|
||||
runningGamesChange(event) {
|
||||
let savedStatus = "";
|
||||
if (event.games.length > 0) {
|
||||
flux: {
|
||||
RUNNING_GAMES_CHANGE(event) {
|
||||
const status = PresenceStore.getStatus(UserStore.getCurrentUser().id);
|
||||
savedStatus = status;
|
||||
updateAsync(settings.store.statusToSet);
|
||||
} else if (event.games.length === 0) {
|
||||
updateAsync(savedStatus);
|
||||
}
|
||||
},
|
||||
start() {
|
||||
FluxDispatcher.subscribe("RUNNING_GAMES_CHANGE", this.runningGamesChange);
|
||||
},
|
||||
stop() {
|
||||
FluxDispatcher.unsubscribe("RUNNING_GAMES_CHANGE", this.runningGamesChange);
|
||||
if (event.games.length > 0) {
|
||||
if (savedStatus !== "" && savedStatus !== settings.store.statusToSet)
|
||||
updateAsync(savedStatus);
|
||||
} else {
|
||||
if (status !== settings.store.statusToSet) {
|
||||
savedStatus = status;
|
||||
updateAsync(settings.store.statusToSet);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
|
|
101
src/equicordplugins/unreadBadgeCount/index.tsx
Normal file
101
src/equicordplugins/unreadBadgeCount/index.tsx
Normal file
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./styles.css";
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByPropsLazy, findStoreLazy } from "@webpack";
|
||||
import { ReadStateStore, useStateFromStores } from "@webpack/common";
|
||||
import { Channel } from "discord-types/general";
|
||||
|
||||
const UserGuildSettingsStore = findStoreLazy("UserGuildSettingsStore");
|
||||
const JoinedThreadsStore = findStoreLazy("JoinedThreadsStore");
|
||||
const { NumberBadge } = findByPropsLazy("NumberBadge");
|
||||
|
||||
const settings = definePluginSettings({
|
||||
showOnMutedChannels: {
|
||||
description: "Show unread count on muted channels",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: false,
|
||||
},
|
||||
notificationCountLimit: {
|
||||
description: "Show +99 instead of true amount",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: false,
|
||||
},
|
||||
replaceWhiteDot: {
|
||||
description: "Replace the white dot with the badge",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: false,
|
||||
restartNeeded: true,
|
||||
},
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "UnreadCountBadge",
|
||||
authors: [Devs.Joona],
|
||||
description: "Shows unread message count badges on channels in the channel list",
|
||||
settings,
|
||||
|
||||
patches: [
|
||||
// Kanged from typingindicators
|
||||
{
|
||||
find: "UNREAD_IMPORTANT:",
|
||||
replacement: [
|
||||
{
|
||||
match: /\.name\),.{0,120}\.children.+?:null/,
|
||||
replace: "$&,$self.CountBadge({channel: arguments[0].channel,})",
|
||||
predicate: () => !settings.store.replaceWhiteDot
|
||||
},
|
||||
{
|
||||
match: /\(0,\i\.jsx\)\("div",{className:\i\(\)\(\i\.unread,\i\?\i\.unreadImportant:void 0\)}\)/,
|
||||
replace: "$self.CountBadge({channel: arguments[0].channel, whiteDot:$&})",
|
||||
predicate: () => settings.store.replaceWhiteDot
|
||||
}
|
||||
]
|
||||
},
|
||||
// Threads
|
||||
{
|
||||
// This is the thread "spine" that shows in the left
|
||||
find: "M11 9H4C2.89543 9 2 8.10457 2 7V1C2 0.447715 1.55228 0 1 0C0.447715 0 0 0.447715 0 1V7C0 9.20914 1.79086 11 4 11H11C11.5523 11 12 10.5523 12 10C12 9.44771 11.5523 9 11 9Z",
|
||||
replacement: [
|
||||
{
|
||||
match: /mentionsCount:\i.{0,50}?null/,
|
||||
replace: "$&,$self.CountBadge({channel: arguments[0].thread})",
|
||||
predicate: () => !settings.store.replaceWhiteDot
|
||||
},
|
||||
{
|
||||
match: /\(0,\i\.jsx\)\("div",{className:\i\(\)\(\i\.unread,\i\.unreadImportant\)}\)/,
|
||||
replace: "$self.CountBadge({channel: arguments[0].thread, whiteDot:$&})",
|
||||
predicate: () => settings.store.replaceWhiteDot
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
|
||||
CountBadge: ErrorBoundary.wrap(({ channel, whiteDot }: { channel: Channel, whiteDot?: JSX.Element; }) => {
|
||||
const unreadCount = useStateFromStores([ReadStateStore], () => ReadStateStore.getUnreadCount(channel.id));
|
||||
if (!unreadCount) return whiteDot || null;
|
||||
|
||||
if (!settings.store.showOnMutedChannels && (UserGuildSettingsStore.isChannelMuted(channel.guild_id, channel.id) || JoinedThreadsStore.isMuted(channel.id)))
|
||||
return null;
|
||||
const className = `vc-unreadCountBadge${whiteDot ? "-dot" : ""}${channel.threadMetadata ? "-thread" : ""}`;
|
||||
return (
|
||||
<NumberBadge
|
||||
color="var(--brand-500)"
|
||||
className={className}
|
||||
count={
|
||||
unreadCount > 99 && settings.store.notificationCountLimit
|
||||
? "+99"
|
||||
: unreadCount
|
||||
}
|
||||
/>
|
||||
);
|
||||
}, { noop: true }),
|
||||
});
|
23
src/equicordplugins/unreadBadgeCount/styles.css
Normal file
23
src/equicordplugins/unreadBadgeCount/styles.css
Normal file
|
@ -0,0 +1,23 @@
|
|||
.unreadCountBadge {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.vc-unreadCountBadge-dot {
|
||||
position: absolute;
|
||||
top: 25%;
|
||||
margin-left: -3%;
|
||||
scale: 0.9;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.vc-unreadCountBadge-dot-thread {
|
||||
position: absolute;
|
||||
top: 20%;
|
||||
margin-left: -18%;
|
||||
scale: 0.9;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
[class*="modeMuted_"] .unreadCountBadge {
|
||||
display: none;
|
||||
}
|
|
@ -7,7 +7,7 @@
|
|||
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { makeRange } from "@components/PluginSettings/components";
|
||||
import { Devs, EquicordDevs } from "@utils/constants";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findStoreLazy } from "@webpack";
|
||||
import { GuildChannelStore, Menu, React, RestAPI, UserStore } from "@webpack/common";
|
||||
|
@ -159,8 +159,10 @@ const settings = definePluginSettings({
|
|||
export default definePlugin({
|
||||
name: "VoiceChatUtilities",
|
||||
description: "This plugin allows you to perform multiple actions on an entire channel (move, mute, disconnect, etc.) (originally by dutake)",
|
||||
authors: [EquicordDevs.Dams, Devs.D3SOX],
|
||||
authors: [Devs.D3SOX],
|
||||
|
||||
settings,
|
||||
|
||||
contextMenus: {
|
||||
"channel-context": VoiceChannelContext
|
||||
},
|
||||
|
|
|
@ -8,8 +8,11 @@ import { definePluginSettings } from "@api/Settings";
|
|||
import { makeRange } from "@components/PluginSettings/components";
|
||||
import { EquicordDevs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { Text } from "@webpack/common";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
import ExampleWiggle from "./ui/components/ExampleWiggle";
|
||||
|
||||
const settings = definePluginSettings({
|
||||
intensity: {
|
||||
type: OptionType.SLIDER,
|
||||
|
@ -18,22 +21,6 @@ const settings = definePluginSettings({
|
|||
default: 4,
|
||||
stickToMarkers: true,
|
||||
onChange: () => updateStyles()
|
||||
},
|
||||
direction: {
|
||||
type: OptionType.SELECT,
|
||||
description: "Swing direction",
|
||||
options: [{
|
||||
label: "Circle",
|
||||
value: "xy",
|
||||
default: true
|
||||
}, {
|
||||
label: "Horizontal",
|
||||
value: "x",
|
||||
}, {
|
||||
label: "Vertical",
|
||||
value: "y"
|
||||
}],
|
||||
onChange: () => updateStyles()
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -42,56 +29,103 @@ const dirMap = {
|
|||
y: "1.2s wiggle-wavy-y linear infinite"
|
||||
};
|
||||
|
||||
const classMap = [
|
||||
{
|
||||
chars: ["<", ">"],
|
||||
className: "wiggle-inner-x",
|
||||
},
|
||||
{
|
||||
chars: ["^", "^"],
|
||||
className: "wiggle-inner-y",
|
||||
},
|
||||
{
|
||||
chars: [")", "("],
|
||||
className: "wiggle-inner-xy"
|
||||
}
|
||||
];
|
||||
|
||||
let styles: HTMLStyleElement;
|
||||
const updateStyles = () => {
|
||||
const inten = Vencord.Settings.plugins.WigglyText.intensity + "px";
|
||||
const dir = Vencord.Settings.plugins.WigglyText.direction as string;
|
||||
styles.textContent = `
|
||||
.wiggly-inner {
|
||||
animation: ${dir.split("").map(dir => dirMap[dir]).join(", ")};
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
.wiggle-example {
|
||||
list-style-type: disc;
|
||||
list-style-position: outside;
|
||||
margin: 4px 0 0 16px;
|
||||
}
|
||||
|
||||
@keyframes wiggle-wavy-x {
|
||||
from {
|
||||
left: -${inten};
|
||||
}
|
||||
.wiggle-example li {
|
||||
white-space: break-spaces;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
to {
|
||||
left: ${inten};
|
||||
}
|
||||
}
|
||||
.wiggle-inner {
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
@keyframes wiggle-wavy-y {
|
||||
0% {
|
||||
top: 0;
|
||||
animation-timing-function: ease-out;
|
||||
}
|
||||
&.wiggle-inner-x {
|
||||
animation: ${dirMap.x};
|
||||
}
|
||||
|
||||
25% {
|
||||
top: -${inten};
|
||||
animation-timing-function: ease-in;
|
||||
}
|
||||
&.wiggle-inner-y {
|
||||
animation: ${dirMap.y};
|
||||
}
|
||||
|
||||
50% {
|
||||
top: 0;
|
||||
animation-timing-function: ease-out;
|
||||
}
|
||||
&.wiggle-inner-xy {
|
||||
animation: ${dirMap.x}, ${dirMap.y};
|
||||
}
|
||||
}
|
||||
|
||||
75% {
|
||||
top: ${inten};
|
||||
animation-timing-function: ease-in;
|
||||
}
|
||||
}`;
|
||||
@keyframes wiggle-wavy-x {
|
||||
from {
|
||||
left: -${inten};
|
||||
}
|
||||
|
||||
to {
|
||||
left: ${inten};
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes wiggle-wavy-y {
|
||||
0% {
|
||||
top: 0;
|
||||
animation-timing-function: ease-out;
|
||||
}
|
||||
|
||||
25% {
|
||||
top: -${inten};
|
||||
animation-timing-function: ease-in;
|
||||
}
|
||||
|
||||
50% {
|
||||
top: 0;
|
||||
animation-timing-function: ease-out;
|
||||
}
|
||||
|
||||
75% {
|
||||
top: ${inten};
|
||||
animation-timing-function: ease-in;
|
||||
}
|
||||
}`;
|
||||
};
|
||||
|
||||
export default definePlugin({
|
||||
name: "WigglyText",
|
||||
description: "Adds a new markdown formatting that makes text ~wiggly~",
|
||||
description: "Adds a new markdown formatting that makes text wiggly.",
|
||||
authors: [EquicordDevs.nexpid],
|
||||
settings,
|
||||
settingsAboutComponent: () => (
|
||||
<Text>
|
||||
You can make text wiggle with the following:<br />
|
||||
<ul className="wiggle-example">
|
||||
<li><ExampleWiggle wiggle="x">left and right</ExampleWiggle> by typing <code><~text~></code></li>
|
||||
<li><ExampleWiggle wiggle="y">up and down</ExampleWiggle> by typing <code>^~text~^</code></li>
|
||||
<li><ExampleWiggle wiggle="xy">in a circle</ExampleWiggle> by typing <code>)~text~(</code></li>
|
||||
</ul>
|
||||
</Text>
|
||||
),
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: "parseToAST:",
|
||||
|
@ -104,16 +138,21 @@ export default definePlugin({
|
|||
|
||||
wigglyRule: {
|
||||
order: 24,
|
||||
match: (source: string) => source.match(/^~([\s\S]+?)~(?!_)/),
|
||||
match: (source: string) => classMap.map(({ chars }) => source.match(new RegExp(`^(\\${chars[0]})~([\\s\\S]+?)~(\\${chars[1]})(?!_)`))).find(x => x !== null),
|
||||
parse: (
|
||||
capture: RegExpMatchArray,
|
||||
transform: (...args: any[]) => any,
|
||||
state: any
|
||||
) => ({
|
||||
content: transform(capture[1], state),
|
||||
}),
|
||||
) => {
|
||||
const className = classMap.find(({ chars }) => chars[0] === capture[1] && chars[1] === capture[3])?.className ?? "";
|
||||
|
||||
return {
|
||||
content: transform(capture[2], state),
|
||||
className
|
||||
};
|
||||
},
|
||||
react: (
|
||||
data: { content: any[]; },
|
||||
data: { content: any[]; className: string; },
|
||||
output: (...args: any[]) => ReactNode[]
|
||||
) => {
|
||||
let offset = 0;
|
||||
|
@ -129,7 +168,7 @@ export default definePlugin({
|
|||
children[j] = child.split("").map((x, i) => (
|
||||
<span key={i}>
|
||||
<span
|
||||
className="wiggly-inner"
|
||||
className={`wiggle-inner ${data.className}`}
|
||||
style={{
|
||||
animationDelay: `${((offset++) * 25) % 1200}ms`,
|
||||
}}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
export default function ExampleWiggle({ wiggle, children }: { wiggle: "x" | "y" | "xy", children: string; }) {
|
||||
return children.split("").map((x, i) => (
|
||||
<span key={i}>
|
||||
<span
|
||||
className={`wiggle-inner wiggle-inner-${wiggle}`}
|
||||
style={{
|
||||
animationDelay: `${(i * 25) % 1200}ms`,
|
||||
}}
|
||||
>
|
||||
{x}
|
||||
</span>
|
||||
</span>
|
||||
));
|
||||
}
|
|
@ -91,7 +91,7 @@ export default definePlugin({
|
|||
replacement: [
|
||||
// Use Decor avatar decoration hook
|
||||
{
|
||||
match: /(?<=\i\)\({avatarDecoration:)(\i).avatarDecoration(?=,)/,
|
||||
match: /(?<=\i\)\({avatarDecoration:)(\i)(?=,)(?<=currentUser:(\i).+?)/,
|
||||
replace: "$self.useUserDecorAvatarDecoration($1)??$&"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -21,7 +21,7 @@ import { Devs } from "@utils/constants";
|
|||
import { isNonNullish } from "@utils/guards";
|
||||
import definePlugin from "@utils/types";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { Avatar, ChannelStore, Clickable, IconUtils, RelationshipStore, ScrollerThin, UserStore } from "@webpack/common";
|
||||
import { Avatar, ChannelStore, Clickable, IconUtils, RelationshipStore, ScrollerThin, useMemo, UserStore } from "@webpack/common";
|
||||
import { Channel, User } from "discord-types/general";
|
||||
|
||||
const SelectedChannelActionCreators = findByPropsLazy("selectPrivateChannel");
|
||||
|
@ -39,6 +39,19 @@ function getGroupDMName(channel: Channel) {
|
|||
.join(", ");
|
||||
}
|
||||
|
||||
const getMutualGroupDms = (userId: string) =>
|
||||
ChannelStore.getSortedPrivateChannels()
|
||||
.filter(c => c.isGroupDM() && c.recipients.includes(userId));
|
||||
|
||||
const isBotOrSelf = (user: User) => user.bot || user.id === UserStore.getCurrentUser().id;
|
||||
|
||||
function getMutualGDMCountText(user: User) {
|
||||
const count = getMutualGroupDms(user.id).length;
|
||||
return `${count === 0 ? "No" : count} Mutual Group${count !== 1 ? "s" : ""}`;
|
||||
}
|
||||
|
||||
const IS_PATCHED = Symbol("MutualGroupDMs.Patched");
|
||||
|
||||
export default definePlugin({
|
||||
name: "MutualGroupDMs",
|
||||
description: "Shows mutual group dms in profiles",
|
||||
|
@ -63,8 +76,8 @@ export default definePlugin({
|
|||
find: ".MUTUAL_FRIENDS?(",
|
||||
replacement: [
|
||||
{
|
||||
match: /(?<=onItemSelect:\i,children:)(\i)\.map/,
|
||||
replace: "[...$1, ...($self.isBotOrSelf(arguments[0].user) ? [] : [{section:'MUTUAL_GDMS',text:$self.getMutualGDMCountText(arguments[0].user)}])].map"
|
||||
match: /\i\.useEffect.{0,100}(\i)\[0\]\.section/,
|
||||
replace: "$self.pushSection($1, arguments[0].user);$&"
|
||||
},
|
||||
{
|
||||
match: /\(0,\i\.jsx\)\(\i,\{items:\i,section:(\i)/,
|
||||
|
@ -74,15 +87,23 @@ export default definePlugin({
|
|||
}
|
||||
],
|
||||
|
||||
isBotOrSelf: (user: User) => user.bot || user.id === UserStore.getCurrentUser().id,
|
||||
isBotOrSelf,
|
||||
getMutualGDMCountText,
|
||||
|
||||
getMutualGDMCountText: (user: User) => {
|
||||
const count = ChannelStore.getSortedPrivateChannels().filter(c => c.isGroupDM() && c.recipients.includes(user.id)).length;
|
||||
return `${count === 0 ? "No" : count} Mutual Group${count !== 1 ? "s" : ""}`;
|
||||
pushSection(sections: any[], user: User) {
|
||||
if (isBotOrSelf(user) || sections[IS_PATCHED]) return;
|
||||
|
||||
sections[IS_PATCHED] = true;
|
||||
sections.push({
|
||||
section: "MUTUAL_GDMS",
|
||||
text: getMutualGDMCountText(user)
|
||||
});
|
||||
},
|
||||
|
||||
renderMutualGDMs: ErrorBoundary.wrap(({ user, onClose }: { user: User, onClose: () => void; }) => {
|
||||
const entries = ChannelStore.getSortedPrivateChannels().filter(c => c.isGroupDM() && c.recipients.includes(user.id)).map(c => (
|
||||
const mutualDms = useMemo(() => getMutualGroupDms(user.id), [user.id]);
|
||||
|
||||
const entries = mutualDms.map(c => (
|
||||
<Clickable
|
||||
className={ProfileListClasses.listRow}
|
||||
onClick={() => {
|
||||
|
|
|
@ -19,16 +19,11 @@
|
|||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import { findLazy } from "@webpack";
|
||||
import { Constants, GuildStore, i18n, RestAPI } from "@webpack/common";
|
||||
|
||||
const InvitesDisabledExperiment = findLazy(m => m.definition?.id === "2022-07_invites_disabled");
|
||||
|
||||
function showDisableInvites(guildId: string) {
|
||||
// Once the experiment is removed, this should keep working
|
||||
const { enableInvitesDisabled } = InvitesDisabledExperiment?.getCurrentConfig?.({ guildId }) ?? { enableInvitesDisabled: true };
|
||||
// @ts-ignore
|
||||
return enableInvitesDisabled && !GuildStore.getGuild(guildId).hasFeature("INVITES_DISABLED");
|
||||
return !GuildStore.getGuild(guildId).hasFeature("INVITES_DISABLED");
|
||||
}
|
||||
|
||||
function disableInvites(guildId: string) {
|
||||
|
|
|
@ -24,13 +24,13 @@ const settings = definePluginSettings({
|
|||
export default definePlugin({
|
||||
name: "PictureInPicture",
|
||||
description: "Adds picture in picture to videos (next to the Download button)",
|
||||
authors: [Devs.Nobody],
|
||||
authors: [Devs.Lumap],
|
||||
settings,
|
||||
patches: [
|
||||
{
|
||||
find: ".nonMediaMosaicItem]",
|
||||
find: ".removeMosaicItemHoverButton),",
|
||||
replacement: {
|
||||
match: /\.nonMediaMosaicItem\]:!(\i).{0,50}?children:\[(\S)/,
|
||||
match: /\.nonMediaMosaicItem\]:!(\i).{0,50}?children:\[\S,(\S)/,
|
||||
replace: "$&,$1&&$2&&$self.renderPiPButton(),"
|
||||
},
|
||||
},
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--background-secondary);
|
||||
background-color: var(--background-primary);
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
border-radius: 100%;
|
||||
|
|
|
@ -51,7 +51,7 @@ export default definePlugin({
|
|||
},
|
||||
},
|
||||
{
|
||||
find: "2022-07_invites_disabled",
|
||||
find: "INVITES_DISABLED))||",
|
||||
predicate: () => settings.store.showInvitesPaused,
|
||||
replacement: {
|
||||
match: /\i\.\i\.can\(\i\.\i.MANAGE_GUILD,\i\)/,
|
||||
|
|
|
@ -55,6 +55,7 @@ interface PlayerState {
|
|||
|
||||
// added by patch
|
||||
actual_repeat: Repeat;
|
||||
shuffle: boolean;
|
||||
}
|
||||
|
||||
interface Device {
|
||||
|
@ -182,6 +183,7 @@ export const SpotifyStore = proxyLazyWebpack(() => {
|
|||
store.isPlaying = e.isPlaying ?? false;
|
||||
store.volume = e.volumePercent ?? 0;
|
||||
store.repeat = e.actual_repeat || "off";
|
||||
store.shuffle = e.shuffle ?? false;
|
||||
store.position = e.position ?? 0;
|
||||
store.isSettingPosition = false;
|
||||
store.emitChange();
|
||||
|
|
|
@ -70,21 +70,20 @@ export default definePlugin({
|
|||
replace: "false",
|
||||
}]
|
||||
},
|
||||
// Discord doesn't give you the repeat kind, only a boolean
|
||||
{
|
||||
find: 'repeat:"off"!==',
|
||||
replacement: {
|
||||
match: /repeat:"off"!==(.{1,3}),/,
|
||||
replace: "actual_repeat:$1,$&"
|
||||
}
|
||||
replacement: [
|
||||
{
|
||||
// Discord doesn't give you shuffle state and the repeat kind, only a boolean
|
||||
match: /repeat:"off"!==(\i),/,
|
||||
replace: "shuffle:arguments[2]?.shuffle_state??false,actual_repeat:$1,$&"
|
||||
},
|
||||
{
|
||||
match: /(?<=artists.filter\(\i=>).{0,10}\i\.id\)&&/,
|
||||
replace: ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
find: "artists.filter",
|
||||
replacement: {
|
||||
match: /(?<=artists.filter\(\i=>).{0,10}\i\.id\)&&/,
|
||||
replace: ""
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
start: () => toggleHoverControls(Settings.plugins.SpotifyControls.hoverControls),
|
||||
|
|
|
@ -22,7 +22,6 @@ import definePlugin, { OptionType } from "@utils/types";
|
|||
import { saveFile } from "@utils/web";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { Clipboard, ComponentDispatch } from "@webpack/common";
|
||||
|
||||
const ctxMenuCallbacks = findByPropsLazy("contextMenuCallbackNative");
|
||||
|
||||
async function fetchImage(url: string) {
|
||||
|
@ -41,7 +40,7 @@ const settings = definePluginSettings({
|
|||
description: "Add back the Discord context menus for images, links and the chat input bar",
|
||||
// Web slate menu has proper spellcheck suggestions and image context menu is also pretty good,
|
||||
// so disable this by default. Vesktop just doesn't, so enable by default
|
||||
default: IS_VESKTOP || IS_EQUIBOP,
|
||||
default: IS_VESKTOP && !IS_EQUIBOP || !IS_VESKTOP && IS_EQUIBOP,
|
||||
restartNeeded: true
|
||||
}
|
||||
});
|
||||
|
|
|
@ -35,10 +35,6 @@ export interface Dev {
|
|||
* If you are fine with attribution but don't want the badge, add badge: false
|
||||
*/
|
||||
export const Devs = /* #__PURE__*/ Object.freeze({
|
||||
Nobody: {
|
||||
name: "Nobody",
|
||||
id: 0n,
|
||||
},
|
||||
Ven: {
|
||||
name: "Vee",
|
||||
id: 343383572805058560n
|
||||
|
@ -547,13 +543,33 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
|||
surgedevs: {
|
||||
name: "Chloe",
|
||||
id: 1084592643784331324n
|
||||
}
|
||||
},
|
||||
Lumap: {
|
||||
name: "Lumap",
|
||||
id: 585278686291427338n,
|
||||
},
|
||||
} satisfies Record<string, Dev>);
|
||||
|
||||
export const EquicordDevs = Object.freeze({
|
||||
vmohammad: {
|
||||
name: "vMohammad",
|
||||
id: 921098159348924457n
|
||||
thororen: {
|
||||
name: "thororen",
|
||||
id: 848339671629299742n
|
||||
},
|
||||
nyx: {
|
||||
name: "verticalsync",
|
||||
id: 328165170536775680n,
|
||||
},
|
||||
Cortex: {
|
||||
name: "Cortex",
|
||||
id: 825069530376044594n,
|
||||
},
|
||||
KrystalSkull: {
|
||||
name: "krystalskullofficial",
|
||||
id: 929208515883569182n
|
||||
},
|
||||
Naibuu: {
|
||||
name: "hs50",
|
||||
id: 1120045713867423835n,
|
||||
},
|
||||
nexpid: {
|
||||
name: "Nexpid",
|
||||
|
@ -575,10 +591,6 @@ export const EquicordDevs = Object.freeze({
|
|||
name: "ryan",
|
||||
id: 479403382994632704n
|
||||
},
|
||||
thororen: {
|
||||
name: "thororen",
|
||||
id: 848339671629299742n
|
||||
},
|
||||
MrDiamond: {
|
||||
name: "MrDiamond",
|
||||
id: 523338295644782592n
|
||||
|
@ -615,18 +627,10 @@ export const EquicordDevs = Object.freeze({
|
|||
name: "cooles",
|
||||
id: 406084422308331522n,
|
||||
},
|
||||
KrystalSkull: {
|
||||
name: "krystalskullofficial",
|
||||
id: 929208515883569182n
|
||||
},
|
||||
SerStars: {
|
||||
name: "SerStars",
|
||||
id: 861631850681729045n
|
||||
},
|
||||
nyx: {
|
||||
name: "verticalsync",
|
||||
id: 328165170536775680n,
|
||||
},
|
||||
MaxHerbold: {
|
||||
name: "MaxHerbold",
|
||||
id: 1189527130611138663n,
|
||||
|
@ -639,13 +643,9 @@ export const EquicordDevs = Object.freeze({
|
|||
name: "Megal",
|
||||
id: 387790666484285441n
|
||||
},
|
||||
Cortex: {
|
||||
name: "Cortex",
|
||||
id: 825069530376044594n,
|
||||
},
|
||||
Woosh: {
|
||||
name: "w00shh.",
|
||||
id: 689165844835860522n,
|
||||
id: 919239894327521361n,
|
||||
},
|
||||
Hanzy: {
|
||||
name: "hanzydev",
|
||||
|
@ -683,10 +683,6 @@ export const EquicordDevs = Object.freeze({
|
|||
name: "Fafa",
|
||||
id: 428188716641812481n,
|
||||
},
|
||||
Naibuu: {
|
||||
name: "hs50",
|
||||
id: 1120045713867423835n,
|
||||
},
|
||||
Colorman: {
|
||||
name: "colorman",
|
||||
id: 298842558610800650n,
|
||||
|
@ -743,6 +739,18 @@ export const EquicordDevs = Object.freeze({
|
|||
name: "sadan",
|
||||
id: 521819891141967883n
|
||||
},
|
||||
x3rt: {
|
||||
name: "x3rt",
|
||||
id: 131602100332396544n
|
||||
},
|
||||
Hen: {
|
||||
name: "Hen",
|
||||
id: 279266228151779329n
|
||||
},
|
||||
vmohammad: {
|
||||
name: "vMohammad",
|
||||
id: 921098159348924457n
|
||||
},
|
||||
} satisfies Record<string, Dev>);
|
||||
|
||||
// iife so #__PURE__ works correctly
|
||||
|
|
Loading…
Add table
Reference in a new issue