This commit is contained in:
nin0dev 2024-11-21 07:21:34 -05:00
parent 18045fa5f4
commit cf09f0add4
3 changed files with 392 additions and 393 deletions

View file

@ -1,224 +1,224 @@
/* /*
* Vencord, a Discord client mod * Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors * Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { CheckedTextInput } from "@components/CheckedTextInput"; import { CheckedTextInput } from "@components/CheckedTextInput";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { identity } from "@utils/misc"; import { identity } from "@utils/misc";
import { Card, Forms, PresenceStore, React, Select, SnowflakeUtils, Switch, TextInput, UserStore } from "@webpack/common"; import { Card, Forms, PresenceStore, React, Select, SnowflakeUtils, Switch, TextInput, UserStore } from "@webpack/common";
import { Activity, ActivityType, AppIdSetting, makeEmptyAppId } from "."; import { Activity, ActivityType, AppIdSetting, makeEmptyAppId } from ".";
interface SettingsProps { interface SettingsProps {
appIds: AppIdSetting[]; appIds: AppIdSetting[];
update: () => void; update: () => void;
save: () => void; save: () => void;
} }
function isValidSnowflake(v: string) { function isValidSnowflake(v: string) {
const regex = /^\d{17,20}$/; const regex = /^\d{17,20}$/;
return regex.test(v) && !Number.isNaN(SnowflakeUtils.extractTimestamp(v)); return regex.test(v) && !Number.isNaN(SnowflakeUtils.extractTimestamp(v));
} }
export function ReplaceTutorial() { export function ReplaceTutorial() {
const activities: Activity[] = PresenceStore.getActivities(UserStore.getCurrentUser().id); const activities: Activity[] = PresenceStore.getActivities(UserStore.getCurrentUser().id);
return ( return (
<> <>
<Forms.FormTitle tag="h3">IDs of currently running activities</Forms.FormTitle> <Forms.FormTitle tag="h3">IDs of currently running activities</Forms.FormTitle>
{ {
activities.length === 0 ? <Forms.FormText>No running activities</Forms.FormText> : activities.map(activity => { return activity.flags !== 48 ? <Forms.FormText>{activity.name}: {activity.application_id}</Forms.FormText> : null; /* hide spotify */ }) activities.length === 0 ? <Forms.FormText>No running activities</Forms.FormText> : activities.map(activity => { return activity.flags !== 48 ? <Forms.FormText>{activity.name}: {activity.application_id}</Forms.FormText> : null; /* hide spotify */ })
} }
<Forms.FormTitle tag="h3" className={Margins.top8}>Available variables</Forms.FormTitle> <Forms.FormTitle tag="h3" className={Margins.top8}>Available variables</Forms.FormTitle>
<Forms.FormText> <Forms.FormText>
In all fields (except stream URL), you can put in variables that'll automatically be replaced by their original content: In all fields (except stream URL), you can put in variables that'll automatically be replaced by their original content:
<pre style={{ fontFamily: "monospace" }}> <pre style={{ fontFamily: "monospace" }}>
:name:, :details:, :state: :name:, :details:, :state:
<br /> <br />
:large_image:, :large_text:, :small_image:, :small_text: :large_image:, :large_text:, :small_image:, :small_text:
</pre> </pre>
</Forms.FormText> </Forms.FormText>
<Forms.FormTitle tag="h3" className={Margins.top8}>More details</Forms.FormTitle> <Forms.FormTitle tag="h3" className={Margins.top8}>More details</Forms.FormTitle>
<Forms.FormText> <Forms.FormText>
Leave a field empty to leave it as is. Leave a field empty to leave it as is.
<br /> <br />
Set a field to "null" to hide it on the presence. Set a field to "null" to hide it on the presence.
<br /> <br />
You may need to reload Discord for changes to apply. You may need to reload Discord for changes to apply.
</Forms.FormText> </Forms.FormText>
</> </>
); );
} }
export function ReplaceSettings({ appIds, update, save }: SettingsProps) { export function ReplaceSettings({ appIds, update, save }: SettingsProps) {
async function onChange(val: string | boolean, index: number, key: string) { async function onChange(val: string | boolean, index: number, key: string) {
if (index === appIds.length - 1) if (index === appIds.length - 1)
appIds.push(makeEmptyAppId()); appIds.push(makeEmptyAppId());
appIds[index][key] = val; appIds[index][key] = val;
save(); save();
update(); update();
} }
return ( return (
<> <>
{ {
appIds.map((setting, i) => appIds.map((setting, i) =>
<Card style={{ padding: "1em", opacity: !setting.enabled ? "60%" : "" }}> <Card style={{ padding: "1em", opacity: !setting.enabled ? "60%" : "" }}>
{ {
isValidSnowflake(setting.appId) ? isValidSnowflake(setting.appId) ?
<Switch <Switch
value={setting.enabled} value={setting.enabled}
onChange={value => { onChange={value => {
onChange(value, i, "enabled"); onChange(value, i, "enabled");
}} }}
className={Margins.bottom8} className={Margins.bottom8}
hideBorder={true} hideBorder={true}
> >
Apply edits to app Apply edits to app
</Switch> : <Forms.FormTitle tag="h3">Add new application</Forms.FormTitle> </Switch> : <Forms.FormTitle tag="h3">Add new application</Forms.FormTitle>
} }
<Forms.FormTitle className={`${Margins.top8} ${Margins.bottom8}`}>Application ID</Forms.FormTitle> <Forms.FormTitle className={`${Margins.top8} ${Margins.bottom8}`}>Application ID</Forms.FormTitle>
<CheckedTextInput <CheckedTextInput
value={setting.appId} value={setting.appId}
onChange={async v => { onChange={async v => {
onChange(v, i, "appId"); onChange(v, i, "appId");
}} }}
validate={v => validate={v =>
!v || isValidSnowflake(v) || "Invalid application ID" !v || isValidSnowflake(v) || "Invalid application ID"
} }
/> />
{ {
isValidSnowflake(setting.appId) && <> isValidSnowflake(setting.appId) && <>
<Forms.FormTitle className={Margins.top8}>New activity type</Forms.FormTitle> <Forms.FormTitle className={Margins.top8}>New activity type</Forms.FormTitle>
<Select <Select
options={[ options={[
{ label: "Playing", value: ActivityType.PLAYING }, { label: "Playing", value: ActivityType.PLAYING },
{ label: "Watching", value: ActivityType.WATCHING }, { label: "Watching", value: ActivityType.WATCHING },
{ label: "Listening", value: ActivityType.LISTENING }, { label: "Listening", value: ActivityType.LISTENING },
{ label: "Competing", value: ActivityType.COMPETING }, { label: "Competing", value: ActivityType.COMPETING },
{ label: "Streaming", value: ActivityType.STREAMING } { label: "Streaming", value: ActivityType.STREAMING }
]} ]}
select={value => { select={value => {
onChange(value, i, "newActivityType"); onChange(value, i, "newActivityType");
}} }}
className={Margins.top8} className={Margins.top8}
isSelected={value => setting.newActivityType === value} isSelected={value => setting.newActivityType === value}
serialize={identity} serialize={identity}
/> />
{ {
setting.newActivityType === ActivityType.STREAMING && setting.newActivityType === ActivityType.STREAMING &&
<> <>
<Forms.FormTitle className={`${Margins.top8} ${Margins.bottom8}`}>Stream URL (must be YouTube or Twitch)</Forms.FormTitle> <Forms.FormTitle className={`${Margins.top8} ${Margins.bottom8}`}>Stream URL (must be YouTube or Twitch)</Forms.FormTitle>
<CheckedTextInput <CheckedTextInput
value={setting.newStreamUrl} value={setting.newStreamUrl}
onChange={async v => { onChange={async v => {
onChange(v, i, "newStreamUrl"); onChange(v, i, "newStreamUrl");
}} }}
validate={v => { validate={v => {
return /https?:\/\/(www\.)?(twitch\.tv|youtube\.com)\/\w+/.test(v) || "Invalid stream URL"; return /https?:\/\/(www\.)?(twitch\.tv|youtube\.com)\/\w+/.test(v) || "Invalid stream URL";
}} }}
/> />
</> </>
} }
{ {
setting.newActivityType !== ActivityType.STREAMING && setting.newActivityType !== ActivityType.STREAMING &&
<> <>
<Forms.FormTitle className={Margins.top8}>New name {setting.newActivityType === ActivityType.PLAYING && "(first line)"}</Forms.FormTitle> <Forms.FormTitle className={Margins.top8}>New name {setting.newActivityType === ActivityType.PLAYING && "(first line)"}</Forms.FormTitle>
<TextInput <TextInput
className={Margins.top8} className={Margins.top8}
value={setting.newName} value={setting.newName}
onChange={async v => { onChange={async v => {
onChange(v, i, "newName"); onChange(v, i, "newName");
}} }}
/> />
</> </>
} }
<Forms.FormTitle className={Margins.top8}>New details {setting.newActivityType === ActivityType.PLAYING ? "(second line)" : "(first line)"}</Forms.FormTitle> <Forms.FormTitle className={Margins.top8}>New details {setting.newActivityType === ActivityType.PLAYING ? "(second line)" : "(first line)"}</Forms.FormTitle>
<TextInput <TextInput
className={Margins.top8} className={Margins.top8}
value={setting.newDetails} value={setting.newDetails}
onChange={async v => { onChange={async v => {
onChange(v, i, "newDetails"); onChange(v, i, "newDetails");
}} }}
/> />
<Forms.FormTitle className={Margins.top8}>New state {setting.newActivityType === ActivityType.PLAYING ? "(third line)" : "(second line)"}</Forms.FormTitle> <Forms.FormTitle className={Margins.top8}>New state {setting.newActivityType === ActivityType.PLAYING ? "(third line)" : "(second line)"}</Forms.FormTitle>
<TextInput <TextInput
className={Margins.top8} className={Margins.top8}
value={setting.newState} value={setting.newState}
onChange={async v => { onChange={async v => {
onChange(v, i, "newState"); onChange(v, i, "newState");
}} }}
/> />
{ {
!setting.disableAssets && !setting.disableAssets &&
<> <>
<Forms.FormText style={{ fontSize: "1.05rem", fontWeight: "500" }} className={Margins.top8}>Large image</Forms.FormText> <Forms.FormText style={{ fontSize: "1.05rem", fontWeight: "500" }} className={Margins.top8}>Large image</Forms.FormText>
<Forms.FormTitle className={Margins.top8}>Text {setting.newActivityType !== ActivityType.PLAYING && "(also third line)"}</Forms.FormTitle> <Forms.FormTitle className={Margins.top8}>Text {setting.newActivityType !== ActivityType.PLAYING && "(also third line)"}</Forms.FormTitle>
<TextInput <TextInput
className={Margins.top8} className={Margins.top8}
value={setting.newLargeImageText} value={setting.newLargeImageText}
onChange={async v => { onChange={async v => {
onChange(v, i, "newLargeImageText"); onChange(v, i, "newLargeImageText");
}} }}
/> />
<Forms.FormTitle className={Margins.top8}>URL</Forms.FormTitle> <Forms.FormTitle className={Margins.top8}>URL</Forms.FormTitle>
<TextInput <TextInput
className={Margins.top8} className={Margins.top8}
value={setting.newLargeImageUrl} value={setting.newLargeImageUrl}
onChange={async v => { onChange={async v => {
onChange(v, i, "newLargeImageUrl"); onChange(v, i, "newLargeImageUrl");
}} }}
/> />
<Forms.FormText style={{ fontSize: "1.05rem", fontWeight: "500" }} className={Margins.top8}>Small image</Forms.FormText> <Forms.FormText style={{ fontSize: "1.05rem", fontWeight: "500" }} className={Margins.top8}>Small image</Forms.FormText>
<Forms.FormTitle className={Margins.top8}>Text</Forms.FormTitle> <Forms.FormTitle className={Margins.top8}>Text</Forms.FormTitle>
<TextInput <TextInput
className={Margins.top8} className={Margins.top8}
value={setting.newSmallImageText} value={setting.newSmallImageText}
onChange={async v => { onChange={async v => {
onChange(v, i, "newSmallImageText"); onChange(v, i, "newSmallImageText");
}} }}
/> />
<Forms.FormTitle className={Margins.top8}>URL</Forms.FormTitle> <Forms.FormTitle className={Margins.top8}>URL</Forms.FormTitle>
<TextInput <TextInput
className={Margins.top8} className={Margins.top8}
value={setting.newSmallImageUrl} value={setting.newSmallImageUrl}
onChange={async v => { onChange={async v => {
onChange(v, i, "newSmallImageUrl"); onChange(v, i, "newSmallImageUrl");
}} }}
/> />
</> </>
} }
<Switch <Switch
value={setting.disableAssets} value={setting.disableAssets}
onChange={value => { onChange={value => {
onChange(value, i, "disableAssets"); onChange(value, i, "disableAssets");
}} }}
className={Margins.top8} className={Margins.top8}
hideBorder={true} hideBorder={true}
style={{ marginBottom: "0" }} style={{ marginBottom: "0" }}
> >
Hide assets (large & small images) Hide assets (large & small images)
</Switch> </Switch>
<Switch <Switch
value={setting.disableTimestamps} value={setting.disableTimestamps}
onChange={value => { onChange={value => {
onChange(value, i, "disableTimestamps"); onChange(value, i, "disableTimestamps");
}} }}
className={Margins.top8} className={Margins.top8}
hideBorder={true} hideBorder={true}
style={{ marginBottom: "0" }} style={{ marginBottom: "0" }}
> >
Hide timestamps Hide timestamps
</Switch> </Switch>
</> </>
} }
</Card> </Card>
) )
} }
</> </>
); );
} }

