mirror of
https://github.com/Equicord/Equicord.git
synced 2025-01-18 13:23:28 -05:00
More Plugins
This commit is contained in:
parent
b4587182c2
commit
167612920b
14 changed files with 1605 additions and 3 deletions
13
README.md
13
README.md
|
@ -25,13 +25,16 @@ An enhanced version of [Vencord](https://github.com/Vendicated/Vencord) by [Vend
|
|||
|
||||
- AllCallTimers by MaxHerbold and D3SOX
|
||||
- AltKrispSwitch by newwares
|
||||
- AmITyping by MrDiamond
|
||||
- Anammox by Kyuuhachi
|
||||
- BetterActivities by D3SOX, Arjix, AutumnVN
|
||||
- BetterBanReasons by Inbestigator
|
||||
- BetterQuickReact by Ven and Sqaaakoi
|
||||
- BlockKrsip by D3SOX
|
||||
- BypassDND by Inbestigator
|
||||
- CleanChannelName by AutumnVN
|
||||
- ColorMessage by Kyuuhachi
|
||||
- CommandPalette by Ethan
|
||||
- CopyUserMention by Cortex and castdrian
|
||||
- CustomAppIcons by Happy Enderman and SerStars
|
||||
- DeadMembers by Kyuuhachi
|
||||
|
@ -54,14 +57,20 @@ An enhanced version of [Vencord](https://github.com/Vendicated/Vencord) by [Vend
|
|||
- IRememberYou by zoodogood
|
||||
- KeyboardSounds by HypedDomi
|
||||
- KeywordNotify by camila314
|
||||
- MediaDownloader by Colorman
|
||||
- Meow by Samwich
|
||||
- MessageLinkTooltip by Kyuuhachi
|
||||
- MessageLoggerEnhanced by Aria
|
||||
- ModalFade by Kyuuhachi
|
||||
- noAppsAllowed by kvba
|
||||
- NoModalAnimation by AutumnVN
|
||||
- NoNitroUpsell by thororen
|
||||
- NotificationTitle by Kyuuhachi
|
||||
- NotifyUserChanges by D3SOX
|
||||
- OnePingPerDM by ProffDea
|
||||
- PhilsBetterMicrophone by Philhk
|
||||
- PhilsBetterScreenshare by Philhk and walrus
|
||||
- PhilsPluginLibrary by Philhk
|
||||
- PlatformSpoofer by Drag
|
||||
- PurgeMessages by bhop and nyx
|
||||
- Quest Completer by HappyEnderman, SerStars, thororen
|
||||
|
@ -76,13 +85,17 @@ An enhanced version of [Vencord](https://github.com/Vendicated/Vencord) by [Vend
|
|||
- Slap by Korbo
|
||||
- SoundBoardLogger by Moxxie, fres, echo, thororen
|
||||
- TalkInReverse by Tolgchu
|
||||
- TeX by Kyuuhachi
|
||||
- ThemeLibrary by Fafa
|
||||
- Title by Kyuuhachi
|
||||
- TosuRPC by AutumnVN
|
||||
- UnlimitedAccounts by Balaclava and thororen
|
||||
- UserPFP by nexpid and thororen
|
||||
- VCSupport by thororen
|
||||
- VencordRPC by AutumnVN
|
||||
- ViewRaw2 by Kyuuhachi
|
||||
- VoiceChatUtilities by Dams and D3SOX
|
||||
- WebpackTarball by Kyuuhachi
|
||||
- WhosWatching by fres
|
||||
- Woof by Samwich
|
||||
- YoutubeDescription by arHSM
|
||||
|
|
93
src/equicordplugins/TeX/index.tsx
Normal file
93
src/equicordplugins/TeX/index.tsx
Normal file
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./style.css";
|
||||
|
||||
import { Devs } from "@utils/constants";
|
||||
import { classes } from "@utils/misc";
|
||||
import definePlugin from "@utils/types";
|
||||
import { React, Tooltip, useMemo } from "@webpack/common";
|
||||
|
||||
import { useKatex } from "./katexLoader";
|
||||
|
||||
export default definePlugin({
|
||||
name: "TeX",
|
||||
description: "Typesets math in messages, written as `$x$` or `$$x$$`.",
|
||||
authors: [Devs.Kyuuhachi],
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: "inlineCode:{react",
|
||||
replacement: {
|
||||
match: /inlineCode:\{react:\((\i,\i,\i)\)=>/,
|
||||
replace: "$&$self.render($1)??"
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
render({ content }) {
|
||||
const displayMatch = /^\$\$(.*)\$\$$/.exec(content);
|
||||
const inlineMatch = /^\$(.*)\$$/.exec(content);
|
||||
if (displayMatch)
|
||||
return <LazyLatex displayMode formula={displayMatch[1]} delim="$$" />;
|
||||
if (inlineMatch)
|
||||
return <LazyLatex formula={inlineMatch[1]} delim="$" />;
|
||||
}
|
||||
});
|
||||
|
||||
function LazyLatex(props) {
|
||||
const { formula, delim } = props;
|
||||
const katex = useKatex();
|
||||
return katex
|
||||
? <Latex {...props} katex={katex} />
|
||||
: <LatexPlaceholder className="tex-loading" delim={delim}>{formula}</LatexPlaceholder>;
|
||||
}
|
||||
|
||||
function Latex({ katex, formula, displayMode, delim }) {
|
||||
const result = useMemo(() => {
|
||||
try {
|
||||
const html = katex.renderToString(formula, { displayMode });
|
||||
return { html };
|
||||
} catch (error) {
|
||||
return { error };
|
||||
}
|
||||
}, [formula, displayMode]);
|
||||
|
||||
return result.html
|
||||
? <span className="tex" dangerouslySetInnerHTML={{ __html: result.html }} />
|
||||
: <LatexError formula={formula} delim={delim} error={result.error} />;
|
||||
}
|
||||
|
||||
function LatexError({ formula, delim, error }) {
|
||||
const { rawMessage, position, length } = error;
|
||||
const pre = formula.slice(0, position);
|
||||
const mid = formula.slice(position, position + length);
|
||||
const suf = formula.slice(position + length);
|
||||
return (
|
||||
<Tooltip text={rawMessage}>
|
||||
{({ onMouseLeave, onMouseEnter }) => (
|
||||
<LatexPlaceholder
|
||||
onMouseLeave={onMouseLeave}
|
||||
onMouseEnter={onMouseEnter}
|
||||
delim={delim}
|
||||
className="tex-error"
|
||||
>
|
||||
{pre}<strong>{mid}</strong>{suf}
|
||||
</LatexPlaceholder>
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
function LatexPlaceholder({ className, delim, children, ...props }) {
|
||||
return (
|
||||
<code className={classes(className, "tex-placeholder inline")} {...props}>
|
||||
{delim}
|
||||
{children}
|
||||
{delim}
|
||||
</code>
|
||||
);
|
||||
}
|
29
src/equicordplugins/TeX/katexLoader.ts
Normal file
29
src/equicordplugins/TeX/katexLoader.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { makeLazy } from "@utils/lazy";
|
||||
import { useEffect, useState } from "@webpack/common";
|
||||
|
||||
const SCRIPT_URL = "https://unpkg.com/katex@0.16.9/dist/katex.mjs";
|
||||
const STYLE_URL = "https://unpkg.com/katex@0.16.9/dist/katex.min.css";
|
||||
|
||||
let theKatex: undefined | any = undefined;
|
||||
export const loadKatex = makeLazy(async () => {
|
||||
const style = document.createElement("link");
|
||||
style.setAttribute("rel", "stylesheet");
|
||||
style.setAttribute("href", STYLE_URL);
|
||||
document.head.appendChild(style);
|
||||
return theKatex = (await import(SCRIPT_URL)).default;
|
||||
});
|
||||
|
||||
export function useKatex() {
|
||||
const [katex, setKatex] = useState(theKatex);
|
||||
useEffect(() => {
|
||||
if (katex === undefined)
|
||||
loadKatex().then(setKatex);
|
||||
});
|
||||
return katex;
|
||||
}
|
8
src/equicordplugins/TeX/style.css
Normal file
8
src/equicordplugins/TeX/style.css
Normal file
|
@ -0,0 +1,8 @@
|
|||
.katex-display {
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.tex-error {
|
||||
color: var(--status-danger);
|
||||
}
|
|
@ -24,7 +24,20 @@ export default definePlugin({
|
|||
|
||||
cleanChannelName(channel?: Channel) {
|
||||
if (channel) {
|
||||
channel.name = channel.name.normalize("NFKC").replace(/[^\u0020-\u007E]?\p{Extended_Pictographic}[^\u0020-\u007E]?/ug, "").replace(/-?[^\p{Letter}\u0020-\u007E]-?/ug, [2, 4].includes(channel.type) ? " " : "-").replace(/(^-|-$)/g, "");
|
||||
channel.name = channel.name
|
||||
.normalize("NFKC")
|
||||
.replace(/[ᴀʙᴄᴅᴇꜰɢʜɪᴊᴋʟᴍɴᴏᴘǫʀꜱᴛᴜᴠᴡxʏᴢ]/g, match => {
|
||||
const smallCapsToNormal = {
|
||||
"ᴀ": "a", "ʙ": "b", "ᴄ": "c", "ᴅ": "d", "ᴇ": "e", "ꜰ": "f", "ɢ": "g", "ʜ": "h", "ɪ": "i", "ᴊ": "j",
|
||||
"ᴋ": "k", "ʟ": "l", "ᴍ": "m", "ɴ": "n", "ᴏ": "o", "ᴘ": "p", "ǫ": "q", "ʀ": "r", "ꜱ": "s", "ᴛ": "t",
|
||||
"ᴜ": "u", "ᴠ": "v", "ᴡ": "w", "x": "x", "ʏ": "y", "ᴢ": "z"
|
||||
};
|
||||
return smallCapsToNormal[match];
|
||||
})
|
||||
.replace(/[^\u0020-\u007E]?\p{Extended_Pictographic}[^\u0020-\u007E]?/ug, "")
|
||||
.replace(/-?[^\p{Letter}\u0020-\u007E]-?/ug, [2, 4].includes(channel.type) ? " " : "-")
|
||||
.replace(/(^-|-$)/g, "")
|
||||
.replace(/-+/g, "-");
|
||||
}
|
||||
return channel;
|
||||
}
|
||||
|
|
99
src/equicordplugins/modalFade/index.tsx
Normal file
99
src/equicordplugins/modalFade/index.tsx
Normal file
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* 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 { proxyLazy } from "@utils/lazy";
|
||||
import definePlugin from "@utils/types";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { Forms, useEffect, useRef } from "@webpack/common";
|
||||
import type { StoreApi, UseBoundStore } from "zustand";
|
||||
|
||||
type Modal = {
|
||||
Layer?: any,
|
||||
instant?: boolean,
|
||||
backdropStyle?: "SUBTLE" | "DARK" | "BLUR",
|
||||
};
|
||||
|
||||
const { useModalContext, useModalsStore } = proxyLazy(() => Forms as any as {
|
||||
useModalContext(): "default" | "popout";
|
||||
useModalsStore: UseBoundStore<StoreApi<{
|
||||
default: Modal[];
|
||||
popout: Modal[];
|
||||
}>>,
|
||||
});
|
||||
|
||||
const { animated, useSpring, useTransition } = findByPropsLazy("a", "animated", "useTransition");
|
||||
// This doesn't seem to be necessary
|
||||
// const { default: AppLayer } = findByPropsLazy("AppLayerContainer", "AppLayerProvider");
|
||||
|
||||
const ANIMS = {
|
||||
SUBTLE: {
|
||||
off: { opacity: 1 },
|
||||
on: { opacity: 0.9 },
|
||||
},
|
||||
DARK: {
|
||||
off: { opacity: 1 },
|
||||
on: { opacity: 0.7 },
|
||||
},
|
||||
BLUR: {
|
||||
off: { opacity: 1, filter: "blur(0px)" },
|
||||
on: { opacity: 0.7, filter: "blur(8px)" },
|
||||
},
|
||||
};
|
||||
|
||||
export default definePlugin({
|
||||
name: "ModalFade",
|
||||
description: "Makes modals fade the backdrop, rather than dimming",
|
||||
authors: [Devs.Kyuuhachi],
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: "contextMenuCallbackNative,!1",
|
||||
replacement: {
|
||||
match: /(?<=\()"div"(?=,\{className:\i\(\)\(\i\?\i\.mobileApp:\i.app\))/,
|
||||
replace: "$self.MainWrapper",
|
||||
}
|
||||
},
|
||||
{
|
||||
find: "{})).SUBTLE=\"SUBTLE\"",
|
||||
replacement: {
|
||||
match: /\(0,\i\.useTransition\)*/,
|
||||
replace: "$self.nullTransition"
|
||||
}
|
||||
},
|
||||
],
|
||||
|
||||
nullTransition(value: any, args: object) {
|
||||
return useTransition(value, {
|
||||
...args,
|
||||
from: {},
|
||||
enter: { _: 0 }, // Spring gets unhappy if there's zero animations
|
||||
leave: {},
|
||||
});
|
||||
},
|
||||
|
||||
MainWrapper(props: object) {
|
||||
const context = useModalContext();
|
||||
const modals = useModalsStore(modals => modals[context] ?? []);
|
||||
const modal = modals.findLast(modal => modal.Layer == null); // || modal.Layer === AppLayer
|
||||
const anim = ANIMS[modal?.backdropStyle ?? "DARK"];
|
||||
const isInstant = modal?.instant;
|
||||
const prevIsInstant = usePrevious(isInstant);
|
||||
const style = useSpring({
|
||||
config: { duration: isInstant || prevIsInstant ? 0 : 300 },
|
||||
...modal != null ? anim.on : anim.off,
|
||||
});
|
||||
return <animated.div style={style} {...props} />;
|
||||
}
|
||||
});
|
||||
|
||||
function usePrevious<T>(value: T | undefined): T | undefined {
|
||||
const ref = useRef<T>();
|
||||
useEffect(() => {
|
||||
ref.current = value;
|
||||
}, [value]);
|
||||
return ref.current;
|
||||
}
|
69
src/equicordplugins/notificationTitle/index.tsx
Normal file
69
src/equicordplugins/notificationTitle/index.tsx
Normal file
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* 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";
|
||||
import { findByCodeLazy, findByPropsLazy } from "@webpack";
|
||||
import { ChannelStore, GuildStore, i18n, RelationshipStore, UserStore } from "@webpack/common";
|
||||
|
||||
const { getName } = findByPropsLazy("getName", "useName", "getNickname");
|
||||
const computeChannelName = findByCodeLazy(".isThread())return'\"'.concat(");
|
||||
|
||||
const ChannelTypes = findByPropsLazy("DM", "GUILD_TEXT", "PUBLIC_THREAD", "UNKNOWN");
|
||||
const ChannelTypesSets = findByPropsLazy("THREADS", "GUILD_TEXTUAL", "ALL_DMS");
|
||||
const MessageTypes = findByPropsLazy("REPLY", "STAGE_RAISE_HAND", "CHANNEL_NAME_CHANGE");
|
||||
|
||||
|
||||
export default definePlugin({
|
||||
name: "NotificationTitle",
|
||||
description: "Makes desktop notifications more informative",
|
||||
authors: [Devs.Kyuuhachi],
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: '"SystemMessageUtils.stringify(...) could not convert"',
|
||||
replacement: {
|
||||
match: /{icon:.*?}/,
|
||||
replace: "($self.makeTitle($&,...arguments))",
|
||||
}
|
||||
},
|
||||
],
|
||||
|
||||
makeTitle(result, channel, message, user) {
|
||||
const username = getName(channel.guild_id, channel.id, user);
|
||||
|
||||
let title = username;
|
||||
if (message.type === MessageTypes.REPLY && message.referenced_message?.author) {
|
||||
const replyUser = UserStore.getUser(message.referenced_message.author.id);
|
||||
const replyUsername = getName(channel.guild_id, channel.id, replyUser);
|
||||
title = i18n.Messages.CHANNEL_MESSAGE_REPLY_A11Y_LABEL.format({
|
||||
author: username,
|
||||
repliedAuthor: replyUsername,
|
||||
});
|
||||
}
|
||||
|
||||
const guild = GuildStore.getGuild(channel.guild_id);
|
||||
const parent = ChannelStore.getChannel(channel.parent_id);
|
||||
|
||||
if (channel.type !== ChannelTypes.DM) {
|
||||
let where = ChannelTypesSets.THREADS.has(channel.type)
|
||||
? `${channelName(channel)} in ${channelName(parent, true)}`
|
||||
: `${channelName(channel, true)}`;
|
||||
if (guild != null)
|
||||
where += `, ${guild.name}`;
|
||||
title += `\n(${where})`;
|
||||
}
|
||||
result.title = title;
|
||||
|
||||
console.log({ ...result, channel, message, user });
|
||||
|
||||
return result;
|
||||
}
|
||||
});
|
||||
|
||||
function channelName(channel, withPrefix = false) {
|
||||
return computeChannelName(channel, UserStore, RelationshipStore, withPrefix);
|
||||
}
|
219
src/equicordplugins/tosuRPC/index.ts
Normal file
219
src/equicordplugins/tosuRPC/index.ts
Normal file
|
@ -0,0 +1,219 @@
|
|||
/*
|
||||
* 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";
|
||||
import { ApplicationAssetUtils, FluxDispatcher } from "@webpack/common";
|
||||
|
||||
import { Activity, ActivityType, BanchoStatusEnum, GameState, Modes, TosuApi, UserLoginStatus } from "./type";
|
||||
|
||||
const socketId = "tosu";
|
||||
const OSU_APP_ID = "367827983903490050";
|
||||
const OSU_LARGE_IMAGE = "373344233077211136";
|
||||
const OSU_STARDARD_SMALL_IMAGE = "373370493127884800";
|
||||
const OSU_MANIA_SMALL_IMAGE = "373370588703621136";
|
||||
const OSU_TAIKO_SMALL_IMAGE = "373370519891738624";
|
||||
const OSU_CATCH_SMALL_IMAGE = "373370543161999361";
|
||||
|
||||
const throttledOnMessage = throttle(onMessage, 3000, () => FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", activity: null, socketId: "tosu" }));
|
||||
|
||||
let ws: WebSocket;
|
||||
let wsReconnect: NodeJS.Timeout;
|
||||
|
||||
export default definePlugin({
|
||||
name: "TosuRPC",
|
||||
description: "osu! RPC with data from tosu",
|
||||
authors: [Devs.AutumnVN],
|
||||
start() {
|
||||
(function connect() {
|
||||
ws = new WebSocket("ws://localhost:24050/websocket/v2");
|
||||
ws.addEventListener("error", () => ws.close());
|
||||
ws.addEventListener("close", () => wsReconnect = setTimeout(connect, 5000));
|
||||
ws.addEventListener("message", ({ data }) => throttledOnMessage(data));
|
||||
})();
|
||||
},
|
||||
stop() {
|
||||
ws.close();
|
||||
clearTimeout(wsReconnect);
|
||||
FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", activity: null, socketId: "tosu" });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
async function onMessage(data: string) {
|
||||
const json: TosuApi = JSON.parse(data);
|
||||
// @ts-ignore
|
||||
if (json.error) return FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", activity: null, socketId: "tosu" });
|
||||
|
||||
const { state, session, profile, beatmap, play, resultsScreen } = json;
|
||||
|
||||
const activity: Activity = {
|
||||
application_id: OSU_APP_ID,
|
||||
name: "osu!",
|
||||
type: ActivityType.PLAYING,
|
||||
assets: {
|
||||
large_image: OSU_LARGE_IMAGE,
|
||||
large_text: profile.userStatus.number === UserLoginStatus.Connected ? `${profile.name} | #${profile.globalRank} | ${Math.round(profile.pp)}pp` : undefined,
|
||||
},
|
||||
timestamps: {
|
||||
start: Date.now() - session.playTime
|
||||
},
|
||||
flags: 1 << 0,
|
||||
};
|
||||
|
||||
switch (profile.mode.number) {
|
||||
case Modes.Osu:
|
||||
activity.assets.small_image = OSU_STARDARD_SMALL_IMAGE;
|
||||
activity.assets.small_text = "osu!";
|
||||
break;
|
||||
case Modes.Mania:
|
||||
activity.assets.small_image = OSU_MANIA_SMALL_IMAGE;
|
||||
activity.assets.small_text = "osu!mania";
|
||||
break;
|
||||
case Modes.Taiko:
|
||||
activity.assets.small_image = OSU_TAIKO_SMALL_IMAGE;
|
||||
activity.assets.small_text = "osu!taiko";
|
||||
break;
|
||||
case Modes.Fruits:
|
||||
activity.assets.small_image = OSU_CATCH_SMALL_IMAGE;
|
||||
activity.assets.small_text = "osu!catch";
|
||||
break;
|
||||
}
|
||||
|
||||
let player = "";
|
||||
let mods = "";
|
||||
let fc = "";
|
||||
let combo = "";
|
||||
let h100 = "";
|
||||
let h50 = "";
|
||||
let h0 = "";
|
||||
let sb = "";
|
||||
let pp = "";
|
||||
switch (state.number) {
|
||||
case GameState.Play:
|
||||
activity.type = profile.banchoStatus.number === BanchoStatusEnum.Playing ? ActivityType.PLAYING : ActivityType.WATCHING;
|
||||
|
||||
player = profile.banchoStatus.number === BanchoStatusEnum.Playing ? "" : `${play.playerName} | `;
|
||||
mods = play.mods.name ? `+${play.mods.name} ` : "";
|
||||
activity.name = `${player}${beatmap.artist} - ${beatmap.title} [${beatmap.version}] ${mods}(${beatmap.mapper}, ${beatmap.stats.stars.total.toFixed(2)}*)`;
|
||||
|
||||
combo = play.hits[0] === 0 && play.hits.sliderBreaks === 0
|
||||
? `${play.combo.current}x`
|
||||
: `${play.combo.current}x/${play.combo.max}x`;
|
||||
pp = play.hits[0] === 0 && play.hits.sliderBreaks === 0
|
||||
? `${Math.round(play.pp.current)}pp`
|
||||
: `${Math.round(play.pp.current)}pp/${Math.round(play.pp.fc)}pp`;
|
||||
activity.details = `${play.accuracy.toFixed(2)}% | ${combo} | ${pp}`;
|
||||
|
||||
h100 = play.hits[100] > 0 ? `${play.hits[100]}x100` : "";
|
||||
h50 = play.hits[50] > 0 ? `${play.hits[50]}x50` : "";
|
||||
h0 = play.hits[0] > 0 ? `${play.hits[0]}xMiss` : "";
|
||||
sb = play.hits.sliderBreaks > 0 ? `${play.hits.sliderBreaks}xSB` : "";
|
||||
activity.state = [h100, h50, h0, sb].filter(Boolean).join(" | ");
|
||||
|
||||
const playRank = await getAsset(`https://raw.githubusercontent.com/AutumnVN/gosu-rich-presence/main/grade/${play.rank.current.toLowerCase().replace("x", "ss")}.png`);
|
||||
activity.assets.small_image = playRank;
|
||||
activity.assets.small_text = undefined;
|
||||
break;
|
||||
case GameState.ResultScreen:
|
||||
activity.type = ActivityType.WATCHING;
|
||||
|
||||
mods = resultsScreen.mods.name ? `+${resultsScreen.mods.name} ` : "";
|
||||
activity.name = `${resultsScreen.playerName} | ${beatmap.artist} - ${beatmap.title} [${beatmap.version}] ${mods}(${beatmap.mapper}, ${beatmap.stats.stars.total.toFixed(2)}*)`;
|
||||
|
||||
fc = resultsScreen.maxCombo === beatmap.stats.maxCombo ? "FC" : `| ${resultsScreen.maxCombo}x/${beatmap.stats.maxCombo}x`;
|
||||
pp = !resultsScreen.pp.current ? ""
|
||||
: Math.round(resultsScreen.pp.current) === Math.round(resultsScreen.pp.fc)
|
||||
? `| ${Math.round(resultsScreen.pp.current)}pp`
|
||||
: `| ${Math.round(resultsScreen.pp.current)}pp/${Math.round(resultsScreen.pp.fc)}pp`;
|
||||
activity.details = `${resultsScreen.accuracy.toFixed(2)}% ${fc} ${pp}`;
|
||||
|
||||
h100 = resultsScreen.hits[100] > 0 ? `${resultsScreen.hits[100]}x100` : "";
|
||||
h50 = resultsScreen.hits[50] > 0 ? `${resultsScreen.hits[50]}x50` : "";
|
||||
h0 = resultsScreen.hits[0] > 0 ? `${resultsScreen.hits[0]}xMiss` : "";
|
||||
sb = play.hits.sliderBreaks > 0 ? `${play.hits.sliderBreaks}xSB` : "";
|
||||
activity.state = [h100, h50, h0].filter(Boolean).join(" | ");
|
||||
|
||||
const resultRank = await getAsset(`https://raw.githubusercontent.com/AutumnVN/gosu-rich-presence/main/grade/${resultsScreen.rank.toLowerCase().replace("x", "ss")}.png`);
|
||||
activity.assets.small_image = resultRank;
|
||||
activity.assets.small_text = undefined;
|
||||
break;
|
||||
default:
|
||||
activity.type = ActivityType.LISTENING;
|
||||
mods = play.mods.name ? `+${play.mods.name} ` : "";
|
||||
activity.name = `${beatmap.artist} - ${beatmap.title} [${beatmap.version}] ${mods}(${beatmap.mapper}, ${beatmap.stats.stars.total.toFixed(2)}*)`;
|
||||
|
||||
switch (state.number) {
|
||||
case GameState.Menu: activity.details = "Main Menu"; break;
|
||||
case GameState.Edit: activity.details = "Edit"; break;
|
||||
case GameState.SelectEdit: activity.details = "Song Select (Edit)"; break;
|
||||
case GameState.SelectPlay: activity.details = "Song Select (Play)"; break;
|
||||
case GameState.SelectDrawings: activity.details = "Select Drawings"; break;
|
||||
case GameState.Update: activity.details = "Update"; break;
|
||||
case GameState.Busy: activity.details = "Busy"; break;
|
||||
case GameState.Lobby: activity.details = "Lobby"; break;
|
||||
case GameState.MatchSetup: activity.details = "Match Setup"; break;
|
||||
case GameState.SelectMulti: activity.details = "Select Multi"; break;
|
||||
case GameState.RankingVs: activity.details = "Ranking Vs"; break;
|
||||
case GameState.OnlineSelection: activity.details = "Online Selection"; break;
|
||||
case GameState.OptionsOffsetWizard: activity.details = "Options Offset Wizard"; break;
|
||||
case GameState.RankingTagCoop: activity.details = "Ranking Tag Coop"; break;
|
||||
case GameState.RankingTeam: activity.details = "Ranking Team"; break;
|
||||
case GameState.BeatmapImport: activity.details = "Beatmap Import"; break;
|
||||
case GameState.PackageUpdater: activity.details = "Package Updater"; break;
|
||||
case GameState.Benchmark: activity.details = "Benchmark"; break;
|
||||
case GameState.Tourney: activity.details = "Tourney"; break;
|
||||
case GameState.Charts: activity.details = "Charts"; break;
|
||||
}
|
||||
|
||||
switch (profile.banchoStatus.number) {
|
||||
case BanchoStatusEnum.Idle: activity.state = "Idle"; break;
|
||||
case BanchoStatusEnum.Afk: activity.state = "AFK"; break;
|
||||
case BanchoStatusEnum.Playing: activity.state = "Playing"; break;
|
||||
case BanchoStatusEnum.Editing: activity.state = "Editing"; break;
|
||||
case BanchoStatusEnum.Modding: activity.state = "Modding"; break;
|
||||
case BanchoStatusEnum.Multiplayer: activity.state = "Multiplayer"; break;
|
||||
case BanchoStatusEnum.Watching: activity.state = "Watching"; break;
|
||||
case BanchoStatusEnum.Testing: activity.state = "Testing"; break;
|
||||
case BanchoStatusEnum.Submitting: activity.state = "Submitting"; break;
|
||||
case BanchoStatusEnum.Paused: activity.state = "Paused"; break;
|
||||
case BanchoStatusEnum.Lobby: activity.state = "Lobby"; break;
|
||||
case BanchoStatusEnum.Multiplaying: activity.state = "Multiplaying"; break;
|
||||
case BanchoStatusEnum.OsuDirect: activity.state = "osu!direct"; break;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (beatmap.set > 0) {
|
||||
const mapBg = await getAsset(`https://assets.ppy.sh/beatmaps/${beatmap.set}/covers/list@2x.jpg`);
|
||||
const res = await fetch(mapBg.replace(/^mp:/, "https://media.discordapp.net/"), { method: "HEAD" });
|
||||
if (res.ok) activity.assets.large_image = mapBg;
|
||||
}
|
||||
|
||||
FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", activity, socketId: "tosu" });
|
||||
}
|
||||
|
||||
function throttle<T extends Function>(func: T, limit: number, timedOutCallback?: () => void): T {
|
||||
let inThrottle: boolean;
|
||||
let callbackTimeout: NodeJS.Timeout;
|
||||
return function (this: any, ...args: any[]) {
|
||||
if (!inThrottle) {
|
||||
func.apply(this, args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => inThrottle = false, limit);
|
||||
if (timedOutCallback) {
|
||||
clearTimeout(callbackTimeout);
|
||||
callbackTimeout = setTimeout(timedOutCallback, limit * 2);
|
||||
}
|
||||
}
|
||||
} as any;
|
||||
}
|
||||
|
||||
async function getAsset(key: string): Promise<string> {
|
||||
if (/https?:\/\/(cdn|media)\.discordapp\.(com|net)\/attachments\//.test(key)) return "mp:" + key.replace(/https?:\/\/(cdn|media)\.discordapp\.(com|net)\//, "");
|
||||
return (await ApplicationAssetUtils.fetchAssetIds(OSU_APP_ID, [key]))[0];
|
||||
}
|
652
src/equicordplugins/tosuRPC/type.ts
Normal file
652
src/equicordplugins/tosuRPC/type.ts
Normal file
|
@ -0,0 +1,652 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
export interface ActivityAssets {
|
||||
large_image?: string;
|
||||
large_text?: string;
|
||||
small_image?: string;
|
||||
small_text?: string;
|
||||
}
|
||||
|
||||
export interface Activity {
|
||||
state?: string;
|
||||
details?: string;
|
||||
timestamps?: {
|
||||
start?: number;
|
||||
end?: number;
|
||||
};
|
||||
assets: ActivityAssets;
|
||||
buttons?: Array<string>;
|
||||
name: string;
|
||||
application_id: string;
|
||||
metadata?: {
|
||||
button_urls?: Array<string>;
|
||||
};
|
||||
type: ActivityType;
|
||||
url?: string;
|
||||
flags: number;
|
||||
}
|
||||
|
||||
export enum ActivityType {
|
||||
PLAYING = 0,
|
||||
STREAMING = 1,
|
||||
LISTENING = 2,
|
||||
WATCHING = 3,
|
||||
COMPETING = 5
|
||||
}
|
||||
|
||||
export enum BeatmapStatuses {
|
||||
Unknown,
|
||||
NotSubmitted = 1,
|
||||
Pending = 2,
|
||||
Ranked = 4,
|
||||
Approved = 5,
|
||||
Qualified = 6,
|
||||
Loved = 7
|
||||
}
|
||||
|
||||
export enum Modes {
|
||||
Osu = 0,
|
||||
Taiko = 1,
|
||||
Fruits = 2,
|
||||
Mania = 3
|
||||
}
|
||||
|
||||
export enum BanchoStatusEnum {
|
||||
Idle,
|
||||
Afk,
|
||||
Playing,
|
||||
Editing,
|
||||
Modding,
|
||||
Multiplayer,
|
||||
Watching,
|
||||
Unknown,
|
||||
Testing,
|
||||
Submitting,
|
||||
Paused,
|
||||
Lobby,
|
||||
Multiplaying,
|
||||
OsuDirect
|
||||
}
|
||||
|
||||
export enum UserLoginStatus {
|
||||
Reconnecting = 0,
|
||||
Guest = 256,
|
||||
Recieving_data = 257,
|
||||
Disconnected = 65537,
|
||||
Connected = 65793
|
||||
}
|
||||
|
||||
export enum ReleaseStream {
|
||||
CuttingEdge,
|
||||
Stable,
|
||||
Beta,
|
||||
Fallback
|
||||
}
|
||||
|
||||
export enum ScoreMeterType {
|
||||
None,
|
||||
Colour,
|
||||
Error
|
||||
}
|
||||
|
||||
export enum LeaderboardType {
|
||||
Local,
|
||||
Global,
|
||||
Selectedmods,
|
||||
Friends,
|
||||
Country
|
||||
}
|
||||
|
||||
export enum GroupType {
|
||||
None,
|
||||
Artist,
|
||||
BPM,
|
||||
Creator,
|
||||
Date,
|
||||
Difficulty,
|
||||
Length,
|
||||
Rank,
|
||||
MyMaps,
|
||||
Search = 12,
|
||||
Show_All = 12,
|
||||
Title,
|
||||
LastPlayed,
|
||||
OnlineFavourites,
|
||||
ManiaKeys,
|
||||
Mode,
|
||||
Collection,
|
||||
RankedStatus
|
||||
}
|
||||
|
||||
export enum SortType {
|
||||
Artist,
|
||||
BPM,
|
||||
Creator,
|
||||
Date,
|
||||
Difficulty,
|
||||
Length,
|
||||
Rank,
|
||||
Title
|
||||
}
|
||||
|
||||
export enum ChatStatus {
|
||||
Hidden,
|
||||
Visible,
|
||||
VisibleWithFriendsList
|
||||
}
|
||||
|
||||
export enum ProgressBarType {
|
||||
Off,
|
||||
Pie,
|
||||
TopRight,
|
||||
BottomRight,
|
||||
Bottom
|
||||
}
|
||||
|
||||
export enum GameState {
|
||||
Menu,
|
||||
Edit,
|
||||
Play,
|
||||
Exit,
|
||||
SelectEdit,
|
||||
SelectPlay,
|
||||
SelectDrawings,
|
||||
ResultScreen,
|
||||
Update,
|
||||
Busy,
|
||||
Unknown,
|
||||
Lobby,
|
||||
MatchSetup,
|
||||
SelectMulti,
|
||||
RankingVs,
|
||||
OnlineSelection,
|
||||
OptionsOffsetWizard,
|
||||
RankingTagCoop,
|
||||
RankingTeam,
|
||||
BeatmapImport,
|
||||
PackageUpdater,
|
||||
Benchmark,
|
||||
Tourney,
|
||||
Charts
|
||||
}
|
||||
|
||||
export type ApiAnswer = TosuApi | { error?: string; };
|
||||
export type ApiAnswerPrecise = TosuPreciseAnswer | { error?: string; };
|
||||
|
||||
export interface TosuApi {
|
||||
state: NumberName;
|
||||
session: Session;
|
||||
settings: Settings;
|
||||
profile: Profile;
|
||||
beatmap: Beatmap;
|
||||
play: Play;
|
||||
leaderboard: Leaderboard[];
|
||||
performance: Performance;
|
||||
resultsScreen: ResultsScreen;
|
||||
folders: Folders;
|
||||
files: Files;
|
||||
directPath: DirectPath;
|
||||
tourney: Tourney | undefined;
|
||||
}
|
||||
|
||||
export interface BeatmapTime {
|
||||
live: number;
|
||||
firstObject: number;
|
||||
lastObject: number;
|
||||
mp3Length: number;
|
||||
}
|
||||
|
||||
export interface Session {
|
||||
playTime: number;
|
||||
playCount: number;
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
interfaceVisible: boolean;
|
||||
replayUIVisible: boolean;
|
||||
chatVisibilityStatus: NumberName;
|
||||
leaderboard: SettingsLeaderboard;
|
||||
|
||||
progressBar: NumberName;
|
||||
bassDensity: number;
|
||||
|
||||
resolution: Resolution;
|
||||
client: Client;
|
||||
|
||||
scoreMeter: ScoreMeter;
|
||||
cursor: Cursor;
|
||||
mouse: Mouse;
|
||||
mania: Mania;
|
||||
|
||||
sort: NumberName;
|
||||
group: NumberName;
|
||||
|
||||
skin: Skin;
|
||||
mode: NumberName;
|
||||
audio: Audio;
|
||||
background: Background;
|
||||
|
||||
keybinds: Keybinds;
|
||||
}
|
||||
|
||||
export interface Keybinds {
|
||||
osu: KeybindsOsu;
|
||||
fruits: KeybindsFruits;
|
||||
taiko: KeybindsTaiko;
|
||||
quickRetry: string;
|
||||
}
|
||||
|
||||
export interface KeybindsOsu {
|
||||
k1: string;
|
||||
k2: string;
|
||||
smokeKey: string;
|
||||
}
|
||||
|
||||
export interface KeybindsFruits {
|
||||
k1: string;
|
||||
k2: string;
|
||||
Dash: string;
|
||||
}
|
||||
|
||||
export interface KeybindsTaiko {
|
||||
innerLeft: string;
|
||||
innerRight: string;
|
||||
outerLeft: string;
|
||||
outerRight: string;
|
||||
}
|
||||
|
||||
export interface Volume {
|
||||
master: number;
|
||||
music: number;
|
||||
effect: number;
|
||||
}
|
||||
|
||||
export interface Audio {
|
||||
ignoreBeatmapSounds: boolean;
|
||||
useSkinSamples: boolean;
|
||||
volume: Volume;
|
||||
offset: Offset;
|
||||
}
|
||||
|
||||
export interface Background {
|
||||
storyboard: boolean;
|
||||
video: boolean;
|
||||
dim: number;
|
||||
}
|
||||
|
||||
export interface Client {
|
||||
updateAvailable: boolean;
|
||||
branch: number;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface Resolution {
|
||||
fullscreen: boolean;
|
||||
width: number;
|
||||
height: number;
|
||||
widthFullscreen: number;
|
||||
heightFullscreen: number;
|
||||
}
|
||||
|
||||
export interface Offset {
|
||||
universal: number;
|
||||
}
|
||||
|
||||
export interface Cursor {
|
||||
useSkinCursor: boolean;
|
||||
autoSize: boolean;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface Mouse {
|
||||
disableButtons: boolean;
|
||||
disableWheel: boolean;
|
||||
rawInput: boolean;
|
||||
sensitivity: number;
|
||||
}
|
||||
|
||||
export interface Mania {
|
||||
speedBPMScale: boolean;
|
||||
usePerBeatmapSpeedScale: boolean;
|
||||
}
|
||||
|
||||
export interface Skin {
|
||||
useDefaultSkinInEditor: boolean;
|
||||
ignoreBeatmapSkins: boolean;
|
||||
tintSliderBall: boolean;
|
||||
useTaikoSkin: boolean;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface SettingsLeaderboard {
|
||||
visible: boolean;
|
||||
type: NumberName;
|
||||
}
|
||||
|
||||
export interface ScoreMeter {
|
||||
type: NumberName;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface Volume {
|
||||
master: number;
|
||||
music: number;
|
||||
effect: number;
|
||||
}
|
||||
|
||||
export interface NumberName {
|
||||
number: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Profile {
|
||||
userStatus: NumberName;
|
||||
banchoStatus: NumberName;
|
||||
id: number;
|
||||
name: string;
|
||||
mode: NumberName;
|
||||
rankedScore: number;
|
||||
level: number;
|
||||
accuracy: number;
|
||||
pp: number;
|
||||
playCount: number;
|
||||
globalRank: number;
|
||||
countryCode: NumberName;
|
||||
backgroundColour: string;
|
||||
}
|
||||
|
||||
export interface Beatmap {
|
||||
time: BeatmapTime;
|
||||
status: NumberName;
|
||||
checksum: string;
|
||||
id: number;
|
||||
set: number;
|
||||
mode: NumberName;
|
||||
artist: string;
|
||||
artistUnicode: string;
|
||||
title: string;
|
||||
titleUnicode: string;
|
||||
mapper: string;
|
||||
version: string;
|
||||
stats: Stats;
|
||||
}
|
||||
|
||||
export interface Stats {
|
||||
stars: Stars;
|
||||
ar: Ar;
|
||||
cs: Cs;
|
||||
od: Od;
|
||||
hp: Hp;
|
||||
bpm: Bpm;
|
||||
objects: Objects;
|
||||
maxCombo: number;
|
||||
}
|
||||
|
||||
export interface Stars {
|
||||
live: number;
|
||||
aim: number | undefined;
|
||||
speed: number | undefined;
|
||||
flashlight: number | undefined;
|
||||
sliderFactor: number | undefined;
|
||||
stamina: number | undefined;
|
||||
rhythm: number | undefined;
|
||||
color: number | undefined;
|
||||
peak: number | undefined;
|
||||
hitWindow: number | undefined;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface Ar {
|
||||
original: number;
|
||||
converted: number;
|
||||
}
|
||||
|
||||
export interface Cs {
|
||||
original: number;
|
||||
converted: number;
|
||||
}
|
||||
|
||||
export interface Od {
|
||||
original: number;
|
||||
converted: number;
|
||||
}
|
||||
|
||||
export interface Hp {
|
||||
original: number;
|
||||
converted: number;
|
||||
}
|
||||
|
||||
export interface Bpm {
|
||||
common: number;
|
||||
min: number;
|
||||
max: number;
|
||||
}
|
||||
|
||||
export interface Objects {
|
||||
circles: number;
|
||||
sliders: number;
|
||||
spinners: number;
|
||||
holds: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface Play {
|
||||
playerName: string;
|
||||
mode: NumberName;
|
||||
score: number;
|
||||
accuracy: number;
|
||||
healthBar: HealthBar;
|
||||
hits: Hits;
|
||||
hitErrorArray: any[];
|
||||
combo: Combo;
|
||||
mods: NumberName;
|
||||
rank: Rank;
|
||||
pp: Pp;
|
||||
unstableRate: number;
|
||||
}
|
||||
|
||||
export interface HealthBar {
|
||||
normal: number;
|
||||
smooth: number;
|
||||
}
|
||||
|
||||
export interface Hits {
|
||||
"0": number;
|
||||
"50": number;
|
||||
"100": number;
|
||||
"300": number;
|
||||
geki: number;
|
||||
katu: number;
|
||||
sliderBreaks: number;
|
||||
}
|
||||
|
||||
export interface Combo {
|
||||
current: number;
|
||||
max: number;
|
||||
}
|
||||
|
||||
export interface Rank {
|
||||
current: string;
|
||||
maxThisPlay: string;
|
||||
}
|
||||
|
||||
export interface Pp {
|
||||
current: number;
|
||||
fc: number;
|
||||
maxAchievedThisPlay: number;
|
||||
}
|
||||
export interface Pp2 {
|
||||
current: number;
|
||||
fc: number;
|
||||
}
|
||||
|
||||
export interface Leaderboard {
|
||||
isFailed: boolean;
|
||||
position: number;
|
||||
team: number;
|
||||
name: string;
|
||||
score: number;
|
||||
accuracy: number;
|
||||
hits: Hits2;
|
||||
combo: Combo2;
|
||||
mods: NumberName;
|
||||
rank: string;
|
||||
}
|
||||
|
||||
export interface Hits2 {
|
||||
"0": number;
|
||||
"50": number;
|
||||
"100": number;
|
||||
"300": number;
|
||||
geki: number;
|
||||
katu: number;
|
||||
}
|
||||
|
||||
export interface Combo2 {
|
||||
current: number;
|
||||
max: number;
|
||||
}
|
||||
|
||||
export interface TosuPreciseAnswer {
|
||||
currentTime: number;
|
||||
keys: KeyOverlay;
|
||||
hitErrors: number[];
|
||||
tourney: PreciseTourney[];
|
||||
}
|
||||
|
||||
export interface PreciseTourney {
|
||||
ipcId: number;
|
||||
keys: KeyOverlay;
|
||||
hitErrors: number[];
|
||||
}
|
||||
|
||||
interface KeyOverlay {
|
||||
k1: KeyOverlayButton;
|
||||
k2: KeyOverlayButton;
|
||||
m1: KeyOverlayButton;
|
||||
m2: KeyOverlayButton;
|
||||
}
|
||||
|
||||
interface KeyOverlayButton {
|
||||
isPressed: boolean;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface Performance {
|
||||
accuracy: Accuracy;
|
||||
graph: Graph;
|
||||
}
|
||||
|
||||
export interface Accuracy {
|
||||
"95": number;
|
||||
"96": number;
|
||||
"97": number;
|
||||
"98": number;
|
||||
"99": number;
|
||||
"100": number;
|
||||
}
|
||||
|
||||
export interface Graph {
|
||||
series: Series[];
|
||||
xaxis: number[];
|
||||
}
|
||||
|
||||
export interface Series {
|
||||
name: string;
|
||||
data: number[];
|
||||
}
|
||||
|
||||
export interface ResultsScreen {
|
||||
playerName: string;
|
||||
mode: NumberName;
|
||||
score: number;
|
||||
accuracy: number;
|
||||
name: string;
|
||||
hits: Hits3;
|
||||
mods: NumberName;
|
||||
maxCombo: number;
|
||||
rank: string;
|
||||
pp: Pp2;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface Hits3 {
|
||||
"0": number;
|
||||
"50": number;
|
||||
"100": number;
|
||||
"300": number;
|
||||
geki: number;
|
||||
katu: number;
|
||||
}
|
||||
|
||||
export interface Folders {
|
||||
game: string;
|
||||
skin: string;
|
||||
songs: string;
|
||||
beatmap: string;
|
||||
}
|
||||
|
||||
export interface Files {
|
||||
beatmap: string;
|
||||
background: string;
|
||||
audio: string;
|
||||
}
|
||||
|
||||
export interface DirectPath {
|
||||
beatmapFile: string;
|
||||
beatmapBackground: string;
|
||||
beatmapAudio: string;
|
||||
beatmapFolder: string;
|
||||
skinFolder: string;
|
||||
}
|
||||
|
||||
export interface Tourney {
|
||||
scoreVisible: boolean;
|
||||
starsVisible: boolean;
|
||||
|
||||
ipcState: number;
|
||||
bestOF: number;
|
||||
|
||||
team: {
|
||||
left: string;
|
||||
right: string;
|
||||
};
|
||||
points: {
|
||||
left: number;
|
||||
right: number;
|
||||
};
|
||||
totalScore: {
|
||||
left: number;
|
||||
right: number;
|
||||
};
|
||||
|
||||
chat: TourneyChatMessages[];
|
||||
clients: TourneyClients[];
|
||||
}
|
||||
|
||||
export interface TourneyChatMessages {
|
||||
team: string;
|
||||
name: string;
|
||||
message: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface TourneyClients {
|
||||
ipcId: number;
|
||||
team: "left" | "right";
|
||||
user: {
|
||||
id: number;
|
||||
name: string;
|
||||
country: string;
|
||||
accuracy: number;
|
||||
rankedScore: number;
|
||||
playCount: number;
|
||||
globalRank: number;
|
||||
totalPP: number;
|
||||
};
|
||||
play: Play;
|
||||
}
|
|
@ -16,20 +16,32 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { EquicordDevs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
|
||||
const settings = definePluginSettings({
|
||||
maxAccounts: {
|
||||
description: "Number of accounts that can be added, or 0 for no limit",
|
||||
default: 0,
|
||||
type: OptionType.NUMBER,
|
||||
restartNeeded: true,
|
||||
},
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "UnlimitedAccounts",
|
||||
description: "Increases the amount of accounts you can add.",
|
||||
authors: [EquicordDevs.Balaclava, EquicordDevs.thororen],
|
||||
settings,
|
||||
patches: [
|
||||
{
|
||||
find: "multiaccount_cta_tooltip_seen",
|
||||
replacement: {
|
||||
match: /(let \i=)\d+(,\i="switch-accounts-modal",\i="multiaccount_cta_tooltip_seen")/,
|
||||
replace: "$1Infinity$2",
|
||||
replace: "$1$self.getMaxAccounts()$2",
|
||||
},
|
||||
},
|
||||
],
|
||||
getMaxAccounts() { return settings.store.maxAccounts === 0 ? Infinity : settings.store.maxAccounts; },
|
||||
});
|
||||
|
|
97
src/equicordplugins/viewRaw2/index.tsx
Normal file
97
src/equicordplugins/viewRaw2/index.tsx
Normal file
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* 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 { CodeBlock } from "@components/CodeBlock";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { closeModal, ModalCloseButton, ModalContent, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import definePlugin from "@utils/types";
|
||||
import { Forms, i18n, Menu, Text } from "@webpack/common";
|
||||
import { Message } from "discord-types/general";
|
||||
|
||||
const CopyIcon = () => {
|
||||
return <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" width="18" height="18">
|
||||
<path d="M12.9297 3.25007C12.7343 3.05261 12.4154 3.05226 12.2196 3.24928L11.5746 3.89824C11.3811 4.09297 11.3808 4.40733 11.5739 4.60245L16.5685 9.64824C16.7614 9.84309 16.7614 10.1569 16.5685 10.3517L11.5739 15.3975C11.3808 15.5927 11.3811 15.907 11.5746 16.1017L12.2196 16.7507C12.4154 16.9477 12.7343 16.9474 12.9297 16.7499L19.2604 10.3517C19.4532 10.1568 19.4532 9.84314 19.2604 9.64832L12.9297 3.25007Z" />
|
||||
<path d="M8.42616 4.60245C8.6193 4.40733 8.61898 4.09297 8.42545 3.89824L7.78047 3.24928C7.58466 3.05226 7.26578 3.05261 7.07041 3.25007L0.739669 9.64832C0.5469 9.84314 0.546901 10.1568 0.739669 10.3517L7.07041 16.7499C7.26578 16.9474 7.58465 16.9477 7.78047 16.7507L8.42545 16.1017C8.61898 15.907 8.6193 15.5927 8.42616 15.3975L3.43155 10.3517C3.23869 10.1569 3.23869 9.84309 3.43155 9.64824L8.42616 4.60245Z" />
|
||||
</svg>;
|
||||
};
|
||||
|
||||
function cleanMessage(msg: Message) {
|
||||
const author = { ...msg.author } as any;
|
||||
delete author.email;
|
||||
delete author.phone;
|
||||
delete author.mfaEnabled;
|
||||
delete author.personalConnectionId;
|
||||
return { ...msg, author };
|
||||
}
|
||||
|
||||
function openViewRawModal(obj: any, type: string, isMessage?: boolean) {
|
||||
const key = openModal(props => (
|
||||
<ErrorBoundary>
|
||||
<ModalRoot {...props} size={ModalSize.LARGE}>
|
||||
<ModalHeader>
|
||||
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>View Raw {type}</Text>
|
||||
<ModalCloseButton onClick={() => closeModal(key)} />
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<div style={{ padding: "16px 0" }}>
|
||||
{isMessage && (
|
||||
<>
|
||||
<Forms.FormTitle tag="h5">Content</Forms.FormTitle>
|
||||
<CodeBlock content={obj.content} lang="markdown" />
|
||||
<Forms.FormDivider className={Margins.bottom20} />
|
||||
</>
|
||||
)}
|
||||
|
||||
<Forms.FormTitle tag="h5">{type} Data</Forms.FormTitle>
|
||||
<CodeBlock content={JSON.stringify(obj, null, 4)} lang="json" />
|
||||
</div>
|
||||
</ModalContent >
|
||||
</ModalRoot >
|
||||
</ErrorBoundary >
|
||||
));
|
||||
}
|
||||
|
||||
function makeContextCallback(name: string, action: (any) => void): NavContextMenuPatchCallback {
|
||||
return (children, props) => {
|
||||
if (props.label === i18n.Messages.CHANNEL_ACTIONS_MENU_LABEL) return; // random shit like notification settings
|
||||
|
||||
const value = props[name];
|
||||
if (!value) return;
|
||||
|
||||
const lastChild = children.at(-1);
|
||||
if (lastChild?.key === "developer-actions") {
|
||||
const p = lastChild.props;
|
||||
if (!Array.isArray(p.children))
|
||||
p.children = [p.children];
|
||||
|
||||
children = p.children;
|
||||
}
|
||||
|
||||
children.push(
|
||||
<Menu.MenuItem
|
||||
id={`c98-view-${name}-raw`}
|
||||
label="View Raw"
|
||||
action={() => action(value)}
|
||||
icon={CopyIcon}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "ViewRaw2",
|
||||
description: "Copy and view the raw content/data of any message, channel or guild",
|
||||
authors: [Devs.KingFish, Devs.Ven, Devs.rad, Devs.ImLvna, Devs.Kyuuhachi],
|
||||
contextMenus: {
|
||||
"guild-context": makeContextCallback("guild", val => openViewRawModal(val, "Guild")),
|
||||
"channel-context": makeContextCallback("channel", val => openViewRawModal(val, "Channel")),
|
||||
"user-context": makeContextCallback("user", val => openViewRawModal(val, "User")),
|
||||
"message": makeContextCallback("message", val => openViewRawModal(cleanMessage(val), "Message", true)),
|
||||
}
|
||||
});
|
158
src/equicordplugins/webpackTarball/index.tsx
Normal file
158
src/equicordplugins/webpackTarball/index.tsx
Normal file
|
@ -0,0 +1,158 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Devs, WEBPACK_CHUNK } from "@utils/constants";
|
||||
import { makeLazy } from "@utils/lazy";
|
||||
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/modal";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByProps, wreq } from "@webpack";
|
||||
import { Button, Flex, Forms, Switch, Text, Timestamp, useState } from "@webpack/common";
|
||||
|
||||
import TarFile from "./tar";
|
||||
import * as Webpack from "./webpack";
|
||||
|
||||
export const settings = definePluginSettings({
|
||||
patched: {
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
description: "Include patched modules",
|
||||
restartNeeded: true,
|
||||
},
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "WebpackTarball",
|
||||
description: "Converts Discord's webpack sources into a tarball.",
|
||||
authors: [Devs.Kyuuhachi],
|
||||
settings,
|
||||
|
||||
toolboxActions: {
|
||||
"Webpack Tarball"() {
|
||||
const key = openModal(props => (
|
||||
<TarModal
|
||||
modalProps={props}
|
||||
close={() => closeModal(key)}
|
||||
/>
|
||||
));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const getBuildNumber = makeLazy(() => {
|
||||
try {
|
||||
const metrics = findByProps("_getMetricWithDefaults")._flush.toString();
|
||||
const [, builtAt, buildNumber] = metrics.match(/\{built_at:"(\d+)",build_number:"(\d+)"\}/);
|
||||
return { buildNumber, builtAt: new Date(Number(builtAt)) };
|
||||
} catch (e) {
|
||||
console.error("failed to get build number:", e);
|
||||
return { buildNumber: "unknown", builtAt: new Date() };
|
||||
}
|
||||
});
|
||||
|
||||
async function saveTar(patched: boolean) {
|
||||
const tar = new TarFile();
|
||||
const { buildNumber, builtAt } = getBuildNumber();
|
||||
const mtime = (builtAt.getTime() / 1000) | 0;
|
||||
|
||||
const root = patched ? `vencord-${buildNumber}` : `discord-${buildNumber}`;
|
||||
|
||||
for (const [id, module] of Object.entries(wreq.m)) {
|
||||
const patchedSrc = Function.toString.call(module);
|
||||
const originalSrc = module.toString();
|
||||
if (patched && patchedSrc !== originalSrc)
|
||||
tar.addTextFile(
|
||||
`${root}/${id}.v.js`,
|
||||
`webpack[${JSON.stringify(id)}] = ${patchedSrc}\n`,
|
||||
{ mtime },
|
||||
);
|
||||
tar.addTextFile(
|
||||
`${root}/${id}.js`,
|
||||
`webpack[${JSON.stringify(id)}] = ${originalSrc}\n`,
|
||||
{ mtime },
|
||||
);
|
||||
}
|
||||
tar.save(`${root}.tar`);
|
||||
}
|
||||
|
||||
function TarModal({ modalProps, close }: { modalProps: ModalProps; close(): void; }) {
|
||||
const { buildNumber, builtAt } = getBuildNumber();
|
||||
const [, rerender] = useState({});
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
const paths = Webpack.getChunkPaths(wreq);
|
||||
const status = Object.entries(Webpack.getLoadedChunks(wreq))
|
||||
.filter(([k]) => wreq.o(paths, k))
|
||||
.map(([, v]) => v);
|
||||
const loading = status.length;
|
||||
const loaded = status.filter(v => v === 0 || v === undefined).length;
|
||||
const errored = status.filter(v => v === undefined).length;
|
||||
const all = Object.keys(paths).length;
|
||||
const { patched } = settings.use(["patched"]);
|
||||
return (
|
||||
<ModalRoot {...modalProps}>
|
||||
<ModalHeader>
|
||||
<Forms.FormTitle tag="h2">
|
||||
Webpack Tarball
|
||||
</Forms.FormTitle>
|
||||
<Text variant="text-md/normal">
|
||||
<Timestamp timestamp={new Date(builtAt)} isInline={false}>
|
||||
{"Build number "}
|
||||
{buildNumber}
|
||||
</Timestamp>
|
||||
</Text>
|
||||
<ModalCloseButton onClick={close} />
|
||||
</ModalHeader>
|
||||
|
||||
<ModalContent>
|
||||
<div style={{ marginTop: "8px", marginBottom: "24px" }}>
|
||||
<Forms.FormTitle>
|
||||
Lazy chunks
|
||||
</Forms.FormTitle>
|
||||
<Flex align={Flex.Align.CENTER}>
|
||||
<Text
|
||||
variant="text-md/normal"
|
||||
style={{ flexGrow: 1 }}
|
||||
>
|
||||
{loaded}/{all}
|
||||
{errored ? ` (${errored} errors)` : null}
|
||||
</Text>
|
||||
<Button
|
||||
disabled={loading === all || isLoading}
|
||||
onClick={async () => {
|
||||
setLoading(true);
|
||||
// @ts-ignore
|
||||
await Webpack.protectWebpack(window[WEBPACK_CHUNK], async () => {
|
||||
await Webpack.forceLoadAll(wreq, rerender);
|
||||
});
|
||||
}}
|
||||
>
|
||||
{loaded === all ? "Loaded" : loading === all ? "Loading" : "Load all"}
|
||||
</Button>
|
||||
</Flex>
|
||||
</div>
|
||||
|
||||
<Switch
|
||||
value={patched}
|
||||
onChange={v => settings.store.patched = v}
|
||||
hideBorder
|
||||
>
|
||||
{settings.def.patched.description}
|
||||
</Switch>
|
||||
</ModalContent>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
onClick={() => {
|
||||
saveTar(patched);
|
||||
close();
|
||||
}}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalRoot>
|
||||
);
|
||||
}
|
74
src/equicordplugins/webpackTarball/tar.ts
Normal file
74
src/equicordplugins/webpackTarball/tar.ts
Normal file
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
export type Metadata = { mtime?: number; };
|
||||
export default class TarFile {
|
||||
buffers: ArrayBuffer[];
|
||||
|
||||
constructor() {
|
||||
this.buffers = [];
|
||||
}
|
||||
|
||||
addTextFile(name: string, text: string, metadata: Metadata = {}) {
|
||||
this.addFile(name, new TextEncoder().encode(text), metadata);
|
||||
}
|
||||
|
||||
addFile(name: string, data: Uint8Array, { mtime = 0 }: Metadata = {}) {
|
||||
this.buffers.push(this.header([
|
||||
[100, name.toString()], // name
|
||||
[8, 0o644], // mode
|
||||
[8, 0o1000], // uid
|
||||
[8, 0o1000], // gid
|
||||
[12, data.length], // size
|
||||
[12, mtime], // mtime
|
||||
[8, null], // checksum
|
||||
[1, "0"], // type
|
||||
[100, ""], // name of linked file (??)
|
||||
[255, ""], // padding
|
||||
]));
|
||||
this.buffers.push(data);
|
||||
this.buffers.push(new ArrayBuffer(-data.length & 0x1FF));
|
||||
}
|
||||
|
||||
header(fields: [number, number | string | null][]) {
|
||||
const buffer = new ArrayBuffer(512);
|
||||
const u1 = new Uint8Array(buffer);
|
||||
let checksum = 0;
|
||||
let checksumPos: number = null!;
|
||||
|
||||
let pos = 0;
|
||||
for (const [size, val] of fields) {
|
||||
let string: string;
|
||||
if (val === null) {
|
||||
checksumPos = pos;
|
||||
string = " ".repeat(size);
|
||||
} else if (typeof val === "string") {
|
||||
string = val;
|
||||
} else if (typeof val === "number") {
|
||||
string = val.toString(8).padStart(size - 1, "0");
|
||||
} else {
|
||||
throw new Error("invalid value", val);
|
||||
}
|
||||
if (string.length > size) throw new Error(`${string} is longer than ${size} characters`);
|
||||
Array.from(string).forEach((c, i) => checksum += u1[pos + i] = c.charCodeAt(0));
|
||||
pos += size;
|
||||
}
|
||||
Array.from("\0".repeat(8)).forEach((c, i) => u1[checksumPos + i] = c.charCodeAt(0));
|
||||
Array.from(checksum.toString(8).padStart(7, "0")).forEach((c, i) => u1[checksumPos + i] = c.charCodeAt(0));
|
||||
return buffer;
|
||||
}
|
||||
|
||||
save(filename: string) {
|
||||
const a = document.createElement("a");
|
||||
a.href = URL.createObjectURL(new Blob(this.buffers, { "type": "application/x-tar" }));
|
||||
a.download = filename;
|
||||
a.style.display = "none";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(a.href);
|
||||
}
|
||||
}
|
66
src/equicordplugins/webpackTarball/webpack.ts
Normal file
66
src/equicordplugins/webpackTarball/webpack.ts
Normal file
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { WebpackInstance } from "discord-types/other";
|
||||
|
||||
export async function protectWebpack<T>(webpack: any[], body: () => Promise<T>): Promise<T> {
|
||||
const prev_m = Object.getOwnPropertyDescriptor(Function.prototype, "m")!;
|
||||
Object.defineProperty(Function.prototype, "m", {
|
||||
get() { throw "get require.m"; },
|
||||
set() { throw "set require.m"; },
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
try {
|
||||
return await body();
|
||||
} finally {
|
||||
Object.defineProperty(Function.prototype, "m", prev_m);
|
||||
}
|
||||
}
|
||||
|
||||
export function getLoadedChunks(wreq: WebpackInstance): { [chunkId: string | symbol]: 0 | undefined; } {
|
||||
const { o } = wreq;
|
||||
try {
|
||||
wreq.o = (a: any) => { throw a; };
|
||||
wreq.f.j();
|
||||
} catch (e: any) {
|
||||
return e;
|
||||
} finally {
|
||||
wreq.o = o;
|
||||
}
|
||||
throw new Error("getLoadedChunks failed");
|
||||
}
|
||||
|
||||
export function getChunkPaths(wreq: WebpackInstance): { [chunkId: string]: string; } {
|
||||
const sym = Symbol("getChunkPaths");
|
||||
try {
|
||||
Object.defineProperty(Object.prototype, sym, {
|
||||
get() { throw this; },
|
||||
set() { },
|
||||
configurable: true,
|
||||
});
|
||||
wreq.u(sym);
|
||||
} catch (e: any) {
|
||||
return e;
|
||||
} finally {
|
||||
// @ts-ignore
|
||||
delete Object.prototype[sym];
|
||||
}
|
||||
throw new Error("getChunkPaths failed");
|
||||
}
|
||||
|
||||
export async function forceLoadAll(wreq: WebpackInstance, on_chunk: (id: string) => void = () => { }) {
|
||||
const chunks = getChunkPaths(wreq);
|
||||
const loaded = getLoadedChunks(wreq);
|
||||
const ids = Object.keys(chunks).filter(id => loaded[id] !== 0);
|
||||
await Promise.all(ids.map(async id => {
|
||||
try {
|
||||
await wreq.e(id as any);
|
||||
} catch { }
|
||||
on_chunk(id);
|
||||
}));
|
||||
}
|
Loading…
Reference in a new issue