From 167612920b58a0a644ebeef8860fd4a169d1b484 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Thu, 18 Jul 2024 02:14:43 -0400 Subject: [PATCH] More Plugins --- README.md | 13 + src/equicordplugins/TeX/index.tsx | 93 +++ src/equicordplugins/TeX/katexLoader.ts | 29 + src/equicordplugins/TeX/style.css | 8 + src/equicordplugins/cleanChannelName/index.ts | 15 +- src/equicordplugins/modalFade/index.tsx | 99 +++ .../notificationTitle/index.tsx | 69 ++ src/equicordplugins/tosuRPC/index.ts | 219 ++++++ src/equicordplugins/tosuRPC/type.ts | 652 ++++++++++++++++++ .../unlimitedAccounts/index.ts | 16 +- src/equicordplugins/viewRaw2/index.tsx | 97 +++ src/equicordplugins/webpackTarball/index.tsx | 158 +++++ src/equicordplugins/webpackTarball/tar.ts | 74 ++ src/equicordplugins/webpackTarball/webpack.ts | 66 ++ 14 files changed, 1605 insertions(+), 3 deletions(-) create mode 100644 src/equicordplugins/TeX/index.tsx create mode 100644 src/equicordplugins/TeX/katexLoader.ts create mode 100644 src/equicordplugins/TeX/style.css create mode 100644 src/equicordplugins/modalFade/index.tsx create mode 100644 src/equicordplugins/notificationTitle/index.tsx create mode 100644 src/equicordplugins/tosuRPC/index.ts create mode 100644 src/equicordplugins/tosuRPC/type.ts create mode 100644 src/equicordplugins/viewRaw2/index.tsx create mode 100644 src/equicordplugins/webpackTarball/index.tsx create mode 100644 src/equicordplugins/webpackTarball/tar.ts create mode 100644 src/equicordplugins/webpackTarball/webpack.ts diff --git a/README.md b/README.md index 18c8084d..2f3313dc 100644 --- a/README.md +++ b/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 diff --git a/src/equicordplugins/TeX/index.tsx b/src/equicordplugins/TeX/index.tsx new file mode 100644 index 00000000..2daf27bc --- /dev/null +++ b/src/equicordplugins/TeX/index.tsx @@ -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 ; + if (inlineMatch) + return ; + } +}); + +function LazyLatex(props) { + const { formula, delim } = props; + const katex = useKatex(); + return katex + ? + : {formula}; +} + +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 + ? + : ; +} + +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 ( + + {({ onMouseLeave, onMouseEnter }) => ( + + {pre}{mid}{suf} + + )} + + ); +} + +function LatexPlaceholder({ className, delim, children, ...props }) { + return ( + + {delim} + {children} + {delim} + + ); +} diff --git a/src/equicordplugins/TeX/katexLoader.ts b/src/equicordplugins/TeX/katexLoader.ts new file mode 100644 index 00000000..bf01ba06 --- /dev/null +++ b/src/equicordplugins/TeX/katexLoader.ts @@ -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; +} diff --git a/src/equicordplugins/TeX/style.css b/src/equicordplugins/TeX/style.css new file mode 100644 index 00000000..38fb2546 --- /dev/null +++ b/src/equicordplugins/TeX/style.css @@ -0,0 +1,8 @@ +.katex-display { + margin: 0; + display: inline-block; +} + +.tex-error { + color: var(--status-danger); +} diff --git a/src/equicordplugins/cleanChannelName/index.ts b/src/equicordplugins/cleanChannelName/index.ts index 669d1dc5..ee4ea0bb 100644 --- a/src/equicordplugins/cleanChannelName/index.ts +++ b/src/equicordplugins/cleanChannelName/index.ts @@ -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; } diff --git a/src/equicordplugins/modalFade/index.tsx b/src/equicordplugins/modalFade/index.tsx new file mode 100644 index 00000000..1d858b2e --- /dev/null +++ b/src/equicordplugins/modalFade/index.tsx @@ -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>, +}); + +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 ; + } +}); + +function usePrevious(value: T | undefined): T | undefined { + const ref = useRef(); + useEffect(() => { + ref.current = value; + }, [value]); + return ref.current; +} diff --git a/src/equicordplugins/notificationTitle/index.tsx b/src/equicordplugins/notificationTitle/index.tsx new file mode 100644 index 00000000..fa3dfff8 --- /dev/null +++ b/src/equicordplugins/notificationTitle/index.tsx @@ -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); +} diff --git a/src/equicordplugins/tosuRPC/index.ts b/src/equicordplugins/tosuRPC/index.ts new file mode 100644 index 00000000..b1727773 --- /dev/null +++ b/src/equicordplugins/tosuRPC/index.ts @@ -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(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 { + 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]; +} diff --git a/src/equicordplugins/tosuRPC/type.ts b/src/equicordplugins/tosuRPC/type.ts new file mode 100644 index 00000000..67c3e495 --- /dev/null +++ b/src/equicordplugins/tosuRPC/type.ts @@ -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; + name: string; + application_id: string; + metadata?: { + button_urls?: Array; + }; + 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; +} diff --git a/src/equicordplugins/unlimitedAccounts/index.ts b/src/equicordplugins/unlimitedAccounts/index.ts index cbeb3b5e..a82f233b 100644 --- a/src/equicordplugins/unlimitedAccounts/index.ts +++ b/src/equicordplugins/unlimitedAccounts/index.ts @@ -16,20 +16,32 @@ * along with this program. If not, see . */ +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; }, }); diff --git a/src/equicordplugins/viewRaw2/index.tsx b/src/equicordplugins/viewRaw2/index.tsx new file mode 100644 index 00000000..bfd8da9d --- /dev/null +++ b/src/equicordplugins/viewRaw2/index.tsx @@ -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 ; +}; + +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 => ( + + + + View Raw {type} + closeModal(key)} /> + + +
+ {isMessage && ( + <> + Content + + + + )} + + {type} Data + +
+
+
+
+ )); +} + +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( + 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)), + } +}); diff --git a/src/equicordplugins/webpackTarball/index.tsx b/src/equicordplugins/webpackTarball/index.tsx new file mode 100644 index 00000000..68f0f400 --- /dev/null +++ b/src/equicordplugins/webpackTarball/index.tsx @@ -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 => ( + 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 ( + + + + Webpack Tarball + + + + {"Build number "} + {buildNumber} + + + + + + +
+ + Lazy chunks + + + + {loaded}/{all} + {errored ? ` (${errored} errors)` : null} + + + +
+ + settings.store.patched = v} + hideBorder + > + {settings.def.patched.description} + +
+ + + + +
+ ); +} diff --git a/src/equicordplugins/webpackTarball/tar.ts b/src/equicordplugins/webpackTarball/tar.ts new file mode 100644 index 00000000..f8f25b3a --- /dev/null +++ b/src/equicordplugins/webpackTarball/tar.ts @@ -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); + } +} diff --git a/src/equicordplugins/webpackTarball/webpack.ts b/src/equicordplugins/webpackTarball/webpack.ts new file mode 100644 index 00000000..da0bf4b6 --- /dev/null +++ b/src/equicordplugins/webpackTarball/webpack.ts @@ -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(webpack: any[], body: () => Promise): Promise { + 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); + })); +}