309
index.tsx
View file

@ -1,155 +1,154 @@
/* /*
* Vencord, a Discord client mod * Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors * Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { DataStore } from "@api/index"; import { DataStore } from "@api/index";
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { useForceUpdater } from "@utils/react"; import { useForceUpdater } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { React } from "@webpack/common"; import { React } from "@webpack/common";
import { ReplaceSettings, ReplaceTutorial } from "./ReplaceSettings"; import { ReplaceSettings, ReplaceTutorial } from "./ReplaceSettings";
import { parse } from "path";
const APP_IDS_KEY = "ReplaceActivityType_appids";
const APP_IDS_KEY = "ReplaceActivityType_appids"; export type AppIdSetting = {
export type AppIdSetting = { disableAssets: boolean;
disableAssets: boolean; disableTimestamps: boolean;
disableTimestamps: boolean; appId: string;
appId: string; enabled: boolean;
enabled: boolean; newActivityType: ActivityType;
newActivityType: ActivityType; newName: string,
newName: string, newDetails: string,
newDetails: string, newState: string,
newState: string, newLargeImageUrl: string,
newLargeImageUrl: string, newLargeImageText: string,
newLargeImageText: string, newSmallImageUrl: string,
newSmallImageUrl: string, newSmallImageText: string;
newSmallImageText: string; newStreamUrl: string;
newStreamUrl: string; };
};
export interface Activity {
export interface Activity { state: string;
state: string; details: string;
details: string; timestamps?: {
timestamps?: { start?: number;
start?: number; end?: number;
end?: number; };
}; url?: string;
url?: string; assets: ActivityAssets;
assets: ActivityAssets; buttons?: Array<string>;
buttons?: Array<string>; name: string;
name: string; application_id: string;
application_id: string; metadata?: {
metadata?: { button_urls?: Array<string>;
button_urls?: Array<string>; };
}; type: number;
type: number; flags: number;
flags: number; }
}
interface ActivityAssets {
interface ActivityAssets { large_image: string;
large_image: string; large_text: string;
large_text: string; small_image: string;
small_image: string; small_text: string;
small_text: string; }
}
export const enum ActivityType {
export const enum ActivityType { PLAYING = 0,
PLAYING = 0, STREAMING = 1,
STREAMING = 1, LISTENING = 2,
LISTENING = 2, WATCHING = 3,
WATCHING = 3, COMPETING = 5
COMPETING = 5 }
}
export const makeEmptyAppId: () => AppIdSetting = () => ({
export const makeEmptyAppId: () => AppIdSetting = () => ({ appId: "",
appId: "", enabled: true,
enabled: true, newActivityType: ActivityType.PLAYING,
newActivityType: ActivityType.PLAYING, newName: "",
newName: "", newDetails: "",
newDetails: "", newState: "",
newState: "", newLargeImageUrl: "",
newLargeImageUrl: "", newLargeImageText: "",
newLargeImageText: "", newSmallImageUrl: "",
newSmallImageUrl: "", newSmallImageText: "",
newSmallImageText: "", newStreamUrl: "",
newStreamUrl: "", disableTimestamps: false,
disableTimestamps: false, disableAssets: false
disableAssets: false });
});
let appIds = [makeEmptyAppId()];
let appIds = [makeEmptyAppId()];
const settings = definePluginSettings({
const settings = definePluginSettings({ replacedAppIds: {
replacedAppIds: { type: OptionType.COMPONENT,
type: OptionType.COMPONENT, description: "",
description: "", component: () => {
component: () => { const update = useForceUpdater();
const update = useForceUpdater(); return (
return ( <>
<> <ReplaceSettings
<ReplaceSettings appIds={appIds}
appIds={appIds} update={update}
update={update} save={async () => DataStore.set(APP_IDS_KEY, appIds)}
save={async () => DataStore.set(APP_IDS_KEY, appIds)} />
/> </>
</> );
); }
} },
}, });
});
export default definePlugin({
export default definePlugin({ name: "RPCEditor",
name: "RPCEditor", description: "Edit the type and content of any Rich Presence",
description: "Edit the type and content of any Rich Presence", authors: [Devs.Nyako, Devs.nin0dev],
authors: [Devs.Nyako, Devs.nin0dev], patches: [
patches: [ {
{ find: '"LocalActivityStore"',
find: '"LocalActivityStore"', replacement: {
replacement: { match: /\i\((\i)\)\{.{0,50}activity.{0,10}=\i;/,
match: /\i\((\i)\)\{.{0,50}activity.{0,10}=\i;/, replace: "$&$self.patchActivity($1.activity);",
replace: "$&$self.patchActivity($1.activity);", }
} }
} ],
], settings,
settings, settingsAboutComponent: () => <ReplaceTutorial />,
settingsAboutComponent: () => <ReplaceTutorial />,
async start() {
async start() { appIds = await DataStore.get(APP_IDS_KEY) ?? [makeEmptyAppId()];
appIds = await DataStore.get(APP_IDS_KEY) ?? [makeEmptyAppId()]; },
}, parseField(text: string, originalActivity: Activity): string {
parseField(text: string, originalActivity: Activity): string { if (text === "null") return "";
if (text === "null") return ""; return text
return text .replaceAll(":name:", originalActivity.name)
.replaceAll(":name:", originalActivity.name) .replaceAll(":details:", originalActivity.details)
.replaceAll(":details:", originalActivity.details) .replaceAll(":state:", originalActivity.state)
.replaceAll(":state:", originalActivity.state) .replaceAll(":large_image:", originalActivity.assets.large_image)
.replaceAll(":large_image:", originalActivity.assets.large_image) .replaceAll(":large_text:", originalActivity.assets.large_text)
.replaceAll(":large_text:", originalActivity.assets.large_text) .replaceAll(":small_image:", originalActivity.assets.small_image)
.replaceAll(":small_image:", originalActivity.assets.small_image) .replaceAll(":small_text:", originalActivity.assets.small_text);
.replaceAll(":small_text:", originalActivity.assets.small_text); },
}, patchActivity(activity: Activity) {
patchActivity(activity: Activity) { if (!activity) return;
if (!activity) return; appIds.forEach(app => {
appIds.forEach(app => { if (app.enabled && app.appId === activity.application_id) {
if (app.enabled && app.appId === activity.application_id) { const oldActivity = { ...activity };
const oldActivity = { ...activity }; activity.type = app.newActivityType;
activity.type = app.newActivityType; if (app.newName) activity.name = this.parseField(app.newName, oldActivity);
if (app.newName) activity.name = this.parseField(app.newName, oldActivity); if (app.newActivityType === ActivityType.STREAMING && app.newStreamUrl) activity.url = app.newStreamUrl;
if (app.newActivityType === ActivityType.STREAMING && app.newStreamUrl) activity.url = app.newStreamUrl; if (app.newDetails) activity.details = this.parseField(app.newDetails, oldActivity);
if (app.newDetails) activity.details = this.parseField(app.newDetails, oldActivity); if (app.newState) activity.state = this.parseField(app.newState, 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.newLargeImageText) activity.assets.large_text = this.parseField(app.newLargeImageText, oldActivity); if (app.newLargeImageUrl) activity.assets.large_image = this.parseField(app.newLargeImageUrl, 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.newSmallImageText) activity.assets.small_text = this.parseField(app.newSmallImageText, oldActivity); if (app.newSmallImageUrl) activity.assets.small_image = this.parseField(app.newSmallImageUrl, oldActivity);
if (app.newSmallImageUrl) activity.assets.small_image = this.parseField(app.newSmallImageUrl, oldActivity); // @ts-ignore here we are intentionally nulling assets
// @ts-ignore here we are intentionally nulling assets if (app.disableAssets) activity.assets = {};
if (app.disableAssets) activity.assets = {}; if (app.disableTimestamps) activity.timestamps = {};
if (app.disableTimestamps) activity.timestamps = {}; }
} });
}); },
}, });
});

View file

@ -1,14 +1,14 @@
.vc-rpceditor-horizontal { .vc-rpceditor-horizontal {
display: flex; display: flex;
justify-content: space-evenly; justify-content: space-evenly;
} }
.vc-rpceditor-horizontal div { .vc-rpceditor-horizontal div {
flex: 1; flex: 1;
} }
.vc-rpceditor-horizontal div:first-child { .vc-rpceditor-horizontal div:first-child {
padding-right: 5px; padding-right: 5px;
} }