diff --git a/README.md b/README.md index 6e3eb003..ea28d026 100644 --- a/README.md +++ b/README.md @@ -111,8 +111,8 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch - Quoter by Samwich - RemixMe by kvba - RepeatMessage by Tolgchu -- ReplaceActivityTypes by Nyako - ReplyPingControl by ant0n & MrDiamond +- RPCEditor by Nyako & nin0dev - RPCStats by Samwich - SearchFix by Jaxx - SekaiStickers by MaiKokain diff --git a/src/equicordplugins/replaceActivityTypes/ReplaceSettings.tsx b/src/equicordplugins/replaceActivityTypes/ReplaceSettings.tsx deleted file mode 100644 index d5309380..00000000 --- a/src/equicordplugins/replaceActivityTypes/ReplaceSettings.tsx +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { CheckedTextInput } from "@components/CheckedTextInput"; -import { Margins } from "@utils/margins"; -import { identity } from "@utils/misc"; -import { findByCodeLazy } from "@webpack"; -import { Card, Forms, React, Select, SnowflakeUtils, Switch } from "@webpack/common"; - -import { makeEmptyAppId } from "."; -import { ActivityType, RpcApp, SettingsProps } from "./types"; - - -const fetchApplicationsRPC = findByCodeLazy("APPLICATION_RPC(", "Client ID"); - -const cachedApps: any = {}; -async function lookupApp(appId: string): Promise { - if (cachedApps[appId]) return cachedApps[appId]; - const socket: any = {}; - try { - await fetchApplicationsRPC(socket, appId); - console.log(`Lookup finished for ${socket.application.name}`); - cachedApps[appId] = socket.application; - return socket.application; - } catch { - console.log(`Lookup failed for ${appId}`); - return null; - } -} - -function isValidSnowflake(v: string) { - const regex = /^\d{17,20}$/; - return regex.test(v) && !Number.isNaN(SnowflakeUtils.extractTimestamp(v)); -} - -export function ReplaceTutorial() { - return ( - <> - How to get an Application ID - - The method of getting an app's id will differ depending on what app it is. If the source code is available you can most likely find it inside the app's repository. - - - Another method is to start the app in question, then open Discord's console and look for a log from RPCServer saying something like - "cmd: 'SET_ACTIVITY'" with your app's name somewhere inside - - - - Note: ActivityTypes other than Playing will only show timestamps on Mobile. It's a Discord issue. - - - ); -} - -export function ReplaceSettings({ appIds, update, save }: SettingsProps) { - async function onChange(val: string | boolean, index: number, key: string) { - if (index === appIds.length - 1) - appIds.push(makeEmptyAppId()); - - appIds[index][key] = val; - - if (val && key === "appId") { - const tempApp = await lookupApp(val.toString()); - appIds[index].appName = tempApp?.name || "Unknown"; - } - - if (appIds[index].appId === "" && index !== appIds.length - 1) - appIds.splice(index, 1); - - save(); - update(); - } - - return ( - <> - { - appIds.map((setting, i) => - - { - onChange(value, i, "enabled"); - }} - className={Margins.bottom16} - hideBorder={true} - > - {setting.appName} - - Application ID - { - onChange(v, i, "appId"); - }} - validate={v => - !v || isValidSnowflake(v) || "Invalid appId, must be a snowflake" - } - /> - {setting.activityType === ActivityType.STREAMING && - <> - Stream URL - { - onChange(v, i, "streamUrl"); - }} - validate={st => !/https?:\/\/(www\.)?(twitch\.tv|youtube\.com)\/\w+/.test(st) && "Only Twitch and Youtube urls will work." || true} - /> - } - New activity type - { + onChange(value, i, "newActivityType"); + }} + className={Margins.top8} + isSelected={value => setting.newActivityType === value} + serialize={identity} + /> + { + setting.newActivityType === ActivityType.STREAMING && + <> + Stream URL (must be YouTube or Twitch) + { + onChange(v, i, "newStreamUrl"); + }} + validate={v => { + return /https?:\/\/(www\.)?(twitch\.tv|youtube\.com)\/\w+/.test(v) || "Invalid stream URL"; + }} + /> + + + } + { + setting.newActivityType !== ActivityType.STREAMING && + <> + New name {setting.newActivityType === ActivityType.PLAYING && "(first line)"} + { + onChange(v, i, "newName"); + }} + /> + + } + New details {setting.newActivityType === ActivityType.PLAYING ? "(second line)" : "(first line)"} + { + onChange(v, i, "newDetails"); + }} + /> + New state {setting.newActivityType === ActivityType.PLAYING ? "(third line)" : "(second line)"} + { + onChange(v, i, "newState"); + }} + /> + { + !setting.disableAssets && + <> + Large image + Text {setting.newActivityType !== ActivityType.PLAYING && "(also third line)"} + { + onChange(v, i, "newLargeImageText"); + }} + /> + URL + { + onChange(v, i, "newLargeImageUrl"); + }} + /> + Small image + Text + { + onChange(v, i, "newSmallImageText"); + }} + /> + URL + { + onChange(v, i, "newSmallImageUrl"); + }} + /> + + } + { + onChange(value, i, "disableAssets"); + }} + className={Margins.top8} + hideBorder={true} + style={{ marginBottom: "0" }} + > + Hide assets (large & small images) + + { + onChange(value, i, "disableTimestamps"); + }} + className={Margins.top8} + hideBorder={true} + style={{ marginBottom: "0" }} + > + Hide timestamps + + + } + + ) + } + + ); +} diff --git a/src/equicordplugins/rpcEditor/index.tsx b/src/equicordplugins/rpcEditor/index.tsx new file mode 100644 index 00000000..c3854ef7 --- /dev/null +++ b/src/equicordplugins/rpcEditor/index.tsx @@ -0,0 +1,155 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { DataStore } from "@api/index"; +import { definePluginSettings, migratePluginSettings } from "@api/Settings"; +import { Devs } from "@utils/constants"; +import { useForceUpdater } from "@utils/react"; +import definePlugin, { OptionType } from "@utils/types"; +import { React } from "@webpack/common"; + +import { ReplaceSettings, ReplaceTutorial } from "./ReplaceSettings"; + +const APP_IDS_KEY = "ReplaceActivityType_appids"; +export type AppIdSetting = { + disableAssets: boolean; + disableTimestamps: boolean; + appId: string; + enabled: boolean; + newActivityType: ActivityType; + newName: string, + newDetails: string, + newState: string, + newLargeImageUrl: string, + newLargeImageText: string, + newSmallImageUrl: string, + newSmallImageText: string; + newStreamUrl: string; +}; + +export interface Activity { + state: string; + details: string; + timestamps?: { + start?: number; + end?: number; + }; + url?: string; + assets: ActivityAssets; + buttons?: Array; + name: string; + application_id: string; + metadata?: { + button_urls?: Array; + }; + type: number; + flags: number; +} + +interface ActivityAssets { + large_image: string; + large_text: string; + small_image: string; + small_text: string; +} + +export const enum ActivityType { + PLAYING = 0, + STREAMING = 1, + LISTENING = 2, + WATCHING = 3, + COMPETING = 5 +} + +export const makeEmptyAppId: () => AppIdSetting = () => ({ + appId: "", + enabled: true, + newActivityType: ActivityType.PLAYING, + newName: "", + newDetails: "", + newState: "", + newLargeImageUrl: "", + newLargeImageText: "", + newSmallImageUrl: "", + newSmallImageText: "", + newStreamUrl: "", + disableTimestamps: false, + disableAssets: false +}); + +let appIds = [makeEmptyAppId()]; + +const settings = definePluginSettings({ + replacedAppIds: { + type: OptionType.COMPONENT, + description: "", + component: () => { + const update = useForceUpdater(); + return ( + <> + DataStore.set(APP_IDS_KEY, appIds)} + /> + + ); + } + }, +}); + +migratePluginSettings("RPCEditor", "ReplaceActivityTypes"); +export default definePlugin({ + name: "RPCEditor", + description: "Edit the type and content of any Rich Presence", + authors: [Devs.Nyako, Devs.nin0dev], + patches: [ + { + find: '"LocalActivityStore"', + replacement: { + match: /\i\(\i\)\{.{0,25}activity:(\i)\}=\i;/, + replace: "$&$self.patchActivity($1);", + } + } + ], + settings, + settingsAboutComponent: () => , + + async start() { + appIds = await DataStore.get(APP_IDS_KEY) ?? [makeEmptyAppId()]; + }, + parseField(text: string, originalActivity: Activity): string { + if (text === "null") return ""; + return text + .replaceAll(":name:", originalActivity.name) + .replaceAll(":details:", originalActivity.details) + .replaceAll(":state:", originalActivity.state) + .replaceAll(":large_image:", originalActivity.assets.large_image) + .replaceAll(":large_text:", originalActivity.assets.large_text) + .replaceAll(":small_image:", originalActivity.assets.small_image) + .replaceAll(":small_text:", originalActivity.assets.small_text); + }, + patchActivity(activity: Activity) { + if (!activity) return; + appIds.forEach(app => { + if (app.enabled && app.appId === activity.application_id) { + const oldActivity = { ...activity }; + activity.type = app.newActivityType; + if (app.newName) activity.name = this.parseField(app.newName, oldActivity); + if (app.newActivityType === ActivityType.STREAMING && app.newStreamUrl) activity.url = app.newStreamUrl; + if (app.newDetails) activity.details = this.parseField(app.newDetails, oldActivity); + if (app.newState) activity.state = this.parseField(app.newState, oldActivity); + if (app.newLargeImageText) activity.assets.large_text = this.parseField(app.newLargeImageText, oldActivity); + if (app.newLargeImageUrl) activity.assets.large_image = this.parseField(app.newLargeImageUrl, oldActivity); + if (app.newSmallImageText) activity.assets.small_text = this.parseField(app.newSmallImageText, oldActivity); + if (app.newSmallImageUrl) activity.assets.small_image = this.parseField(app.newSmallImageUrl, oldActivity); + // @ts-ignore here we are intentionally nulling assets + if (app.disableAssets) activity.assets = {}; + if (app.disableTimestamps) activity.timestamps = {}; + } + }); + }, +}); diff --git a/src/equicordplugins/rpcEditor/style.css b/src/equicordplugins/rpcEditor/style.css new file mode 100644 index 00000000..8ceda789 --- /dev/null +++ b/src/equicordplugins/rpcEditor/style.css @@ -0,0 +1,14 @@ +.vc-rpceditor-horizontal { + display: flex; + justify-content: space-evenly; + +} + +.vc-rpceditor-horizontal div { + flex: 1; +} + + +.vc-rpceditor-horizontal div:first-child { + padding-right: 5px; +}