diff --git a/.vscode/settings.json b/.vscode/settings.json
index fa543b38c..8be0795f9 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -14,6 +14,8 @@
"typescript.preferences.quoteStyle": "double",
"javascript.preferences.quoteStyle": "double",
+ "eslint.experimental.useFlatConfig": false,
+
"gitlens.remotes": [
{
"domain": "codeberg.org",
diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts
index 0fde48637..cf4210779 100644
--- a/scripts/generateReport.ts
+++ b/scripts/generateReport.ts
@@ -241,17 +241,26 @@ page.on("console", async e => {
error: await maybeGetError(e.args()[3]) ?? "Unknown error"
});
+ break;
+ case "LazyChunkLoader:":
+ console.error(await getText());
+
+ switch (message) {
+ case "A fatal error occurred:":
+ process.exit(1);
+ }
+
break;
case "Reporter:":
console.error(await getText());
switch (message) {
+ case "A fatal error occurred:":
+ process.exit(1);
case "Webpack Find Fail:":
process.exitCode = 1;
report.badWebpackFinds.push(otherMessage);
break;
- case "A fatal error occurred:":
- process.exit(1);
case "Finished test":
await browser.close();
await printReport();
diff --git a/src/api/Notifications/NotificationComponent.tsx b/src/api/Notifications/NotificationComponent.tsx
index caa4b64ef..d07143c45 100644
--- a/src/api/Notifications/NotificationComponent.tsx
+++ b/src/api/Notifications/NotificationComponent.tsx
@@ -113,7 +113,7 @@ export default ErrorBoundary.wrap(function NotificationComponent({
{timeout !== 0 && !permanent && (
)}
diff --git a/src/api/Settings.ts b/src/api/Settings.ts
index b94e6a3fd..70ba0bd4a 100644
--- a/src/api/Settings.ts
+++ b/src/api/Settings.ts
@@ -129,7 +129,7 @@ export const SettingsStore = new SettingsStoreClass(settings, {
if (path === "plugins" && key in plugins)
return target[key] = {
- enabled: plugins[key].required ?? plugins[key].enabledByDefault ?? false
+ enabled: IS_REPORTER ?? plugins[key].required ?? plugins[key].enabledByDefault ?? false
};
// Since the property is not set, check if this is a plugin's setting and if so, try to resolve
diff --git a/src/components/PluginSettings/index.tsx b/src/components/PluginSettings/index.tsx
index e6b2cf1fb..9c26a9cf1 100644
--- a/src/components/PluginSettings/index.tsx
+++ b/src/components/PluginSettings/index.tsx
@@ -261,8 +261,9 @@ export default function PluginSettings() {
plugins = [];
requiredPlugins = [];
+ const showApi = searchValue.value === "API";
for (const p of sortedPlugins) {
- if (!p.options && p.name.endsWith("API") && searchValue.value !== "API")
+ if (p.hidden || (!p.options && p.name.endsWith("API") && !showApi))
continue;
if (!pluginFilter(p)) continue;
diff --git a/src/debug/loadLazyChunks.ts b/src/debug/loadLazyChunks.ts
new file mode 100644
index 000000000..d8f84335c
--- /dev/null
+++ b/src/debug/loadLazyChunks.ts
@@ -0,0 +1,167 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { Logger } from "@utils/Logger";
+import { canonicalizeMatch } from "@utils/patches";
+import * as Webpack from "@webpack";
+import { wreq } from "@webpack";
+
+const LazyChunkLoaderLogger = new Logger("LazyChunkLoader");
+
+export async function loadLazyChunks() {
+ try {
+ LazyChunkLoaderLogger.log("Loading all chunks...");
+
+ const validChunks = new Set();
+ const invalidChunks = new Set();
+ const deferredRequires = new Set();
+
+ let chunksSearchingResolve: (value: void | PromiseLike) => void;
+ const chunksSearchingDone = new Promise(r => chunksSearchingResolve = r);
+
+ // True if resolved, false otherwise
+ const chunksSearchPromises = [] as Array<() => boolean>;
+
+ const LazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?(\i\.e\("[^)]+?"\)[^\]]*?)(?:\]\))?)\.then\(\i\.bind\(\i,"([^)]+?)"\)\)/g);
+
+ async function searchAndLoadLazyChunks(factoryCode: string) {
+ const lazyChunks = factoryCode.matchAll(LazyChunkRegex);
+ const validChunkGroups = new Set<[chunkIds: string[], entryPoint: string]>();
+
+ // Workaround for a chunk that depends on the ChannelMessage component but may be be force loaded before
+ // the chunk containing the component
+ const shouldForceDefer = factoryCode.includes(".Messages.GUILD_FEED_UNFEATURE_BUTTON_TEXT");
+
+ await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => {
+ const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Webpack.ChunkIdsRegex)).map(m => m[1]) : [];
+
+ if (chunkIds.length === 0) {
+ return;
+ }
+
+ let invalidChunkGroup = false;
+
+ for (const id of chunkIds) {
+ if (wreq.u(id) == null || wreq.u(id) === "undefined.js") continue;
+
+ const isWasm = await fetch(wreq.p + wreq.u(id))
+ .then(r => r.text())
+ .then(t => (IS_WEB && t.includes(".module.wasm")) || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push"));
+
+ if (isWasm && IS_WEB) {
+ invalidChunks.add(id);
+ invalidChunkGroup = true;
+ continue;
+ }
+
+ validChunks.add(id);
+ }
+
+ if (!invalidChunkGroup) {
+ validChunkGroups.add([chunkIds, entryPoint]);
+ }
+ }));
+
+ // Loads all found valid chunk groups
+ await Promise.all(
+ Array.from(validChunkGroups)
+ .map(([chunkIds]) =>
+ Promise.all(chunkIds.map(id => wreq.e(id as any).catch(() => { })))
+ )
+ );
+
+ // Requires the entry points for all valid chunk groups
+ for (const [, entryPoint] of validChunkGroups) {
+ try {
+ if (shouldForceDefer) {
+ deferredRequires.add(entryPoint);
+ continue;
+ }
+
+ if (wreq.m[entryPoint]) wreq(entryPoint as any);
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
+ // setImmediate to only check if all chunks were loaded after this function resolves
+ // We check if all chunks were loaded every time a factory is loaded
+ // If we are still looking for chunks in the other factories, the array will have that factory's chunk search promise not resolved
+ // But, if all chunk search promises are resolved, this means we found every lazy chunk loaded by Discord code and manually loaded them
+ setTimeout(() => {
+ let allResolved = true;
+
+ for (let i = 0; i < chunksSearchPromises.length; i++) {
+ const isResolved = chunksSearchPromises[i]();
+
+ if (isResolved) {
+ // Remove finished promises to avoid having to iterate through a huge array everytime
+ chunksSearchPromises.splice(i--, 1);
+ } else {
+ allResolved = false;
+ }
+ }
+
+ if (allResolved) chunksSearchingResolve();
+ }, 0);
+ }
+
+ Webpack.factoryListeners.add(factory => {
+ let isResolved = false;
+ searchAndLoadLazyChunks(factory.toString()).then(() => isResolved = true);
+
+ chunksSearchPromises.push(() => isResolved);
+ });
+
+ for (const factoryId in wreq.m) {
+ let isResolved = false;
+ searchAndLoadLazyChunks(wreq.m[factoryId].toString()).then(() => isResolved = true);
+
+ chunksSearchPromises.push(() => isResolved);
+ }
+
+ await chunksSearchingDone;
+
+ // Require deferred entry points
+ for (const deferredRequire of deferredRequires) {
+ wreq!(deferredRequire as any);
+ }
+
+ // All chunks Discord has mapped to asset files, even if they are not used anymore
+ const allChunks = [] as string[];
+
+ // Matches "id" or id:
+ for (const currentMatch of wreq!.u.toString().matchAll(/(?:"(\d+?)")|(?:(\d+?):)/g)) {
+ const id = currentMatch[1] ?? currentMatch[2];
+ if (id == null) continue;
+
+ allChunks.push(id);
+ }
+
+ if (allChunks.length === 0) throw new Error("Failed to get all chunks");
+
+ // Chunks that are not loaded (not used) by Discord code anymore
+ const chunksLeft = allChunks.filter(id => {
+ return !(validChunks.has(id) || invalidChunks.has(id));
+ });
+
+ await Promise.all(chunksLeft.map(async id => {
+ const isWasm = await fetch(wreq.p + wreq.u(id))
+ .then(r => r.text())
+ .then(t => (IS_WEB && t.includes(".module.wasm")) || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push"));
+
+ // Loads and requires a chunk
+ if (!isWasm) {
+ await wreq.e(id as any);
+ if (wreq.m[id]) wreq(id as any);
+ }
+ }));
+
+ LazyChunkLoaderLogger.log("Finished loading all chunks!");
+ } catch (e) {
+ LazyChunkLoaderLogger.log("A fatal error occurred:", e);
+ }
+}
diff --git a/src/debug/runReporter.ts b/src/debug/runReporter.ts
index 61c9f162b..6c7a2a03f 100644
--- a/src/debug/runReporter.ts
+++ b/src/debug/runReporter.ts
@@ -5,171 +5,22 @@
*/
import { Logger } from "@utils/Logger";
-import { canonicalizeMatch } from "@utils/patches";
import * as Webpack from "@webpack";
-import { wreq } from "@webpack";
import { patches } from "plugins";
+import { loadLazyChunks } from "./loadLazyChunks";
+
const ReporterLogger = new Logger("Reporter");
async function runReporter() {
- ReporterLogger.log("Starting test...");
-
try {
- const validChunks = new Set();
- const invalidChunks = new Set();
- const deferredRequires = new Set();
+ ReporterLogger.log("Starting test...");
- let chunksSearchingResolve: (value: void | PromiseLike) => void;
- const chunksSearchingDone = new Promise(r => chunksSearchingResolve = r);
+ let loadLazyChunksResolve: (value: void | PromiseLike) => void;
+ const loadLazyChunksDone = new Promise(r => loadLazyChunksResolve = r);
- // True if resolved, false otherwise
- const chunksSearchPromises = [] as Array<() => boolean>;
-
- const LazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?(\i\.e\("[^)]+?"\)[^\]]*?)(?:\]\))?)\.then\(\i\.bind\(\i,"([^)]+?)"\)\)/g);
-
- async function searchAndLoadLazyChunks(factoryCode: string) {
- const lazyChunks = factoryCode.matchAll(LazyChunkRegex);
- const validChunkGroups = new Set<[chunkIds: string[], entryPoint: string]>();
-
- // Workaround for a chunk that depends on the ChannelMessage component but may be be force loaded before
- // the chunk containing the component
- const shouldForceDefer = factoryCode.includes(".Messages.GUILD_FEED_UNFEATURE_BUTTON_TEXT");
-
- await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => {
- const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Webpack.ChunkIdsRegex)).map(m => m[1]) : [];
-
- if (chunkIds.length === 0) {
- return;
- }
-
- let invalidChunkGroup = false;
-
- for (const id of chunkIds) {
- if (wreq.u(id) == null || wreq.u(id) === "undefined.js") continue;
-
- const isWasm = await fetch(wreq.p + wreq.u(id))
- .then(r => r.text())
- .then(t => (IS_WEB && t.includes(".module.wasm")) || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push"));
-
- if (isWasm && IS_WEB) {
- invalidChunks.add(id);
- invalidChunkGroup = true;
- continue;
- }
-
- validChunks.add(id);
- }
-
- if (!invalidChunkGroup) {
- validChunkGroups.add([chunkIds, entryPoint]);
- }
- }));
-
- // Loads all found valid chunk groups
- await Promise.all(
- Array.from(validChunkGroups)
- .map(([chunkIds]) =>
- Promise.all(chunkIds.map(id => wreq.e(id as any).catch(() => { })))
- )
- );
-
- // Requires the entry points for all valid chunk groups
- for (const [, entryPoint] of validChunkGroups) {
- try {
- if (shouldForceDefer) {
- deferredRequires.add(entryPoint);
- continue;
- }
-
- if (wreq.m[entryPoint]) wreq(entryPoint as any);
- } catch (err) {
- console.error(err);
- }
- }
-
- // setImmediate to only check if all chunks were loaded after this function resolves
- // We check if all chunks were loaded every time a factory is loaded
- // If we are still looking for chunks in the other factories, the array will have that factory's chunk search promise not resolved
- // But, if all chunk search promises are resolved, this means we found every lazy chunk loaded by Discord code and manually loaded them
- setTimeout(() => {
- let allResolved = true;
-
- for (let i = 0; i < chunksSearchPromises.length; i++) {
- const isResolved = chunksSearchPromises[i]();
-
- if (isResolved) {
- // Remove finished promises to avoid having to iterate through a huge array everytime
- chunksSearchPromises.splice(i--, 1);
- } else {
- allResolved = false;
- }
- }
-
- if (allResolved) chunksSearchingResolve();
- }, 0);
- }
-
- Webpack.beforeInitListeners.add(async () => {
- ReporterLogger.log("Loading all chunks...");
-
- Webpack.factoryListeners.add(factory => {
- let isResolved = false;
- searchAndLoadLazyChunks(factory.toString()).then(() => isResolved = true);
-
- chunksSearchPromises.push(() => isResolved);
- });
-
- // setImmediate to only search the initial factories after Discord initialized the app
- // our beforeInitListeners are called before Discord initializes the app
- setTimeout(() => {
- for (const factoryId in wreq.m) {
- let isResolved = false;
- searchAndLoadLazyChunks(wreq.m[factoryId].toString()).then(() => isResolved = true);
-
- chunksSearchPromises.push(() => isResolved);
- }
- }, 0);
- });
-
- await chunksSearchingDone;
-
- // Require deferred entry points
- for (const deferredRequire of deferredRequires) {
- wreq!(deferredRequire as any);
- }
-
- // All chunks Discord has mapped to asset files, even if they are not used anymore
- const allChunks = [] as string[];
-
- // Matches "id" or id:
- for (const currentMatch of wreq!.u.toString().matchAll(/(?:"(\d+?)")|(?:(\d+?):)/g)) {
- const id = currentMatch[1] ?? currentMatch[2];
- if (id == null) continue;
-
- allChunks.push(id);
- }
-
- if (allChunks.length === 0) throw new Error("Failed to get all chunks");
-
- // Chunks that are not loaded (not used) by Discord code anymore
- const chunksLeft = allChunks.filter(id => {
- return !(validChunks.has(id) || invalidChunks.has(id));
- });
-
- await Promise.all(chunksLeft.map(async id => {
- const isWasm = await fetch(wreq.p + wreq.u(id))
- .then(r => r.text())
- .then(t => (IS_WEB && t.includes(".module.wasm")) || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push"));
-
- // Loads and requires a chunk
- if (!isWasm) {
- await wreq.e(id as any);
- if (wreq.m[id]) wreq(id as any);
- }
- }));
-
- ReporterLogger.log("Finished loading all chunks!");
+ Webpack.beforeInitListeners.add(() => loadLazyChunks().then((loadLazyChunksResolve)));
+ await loadLazyChunksDone;
for (const patch of patches) {
if (!patch.all) {
diff --git a/src/plugins/appleMusic.desktop/README.md b/src/plugins/appleMusic.desktop/README.md
new file mode 100644
index 000000000..52ab93bfd
--- /dev/null
+++ b/src/plugins/appleMusic.desktop/README.md
@@ -0,0 +1,9 @@
+# AppleMusicRichPresence
+
+This plugin enables Discord rich presence for your Apple Music! (This only works on macOS with the Music app.)
+
+![Screenshot of the activity in Discord](https://github.com/Vendicated/Vencord/assets/70191398/1f811090-ab5f-4060-a9ee-d0ac44a1d3c0)
+
+## Configuration
+
+For the customizable activity format strings, you can use several special strings to include track data in activities! `{name}` is replaced with the track name; `{artist}` is replaced with the artist(s)' name(s); and `{album}` is replaced with the album name.
diff --git a/src/plugins/appleMusic.desktop/index.tsx b/src/plugins/appleMusic.desktop/index.tsx
new file mode 100644
index 000000000..16591028d
--- /dev/null
+++ b/src/plugins/appleMusic.desktop/index.tsx
@@ -0,0 +1,253 @@
+/*
+ * 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 } from "@utils/constants";
+import definePlugin, { OptionType, PluginNative } from "@utils/types";
+import { ApplicationAssetUtils, FluxDispatcher, Forms } from "@webpack/common";
+
+const Native = VencordNative.pluginHelpers.AppleMusic as PluginNative;
+
+interface ActivityAssets {
+ large_image?: string;
+ large_text?: string;
+ small_image?: string;
+ small_text?: string;
+}
+
+interface ActivityButton {
+ label: string;
+ url: string;
+}
+
+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: number;
+ flags: number;
+}
+
+const enum ActivityType {
+ PLAYING = 0,
+ LISTENING = 2,
+}
+
+const enum ActivityFlag {
+ INSTANCE = 1 << 0,
+}
+
+export interface TrackData {
+ name: string;
+ album: string;
+ artist: string;
+
+ appleMusicLink?: string;
+ songLink?: string;
+
+ albumArtwork?: string;
+ artistArtwork?: string;
+
+ playerPosition: number;
+ duration: number;
+}
+
+const enum AssetImageType {
+ Album = "Album",
+ Artist = "Artist",
+}
+
+const applicationId = "1239490006054207550";
+
+function setActivity(activity: Activity | null) {
+ FluxDispatcher.dispatch({
+ type: "LOCAL_ACTIVITY_UPDATE",
+ activity,
+ socketId: "AppleMusic",
+ });
+}
+
+const settings = definePluginSettings({
+ activityType: {
+ type: OptionType.SELECT,
+ description: "Which type of activity",
+ options: [
+ { label: "Playing", value: ActivityType.PLAYING, default: true },
+ { label: "Listening", value: ActivityType.LISTENING }
+ ],
+ },
+ refreshInterval: {
+ type: OptionType.SLIDER,
+ description: "The interval between activity refreshes (seconds)",
+ markers: [1, 2, 2.5, 3, 5, 10, 15],
+ default: 5,
+ restartNeeded: true,
+ },
+ enableTimestamps: {
+ type: OptionType.BOOLEAN,
+ description: "Whether or not to enable timestamps",
+ default: true,
+ },
+ enableButtons: {
+ type: OptionType.BOOLEAN,
+ description: "Whether or not to enable buttons",
+ default: true,
+ },
+ nameString: {
+ type: OptionType.STRING,
+ description: "Activity name format string",
+ default: "Apple Music"
+ },
+ detailsString: {
+ type: OptionType.STRING,
+ description: "Activity details format string",
+ default: "{name}"
+ },
+ stateString: {
+ type: OptionType.STRING,
+ description: "Activity state format string",
+ default: "{artist}"
+ },
+ largeImageType: {
+ type: OptionType.SELECT,
+ description: "Activity assets large image type",
+ options: [
+ { label: "Album artwork", value: AssetImageType.Album, default: true },
+ { label: "Artist artwork", value: AssetImageType.Artist }
+ ],
+ },
+ largeTextString: {
+ type: OptionType.STRING,
+ description: "Activity assets large text format string",
+ default: "{album}"
+ },
+ smallImageType: {
+ type: OptionType.SELECT,
+ description: "Activity assets small image type",
+ options: [
+ { label: "Album artwork", value: AssetImageType.Album },
+ { label: "Artist artwork", value: AssetImageType.Artist, default: true }
+ ],
+ },
+ smallTextString: {
+ type: OptionType.STRING,
+ description: "Activity assets small text format string",
+ default: "{artist}"
+ },
+});
+
+function customFormat(formatStr: string, data: TrackData) {
+ return formatStr
+ .replaceAll("{name}", data.name)
+ .replaceAll("{album}", data.album)
+ .replaceAll("{artist}", data.artist);
+}
+
+function getImageAsset(type: AssetImageType, data: TrackData) {
+ const source = type === AssetImageType.Album
+ ? data.albumArtwork
+ : data.artistArtwork;
+
+ if (!source) return undefined;
+
+ return ApplicationAssetUtils.fetchAssetIds(applicationId, [source]).then(ids => ids[0]);
+}
+
+export default definePlugin({
+ name: "AppleMusicRichPresence",
+ description: "Discord rich presence for your Apple Music!",
+ authors: [Devs.RyanCaoDev],
+ hidden: !navigator.platform.startsWith("Mac"),
+
+ settingsAboutComponent() {
+ return <>
+
+ For the customizable activity format strings, you can use several special strings to include track data in activities!{" "}
+ {"{name}"}
is replaced with the track name; {"{artist}"}
is replaced with the artist(s)' name(s); and {"{album}"}
is replaced with the album name.
+
+ >;
+ },
+
+ settings,
+
+ start() {
+ this.updatePresence();
+ this.updateInterval = setInterval(() => { this.updatePresence(); }, settings.store.refreshInterval * 1000);
+ },
+
+ stop() {
+ clearInterval(this.updateInterval);
+ FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", activity: null });
+ },
+
+ updatePresence() {
+ this.getActivity().then(activity => { setActivity(activity); });
+ },
+
+ async getActivity(): Promise {
+ const trackData = await Native.fetchTrackData();
+ if (!trackData) return null;
+
+ const [largeImageAsset, smallImageAsset] = await Promise.all([
+ getImageAsset(settings.store.largeImageType, trackData),
+ getImageAsset(settings.store.smallImageType, trackData)
+ ]);
+
+ const assets: ActivityAssets = {
+ large_image: largeImageAsset,
+ large_text: customFormat(settings.store.largeTextString, trackData),
+ small_image: smallImageAsset,
+ small_text: customFormat(settings.store.smallTextString, trackData),
+ };
+
+ const buttons: ActivityButton[] = [];
+
+ if (settings.store.enableButtons) {
+ if (trackData.appleMusicLink)
+ buttons.push({
+ label: "Listen on Apple Music",
+ url: trackData.appleMusicLink,
+ });
+
+ if (trackData.songLink)
+ buttons.push({
+ label: "View on SongLink",
+ url: trackData.songLink,
+ });
+ }
+
+ return {
+ application_id: applicationId,
+
+ name: customFormat(settings.store.nameString, trackData),
+ details: customFormat(settings.store.detailsString, trackData),
+ state: customFormat(settings.store.stateString, trackData),
+
+ timestamps: (settings.store.enableTimestamps ? {
+ start: Date.now() - (trackData.playerPosition * 1000),
+ end: Date.now() - (trackData.playerPosition * 1000) + (trackData.duration * 1000),
+ } : undefined),
+
+ assets,
+
+ buttons: buttons.length ? buttons.map(v => v.label) : undefined,
+ metadata: { button_urls: buttons.map(v => v.url) || undefined, },
+
+ type: settings.store.activityType,
+ flags: ActivityFlag.INSTANCE,
+ };
+ }
+});
diff --git a/src/plugins/appleMusic.desktop/native.ts b/src/plugins/appleMusic.desktop/native.ts
new file mode 100644
index 000000000..2eb2a0757
--- /dev/null
+++ b/src/plugins/appleMusic.desktop/native.ts
@@ -0,0 +1,120 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { execFile } from "child_process";
+import { promisify } from "util";
+
+import type { TrackData } from ".";
+
+const exec = promisify(execFile);
+
+// function exec(file: string, args: string[] = []) {
+// return new Promise<{ code: number | null, stdout: string | null, stderr: string | null; }>((resolve, reject) => {
+// const process = spawn(file, args, { stdio: [null, "pipe", "pipe"] });
+
+// let stdout: string | null = null;
+// process.stdout.on("data", (chunk: string) => { stdout ??= ""; stdout += chunk; });
+// let stderr: string | null = null;
+// process.stderr.on("data", (chunk: string) => { stdout ??= ""; stderr += chunk; });
+
+// process.on("exit", code => { resolve({ code, stdout, stderr }); });
+// process.on("error", err => reject(err));
+// });
+// }
+
+async function applescript(cmds: string[]) {
+ const { stdout } = await exec("osascript", cmds.map(c => ["-e", c]).flat());
+ return stdout;
+}
+
+function makeSearchUrl(type: string, query: string) {
+ const url = new URL("https://tools.applemediaservices.com/api/apple-media/music/US/search.json");
+ url.searchParams.set("types", type);
+ url.searchParams.set("limit", "1");
+ url.searchParams.set("term", query);
+ return url;
+}
+
+const requestOptions: RequestInit = {
+ headers: { "user-agent": "Mozilla/5.0 (Windows NT 10.0; rv:125.0) Gecko/20100101 Firefox/125.0" },
+};
+
+interface RemoteData {
+ appleMusicLink?: string,
+ songLink?: string,
+ albumArtwork?: string,
+ artistArtwork?: string;
+}
+
+let cachedRemoteData: { id: string, data: RemoteData; } | { id: string, failures: number; } | null = null;
+
+async function fetchRemoteData({ id, name, artist, album }: { id: string, name: string, artist: string, album: string; }) {
+ if (id === cachedRemoteData?.id) {
+ if ("data" in cachedRemoteData) return cachedRemoteData.data;
+ if ("failures" in cachedRemoteData && cachedRemoteData.failures >= 5) return null;
+ }
+
+ try {
+ const [songData, artistData] = await Promise.all([
+ fetch(makeSearchUrl("songs", artist + " " + album + " " + name), requestOptions).then(r => r.json()),
+ fetch(makeSearchUrl("artists", artist.split(/ *[,&] */)[0]), requestOptions).then(r => r.json())
+ ]);
+
+ const appleMusicLink = songData?.songs?.data[0]?.attributes.url;
+ const songLink = songData?.songs?.data[0]?.id ? `https://song.link/i/${songData?.songs?.data[0]?.id}` : undefined;
+
+ const albumArtwork = songData?.songs?.data[0]?.attributes.artwork.url.replace("{w}", "512").replace("{h}", "512");
+ const artistArtwork = artistData?.artists?.data[0]?.attributes.artwork.url.replace("{w}", "512").replace("{h}", "512");
+
+ cachedRemoteData = {
+ id,
+ data: { appleMusicLink, songLink, albumArtwork, artistArtwork }
+ };
+ return cachedRemoteData.data;
+ } catch (e) {
+ console.error("[AppleMusicRichPresence] Failed to fetch remote data:", e);
+ cachedRemoteData = {
+ id,
+ failures: (id === cachedRemoteData?.id && "failures" in cachedRemoteData ? cachedRemoteData.failures : 0) + 1
+ };
+ return null;
+ }
+}
+
+export async function fetchTrackData(): Promise {
+ try {
+ await exec("pgrep", ["^Music$"]);
+ } catch (error) {
+ return null;
+ }
+
+ const playerState = await applescript(['tell application "Music"', "get player state", "end tell"])
+ .then(out => out.trim());
+ if (playerState !== "playing") return null;
+
+ const playerPosition = await applescript(['tell application "Music"', "get player position", "end tell"])
+ .then(text => Number.parseFloat(text.trim()));
+
+ const stdout = await applescript([
+ 'set output to ""',
+ 'tell application "Music"',
+ "set t_id to database id of current track",
+ "set t_name to name of current track",
+ "set t_album to album of current track",
+ "set t_artist to artist of current track",
+ "set t_duration to duration of current track",
+ 'set output to "" & t_id & "\\n" & t_name & "\\n" & t_album & "\\n" & t_artist & "\\n" & t_duration',
+ "end tell",
+ "return output"
+ ]);
+
+ const [id, name, album, artist, durationStr] = stdout.split("\n").filter(k => !!k);
+ const duration = Number.parseFloat(durationStr);
+
+ const remoteData = await fetchRemoteData({ id, name, artist, album });
+
+ return { name, album, artist, playerPosition, duration, ...remoteData };
+}
diff --git a/src/plugins/consoleShortcuts/index.ts b/src/plugins/consoleShortcuts/index.ts
index ee86b5fcf..0a1323e75 100644
--- a/src/plugins/consoleShortcuts/index.ts
+++ b/src/plugins/consoleShortcuts/index.ts
@@ -25,6 +25,7 @@ import definePlugin, { PluginNative, StartAt } from "@utils/types";
import * as Webpack from "@webpack";
import { extract, filters, findAll, findModuleId, search } from "@webpack";
import * as Common from "@webpack/common";
+import { loadLazyChunks } from "debug/loadLazyChunks";
import type { ComponentType } from "react";
const DESKTOP_ONLY = (f: string) => () => {
@@ -82,6 +83,7 @@ function makeShortcuts() {
wpsearch: search,
wpex: extract,
wpexs: (code: string) => extract(findModuleId(code)!),
+ loadLazyChunks: IS_DEV ? loadLazyChunks : () => { throw new Error("loadLazyChunks is dev only."); },
find,
findAll: findAll,
findByProps,
diff --git a/src/plugins/copyEmojiMarkdown/README.md b/src/plugins/copyEmojiMarkdown/README.md
new file mode 100644
index 000000000..9e62e6635
--- /dev/null
+++ b/src/plugins/copyEmojiMarkdown/README.md
@@ -0,0 +1,5 @@
+# CopyEmojiMarkdown
+
+Allows you to copy emojis as formatted string. Custom emojis will be copied as `<:trolley:1024751352028602449>`, default emojis as `🛒`
+
+![](https://github.com/Vendicated/Vencord/assets/45497981/417f345a-7031-4fe7-8e42-e238870cd547)
diff --git a/src/plugins/copyEmojiMarkdown/index.tsx b/src/plugins/copyEmojiMarkdown/index.tsx
new file mode 100644
index 000000000..a9c018a91
--- /dev/null
+++ b/src/plugins/copyEmojiMarkdown/index.tsx
@@ -0,0 +1,75 @@
+/*
+ * 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 } from "@utils/constants";
+import { copyWithToast } from "@utils/misc";
+import definePlugin, { OptionType } from "@utils/types";
+import { findByPropsLazy } from "@webpack";
+import { Menu } from "@webpack/common";
+
+const { convertNameToSurrogate } = findByPropsLazy("convertNameToSurrogate");
+
+interface Emoji {
+ type: string;
+ id: string;
+ name: string;
+}
+
+interface Target {
+ dataset: Emoji;
+ firstChild: HTMLImageElement;
+}
+
+function getEmojiMarkdown(target: Target, copyUnicode: boolean): string {
+ const { id: emojiId, name: emojiName } = target.dataset;
+
+ if (!emojiId) {
+ return copyUnicode
+ ? convertNameToSurrogate(emojiName)
+ : `:${emojiName}:`;
+ }
+
+ const extension = target?.firstChild.src.match(
+ /https:\/\/cdn\.discordapp\.com\/emojis\/\d+\.(\w+)/
+ )?.[1];
+
+ return `<${extension === "gif" ? "a" : ""}:${emojiName.replace(/~\d+$/, "")}:${emojiId}>`;
+}
+
+const settings = definePluginSettings({
+ copyUnicode: {
+ type: OptionType.BOOLEAN,
+ description: "Copy the raw unicode character instead of :name: for default emojis (👽)",
+ default: true,
+ },
+});
+
+export default definePlugin({
+ name: "CopyEmojiMarkdown",
+ description: "Allows you to copy emojis as formatted string (<:blobcatcozy:1026533070955872337>)",
+ authors: [Devs.HappyEnderman, Devs.Vishnya],
+ settings,
+
+ contextMenus: {
+ "expression-picker"(children, { target }: { target: Target }) {
+ if (target.dataset.type !== "emoji") return;
+
+ children.push(
+ {
+ copyWithToast(
+ getEmojiMarkdown(target, settings.store.copyUnicode),
+ "Success! Copied emoji markdown."
+ );
+ }}
+ />
+ );
+ },
+ },
+});
diff --git a/src/plugins/experiments/hideBugReport.css b/src/plugins/experiments/hideBugReport.css
new file mode 100644
index 000000000..ff78555d7
--- /dev/null
+++ b/src/plugins/experiments/hideBugReport.css
@@ -0,0 +1,3 @@
+#staff-help-popout-staff-help-bug-reporter {
+ display: none;
+}
diff --git a/src/plugins/experiments/index.tsx b/src/plugins/experiments/index.tsx
index 50b9521f9..cf4dbf249 100644
--- a/src/plugins/experiments/index.tsx
+++ b/src/plugins/experiments/index.tsx
@@ -16,31 +16,22 @@
* along with this program. If not, see .
*/
-import { definePluginSettings } from "@api/Settings";
+import { disableStyle, enableStyle } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { ErrorCard } from "@components/ErrorCard";
import { Devs } from "@utils/constants";
-import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins";
-import definePlugin, { OptionType } from "@utils/types";
+import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
-import { Forms, React, UserStore } from "@webpack/common";
-import { User } from "discord-types/general";
+import { Forms, React } from "@webpack/common";
+
+import hideBugReport from "./hideBugReport.css?managed";
const KbdStyles = findByPropsLazy("key", "removeBuildOverride");
-const settings = definePluginSettings({
- enableIsStaff: {
- description: "Enable isStaff",
- type: OptionType.BOOLEAN,
- default: false,
- restartNeeded: true
- }
-});
-
export default definePlugin({
name: "Experiments",
- description: "Enable Access to Experiments in Discord!",
+ description: "Enable Access to Experiments & other dev-only features in Discord!",
authors: [
Devs.Megu,
Devs.Ven,
@@ -48,7 +39,6 @@ export default definePlugin({
Devs.BanTheNons,
Devs.Nuckyz
],
- settings,
patches: [
{
@@ -65,37 +55,25 @@ export default definePlugin({
replace: "$1=!0;"
}
},
- {
- find: '"isStaff",',
- predicate: () => settings.store.enableIsStaff,
- replacement: [
- {
- match: /(?<=>)(\i)\.hasFlag\((\i\.\i)\.STAFF\)(?=})/,
- replace: (_, user, flags) => `$self.isStaff(${user},${flags})`
- },
- {
- match: /hasFreePremium\(\){return this.isStaff\(\)\s*?\|\|/,
- replace: "hasFreePremium(){return ",
- }
- ]
- },
{
find: 'H1,title:"Experiments"',
replacement: {
match: 'title:"Experiments",children:[',
replace: "$&$self.WarningCard(),"
}
+ },
+ // change top right chat toolbar button from the help one to the dev one
+ {
+ find: "toolbar:function",
+ replacement: {
+ match: /\i\.isStaff\(\)/,
+ replace: "true"
+ }
}
],
- isStaff(user: User, flags: any) {
- try {
- return UserStore.getCurrentUser()?.id === user.id || user.hasFlag(flags.STAFF);
- } catch (err) {
- new Logger("Experiments").error(err);
- return user.hasFlag(flags.STAFF);
- }
- },
+ start: () => enableStyle(hideBugReport),
+ stop: () => disableStyle(hideBugReport),
settingsAboutComponent: () => {
const isMacOS = navigator.platform.includes("Mac");
@@ -105,14 +83,10 @@ export default definePlugin({
More Information
- You can enable client DevTools{" "}
+ You can open Discord's DevTools via {" "}
{modKey} +{" "}
{altKey} +{" "}
O{" "}
- after enabling isStaff
below
-
-
- and then toggling Enable DevTools
in the Developer Options
tab in settings.
);
@@ -128,6 +102,12 @@ export default definePlugin({
Only use experiments if you know what you're doing. Vencord is not responsible for any damage caused by enabling experiments.
+
+ If you don't know what an experiment does, ignore it. Do not ask us what experiments do either, we probably don't know.
+
+
+
+ No, you cannot use server-side features like checking the "Send to Client" box.
), { noop: true })
diff --git a/src/plugins/index.ts b/src/plugins/index.ts
index 53ab7983a..32bfe7e97 100644
--- a/src/plugins/index.ts
+++ b/src/plugins/index.ts
@@ -44,7 +44,6 @@ const settings = Settings.plugins;
export function isPluginEnabled(p: string) {
return (
- IS_REPORTER ||
Plugins[p]?.required ||
Plugins[p]?.isDependency ||
settings[p]?.enabled
diff --git a/src/plugins/noOnboardingDelay/index.ts b/src/plugins/noOnboardingDelay/index.ts
new file mode 100644
index 000000000..6211e97c2
--- /dev/null
+++ b/src/plugins/noOnboardingDelay/index.ts
@@ -0,0 +1,35 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2022 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+*/
+
+import { Devs } from "@utils/constants";
+import definePlugin from "@utils/types";
+
+export default definePlugin({
+ name: "NoOnboardingDelay",
+ description: "Skips the slow and annoying onboarding delay",
+ authors: [Devs.nekohaxx],
+ patches: [
+ {
+ find: "Messages.ONBOARDING_COVER_WELCOME_SUBTITLE",
+ replacement: {
+ match: "3e3",
+ replace: "0"
+ },
+ },
+ ],
+});
diff --git a/src/plugins/noPendingCount/index.ts b/src/plugins/noPendingCount/index.ts
index 29458df9d..57a65f52c 100644
--- a/src/plugins/noPendingCount/index.ts
+++ b/src/plugins/noPendingCount/index.ts
@@ -62,6 +62,16 @@ export default definePlugin({
replace: "return 0;"
}
},
+ // New message requests hook
+ {
+ find: "useNewMessageRequestsCount:",
+ predicate: () => settings.store.hideMessageRequestsCount,
+ replacement: {
+ match: /getNonChannelAckId\(\i\.\i\.MESSAGE_REQUESTS\).+?return /,
+ replace: "$&0;"
+ }
+ },
+ // Old message requests hook
{
find: "getMessageRequestsCount(){",
predicate: () => settings.store.hideMessageRequestsCount,
diff --git a/src/plugins/partyMode/index.ts b/src/plugins/partyMode/index.ts
index 56c19c02c..c40f2e3c7 100644
--- a/src/plugins/partyMode/index.ts
+++ b/src/plugins/partyMode/index.ts
@@ -18,7 +18,7 @@
import { definePluginSettings, migratePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
-import definePlugin, { OptionType } from "@utils/types";
+import definePlugin, { OptionType, ReporterTestable } from "@utils/types";
import { FluxDispatcher } from "@webpack/common";
const enum Intensity {
@@ -46,6 +46,7 @@ export default definePlugin({
name: "PartyMode",
description: "Allows you to use party mode cause the party never ends ✨",
authors: [Devs.UwUDev],
+ reporterTestable: ReporterTestable.None,
settings,
start() {
diff --git a/src/plugins/platformIndicators/index.tsx b/src/plugins/platformIndicators/index.tsx
index 9fae9adfa..ea2ae125c 100644
--- a/src/plugins/platformIndicators/index.tsx
+++ b/src/plugins/platformIndicators/index.tsx
@@ -51,14 +51,17 @@ const Icons = {
desktop: Icon("M4 2.5c-1.103 0-2 .897-2 2v11c0 1.104.897 2 2 2h7v2H7v2h10v-2h-4v-2h7c1.103 0 2-.896 2-2v-11c0-1.103-.897-2-2-2H4Zm16 2v9H4v-9h16Z"),
web: Icon("M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93Zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39Z"),
mobile: Icon("M 187 0 L 813 0 C 916.277 0 1000 83.723 1000 187 L 1000 1313 C 1000 1416.277 916.277 1500 813 1500 L 187 1500 C 83.723 1500 0 1416.277 0 1313 L 0 187 C 0 83.723 83.723 0 187 0 Z M 125 1000 L 875 1000 L 875 250 L 125 250 Z M 500 1125 C 430.964 1125 375 1180.964 375 1250 C 375 1319.036 430.964 1375 500 1375 C 569.036 1375 625 1319.036 625 1250 C 625 1180.964 569.036 1125 500 1125 Z", { viewBox: "0 0 1000 1500", height: 17, width: 17 }),
- console: Icon("M14.8 2.7 9 3.1V47h3.3c1.7 0 6.2.3 10 .7l6.7.6V2l-4.2.2c-2.4.1-6.9.3-10 .5zm1.8 6.4c1 1.7-1.3 3.6-2.7 2.2C12.7 10.1 13.5 8 15 8c.5 0 1.2.5 1.6 1.1zM16 33c0 6-.4 10-1 10s-1-4-1-10 .4-10 1-10 1 4 1 10zm15-8v23.3l3.8-.7c2-.3 4.7-.6 6-.6H43V3h-2.2c-1.3 0-4-.3-6-.6L31 1.7V25z", { viewBox: "0 0 50 50" }),
+ embedded: Icon("M14.8 2.7 9 3.1V47h3.3c1.7 0 6.2.3 10 .7l6.7.6V2l-4.2.2c-2.4.1-6.9.3-10 .5zm1.8 6.4c1 1.7-1.3 3.6-2.7 2.2C12.7 10.1 13.5 8 15 8c.5 0 1.2.5 1.6 1.1zM16 33c0 6-.4 10-1 10s-1-4-1-10 .4-10 1-10 1 4 1 10zm15-8v23.3l3.8-.7c2-.3 4.7-.6 6-.6H43V3h-2.2c-1.3 0-4-.3-6-.6L31 1.7V25z", { viewBox: "0 0 50 50" }),
};
type Platform = keyof typeof Icons;
const StatusUtils = findByPropsLazy("useStatusFillColor", "StatusTypes");
const PlatformIcon = ({ platform, status, small }: { platform: Platform, status: string; small: boolean; }) => {
- const tooltip = platform[0].toUpperCase() + platform.slice(1);
+ const tooltip = platform === "embedded"
+ ? "Console"
+ : platform[0].toUpperCase() + platform.slice(1);
+
const Icon = Icons[platform] ?? Icons.desktop;
return ;
diff --git a/src/plugins/serverProfile/GuildProfileModal.tsx b/src/plugins/serverInfo/GuildInfoModal.tsx
similarity index 98%
rename from src/plugins/serverProfile/GuildProfileModal.tsx
rename to src/plugins/serverInfo/GuildInfoModal.tsx
index 8e6f60518..bed520b67 100644
--- a/src/plugins/serverProfile/GuildProfileModal.tsx
+++ b/src/plugins/serverInfo/GuildInfoModal.tsx
@@ -20,10 +20,10 @@ const FriendRow = findExportedComponentLazy("FriendRow");
const cl = classNameFactory("vc-gp-");
-export function openGuildProfileModal(guild: Guild) {
+export function openGuildInfoModal(guild: Guild) {
openModal(props =>
-
+
);
}
@@ -53,7 +53,7 @@ function renderTimestamp(timestamp: number) {
);
}
-function GuildProfileModal({ guild }: GuildProps) {
+function GuildInfoModal({ guild }: GuildProps) {
const [friendCount, setFriendCount] = useState();
const [blockedCount, setBlockedCount] = useState();
diff --git a/src/plugins/serverInfo/README.md b/src/plugins/serverInfo/README.md
new file mode 100644
index 000000000..98c9013e0
--- /dev/null
+++ b/src/plugins/serverInfo/README.md
@@ -0,0 +1,7 @@
+# ServerInfo
+
+Allows you to view info about servers and see friends and blocked users
+
+![](https://github.com/Vendicated/Vencord/assets/45497981/a49783b5-e8fc-41d8-968f-58600e9f6580)
+![](https://github.com/Vendicated/Vencord/assets/45497981/5efc158a-e671-4196-a15a-77edf79a2630)
+![Available as "Server Profile" option in the server context menu](https://github.com/Vendicated/Vencord/assets/45497981/f43be943-6dc4-4232-9709-fbeb382d8e54)
diff --git a/src/plugins/serverProfile/index.tsx b/src/plugins/serverInfo/index.tsx
similarity index 65%
rename from src/plugins/serverProfile/index.tsx
rename to src/plugins/serverInfo/index.tsx
index 9d495c9d3..be3172f01 100644
--- a/src/plugins/serverProfile/index.tsx
+++ b/src/plugins/serverInfo/index.tsx
@@ -5,30 +5,32 @@
*/
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
+import { migratePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { Menu } from "@webpack/common";
import { Guild } from "discord-types/general";
-import { openGuildProfileModal } from "./GuildProfileModal";
+import { openGuildInfoModal } from "./GuildInfoModal";
const Patch: NavContextMenuPatchCallback = (children, { guild }: { guild: Guild; }) => {
const group = findGroupChildrenByChildId("privacy", children);
group?.push(
openGuildProfileModal(guild)}
+ action={() => openGuildInfoModal(guild)}
/>
);
};
+migratePluginSettings("ServerInfo", "ServerProfile"); // what was I thinking with this name lmao
export default definePlugin({
- name: "ServerProfile",
- description: "Allows you to view info about a server by right clicking it in the server list",
+ name: "ServerInfo",
+ description: "Allows you to view info about a server",
authors: [Devs.Ven, Devs.Nuckyz],
- tags: ["guild", "info"],
+ tags: ["guild", "info", "ServerProfile"],
contextMenus: {
"guild-context": Patch,
"guild-header-popout": Patch
diff --git a/src/plugins/serverProfile/styles.css b/src/plugins/serverInfo/styles.css
similarity index 100%
rename from src/plugins/serverProfile/styles.css
rename to src/plugins/serverInfo/styles.css
diff --git a/src/plugins/serverProfile/README.md b/src/plugins/serverProfile/README.md
deleted file mode 100644
index 9da70e74e..000000000
--- a/src/plugins/serverProfile/README.md
+++ /dev/null
@@ -1,7 +0,0 @@
-# ServerProfile
-
-Allows you to view info about servers and see friends and blocked users
-
-![image](https://github.com/Vendicated/Vencord/assets/45497981/a49783b5-e8fc-41d8-968f-58600e9f6580)
-![image](https://github.com/Vendicated/Vencord/assets/45497981/5efc158a-e671-4196-a15a-77edf79a2630)
-![image](https://github.com/Vendicated/Vencord/assets/45497981/f43be943-6dc4-4232-9709-fbeb382d8e54)
diff --git a/src/plugins/spotifyControls/index.tsx b/src/plugins/spotifyControls/index.tsx
index 06595892f..f7972aa36 100644
--- a/src/plugins/spotifyControls/index.tsx
+++ b/src/plugins/spotifyControls/index.tsx
@@ -77,6 +77,13 @@ export default definePlugin({
match: /repeat:"off"!==(.{1,3}),/,
replace: "actual_repeat:$1,$&"
}
+ },
+ {
+ find: "artists.filter",
+ replacement: {
+ match: /\(0,(\i)\.isNotNullish\)\((\i)\.id\)&&/,
+ replace: ""
+ }
}
],
diff --git a/src/plugins/usrbg/index.tsx b/src/plugins/usrbg/index.tsx
index 1221cb9c5..b8e9f14b3 100644
--- a/src/plugins/usrbg/index.tsx
+++ b/src/plugins/usrbg/index.tsx
@@ -74,15 +74,15 @@ export default definePlugin({
]
},
{
- find: /overrideBannerSrc:\i,profileType:/,
+ find: /overrideBannerSrc:\i,overrideBannerWidth:/,
replacement: [
{
match: /(\i)\.premiumType/,
replace: "$self.premiumHook($1)||$&"
},
{
- match: /(?<=function \i\((\i)\)\{)(?=var.{30,50},overrideBannerSrc:)/,
- replace: "$1.overrideBannerSrc=$self.useBannerHook($1);"
+ match: /function \i\((\i)\)\{/,
+ replace: "$&$1.overrideBannerSrc=$self.useBannerHook($1);"
}
]
},
diff --git a/src/plugins/viewIcons/index.tsx b/src/plugins/viewIcons/index.tsx
index 09254d511..a94689689 100644
--- a/src/plugins/viewIcons/index.tsx
+++ b/src/plugins/viewIcons/index.tsx
@@ -184,16 +184,16 @@ export default definePlugin({
patches: [
// Profiles Modal pfp
- {
- find: "User Profile Modal - Context Menu",
+ ...["User Profile Modal - Context Menu", ".UserProfileTypes.FULL_SIZE,hasProfileEffect:"].map(find => ({
+ find,
replacement: {
match: /\{src:(\i)(?=,avatarDecoration)/,
replace: "{src:$1,onClick:()=>$self.openImage($1)"
}
- },
+ })),
// Banners
- {
- find: ".NITRO_BANNER,",
+ ...[".NITRO_BANNER,", /overrideBannerSrc:\i,overrideBannerWidth:/].map(find => ({
+ find,
replacement: {
// style: { backgroundImage: shouldShowBanner ? "url(".concat(bannerUrl,
match: /style:\{(?=backgroundImage:(null!=\i)\?"url\("\.concat\((\i),)/,
@@ -201,7 +201,7 @@ export default definePlugin({
// onClick: () => shouldShowBanner && ev.target.style.backgroundImage && openImage(bannerUrl), style: { cursor: shouldShowBanner ? "pointer" : void 0,
'onClick:ev=>$1&&ev.target.style.backgroundImage&&$self.openImage($2),style:{cursor:$1?"pointer":void 0,'
}
- },
+ })),
// User DMs "User Profile" popup in the right
{
find: ".avatarPositionPanel",
@@ -210,6 +210,14 @@ export default definePlugin({
replace: "$1style:($2)?{cursor:\"pointer\"}:{},onClick:$2?()=>{$self.openImage($3)}"
}
},
+ {
+ find: ".canUsePremiumProfileCustomization,{avatarSrc:",
+ replacement: {
+ match: /children:\(0,\i\.jsx\)\(\i,{src:(\i)/,
+ replace: "style:{cursor:\"pointer\"},onClick:()=>{$self.openImage($1)},$&"
+
+ }
+ },
// Group DMs top small & large icon
{
find: /\.recipients\.length>=2(?!);
// iife so #__PURE__ works correctly
diff --git a/src/utils/types.ts b/src/utils/types.ts
index fe19a1093..2fa4a826e 100644
--- a/src/utils/types.ts
+++ b/src/utils/types.ts
@@ -85,6 +85,10 @@ export interface PluginDef {
* Whether this plugin is required and forcefully enabled
*/
required?: boolean;
+ /**
+ * Whether this plugin should be hidden from the user
+ */
+ hidden?: boolean;
/**
* Whether this plugin should be enabled by default, but can be disabled
*/