Fix timestamp
This commit is contained in:
parent
e70872d84d
commit
e213c562a5
22 changed files with 791 additions and 0 deletions
6
plugins/BetterSpotifyIntegration/manifest.json
Normal file
6
plugins/BetterSpotifyIntegration/manifest.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "BetterSpotifyIntegration",
|
||||
"description": "Shrimply better integration with Spotify",
|
||||
"vendetta": { },
|
||||
"main": "src/index.tsx"
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import { findByProps, findByStoreName } from "@vendetta/metro";
|
||||
import { getAssetIDByName } from "@vendetta/ui/assets";
|
||||
import { Image, View } from "react-native";
|
||||
|
||||
const FluxUtils = findByProps("useStateFromStores");
|
||||
const SpotifyStore = findByStoreName("SpotifyStore");
|
||||
|
||||
const Button = findByProps("Sizes", "Looks") as any;
|
||||
const SpotifySyncUtil = findByProps("play", "sync");
|
||||
|
||||
type ListenAlongButtonProps = Partial<{
|
||||
activity: any;
|
||||
user: any;
|
||||
isCurrentUser: boolean;
|
||||
activityButtonColor: string;
|
||||
[key: string]: any;
|
||||
}>
|
||||
|
||||
export default function ListenAlongButton(props: ListenAlongButtonProps) {
|
||||
const currentPartyId = FluxUtils.useStateFromStores([SpotifyStore], () => SpotifyStore.getActivity()?.party?.id);
|
||||
const alreadySync = props.isCurrentUser || props.activity?.party?.id === currentPartyId;
|
||||
|
||||
return <View pointerEvents={alreadySync ? "none" : "auto"}>
|
||||
<Button
|
||||
text={!alreadySync ? "Listen Along" : "You are already along for this ride"}
|
||||
size={Button.Sizes.SMALL}
|
||||
style={{
|
||||
marginTop: 12,
|
||||
backgroundColor: props.activityButtonColor,
|
||||
opacity: alreadySync ? 0.5 : 1
|
||||
}}
|
||||
renderIcon={() => <Image
|
||||
style={{ height: 16, width: 16, marginRight: 8, tintColor: "white" }}
|
||||
source={getAssetIDByName("ic_headphone")}
|
||||
/>}
|
||||
onPress={() => void SpotifySyncUtil.sync(props.activity, props.user.id)}
|
||||
/>
|
||||
</View>;
|
||||
}
|
||||
33
plugins/BetterSpotifyIntegration/src/index.tsx
Normal file
33
plugins/BetterSpotifyIntegration/src/index.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { findByName } from "@vendetta/metro";
|
||||
import { i18n } from "@vendetta/metro/common";
|
||||
import { before } from "@vendetta/patcher";
|
||||
import { findInReactTree } from "@vendetta/utils";
|
||||
|
||||
import ListenAlongButton from "./components/ListenAlongButton";
|
||||
|
||||
let sectionPatch: () => void;
|
||||
|
||||
export default {
|
||||
onLoad() {
|
||||
const getListeningHeader = () => i18n.Messages.USER_ACTIVITY_HEADER_LISTENING?.intlMessage?.format({ name: "Spotify" });
|
||||
|
||||
sectionPatch = before("default", findByName("UserProfileSection", false), ([props]) => {
|
||||
if (props.title !== getListeningHeader()) return;
|
||||
|
||||
const spotifyElement = findInReactTree(props.children, e => e?.style?.padding === 0);
|
||||
const actionsIndex = spotifyElement?.children?.findIndex?.(e => e?.type?.name === "Actions");
|
||||
|
||||
if (actionsIndex && actionsIndex !== -1) {
|
||||
const actions = spotifyElement.children[actionsIndex];
|
||||
|
||||
spotifyElement.children[actionsIndex] = <>
|
||||
{actions}
|
||||
{<ListenAlongButton {...actions.props} />}
|
||||
</>;
|
||||
}
|
||||
});
|
||||
},
|
||||
onUnload() {
|
||||
sectionPatch?.();
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { findByProps, findByStoreName } from "@vendetta/metro";
|
||||
|
||||
const FluxUtils = findByProps("useStateFromStores");
|
||||
const SpotifyStore = findByStoreName("SpotifyStore");
|
||||
const GamePartyStore = findByStoreName("GamePartyStore");
|
||||
const UserStore = findByStoreName("UserStore");
|
||||
|
||||
export default function useListeningParty() {
|
||||
const partySet = FluxUtils.useStateFromStores([GamePartyStore, SpotifyStore], () => {
|
||||
const party = GamePartyStore.getParty(SpotifyStore.getActivity()?.party?.id);
|
||||
return party?.size > 1 ? party : null;
|
||||
});
|
||||
|
||||
return partySet ? [...partySet].map(id => UserStore.getUser(id)) : [];
|
||||
}
|
||||
7
plugins/HideGiftButton/manifest.json
Normal file
7
plugins/HideGiftButton/manifest.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "HideGiftButton",
|
||||
"description": "Hides the gift button in the chat input.",
|
||||
"vendetta": {
|
||||
"icon": "ic_gift_24px"
|
||||
}
|
||||
}
|
||||
19
plugins/HideGiftButton/src/index.ts
Normal file
19
plugins/HideGiftButton/src/index.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { findByName } from "@vendetta/metro";
|
||||
import { after } from "@vendetta/patcher";
|
||||
import { getAssetIDByName } from "@vendetta/ui/assets";
|
||||
import { findInReactTree } from "@vendetta/utils";
|
||||
|
||||
const ChatInput = findByName("ChatInput");
|
||||
|
||||
let unpatch: () => boolean;
|
||||
|
||||
export default {
|
||||
onLoad() {
|
||||
const blockList = ["ic_thread_normal_24px", "ic_gift"].map(n => getAssetIDByName(n));
|
||||
unpatch = after("render", ChatInput.prototype, (_, ret) => {
|
||||
const input = findInReactTree(ret, t => "forceAnimateButtons" in t.props && t.props.actions);
|
||||
input.props.actions = input.props.actions.filter(a => !blockList.includes(a.source));
|
||||
});
|
||||
},
|
||||
onUnload: unpatch
|
||||
};
|
||||
7
plugins/HoldGIFSend/manifest.json
Normal file
7
plugins/HoldGIFSend/manifest.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "HoldGIFSend",
|
||||
"description": "Hold GIF links to chat input before sending them.",
|
||||
"vendetta": {
|
||||
"icon": "gif"
|
||||
}
|
||||
}
|
||||
13
plugins/HoldGIFSend/src/index.ts
Normal file
13
plugins/HoldGIFSend/src/index.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { findByProps } from "@vendetta/metro";
|
||||
import { instead } from "@vendetta/patcher";
|
||||
|
||||
const messageInputUtil = findByProps("selectGIF");
|
||||
|
||||
export const onUnload = instead("selectGIF", messageInputUtil, (args) => {
|
||||
// args[1] never exists on older version, so we use it as an indicator
|
||||
// prev: selectGIF([GIF]) now: selectGIF(currentChannelId, [GIF]);
|
||||
if (args[1]) args[1] = args[1].url;
|
||||
else args[0] = args[0].url;
|
||||
|
||||
messageInputUtil.insertText(...args);
|
||||
});
|
||||
7
plugins/MoreConfirm/manifest.json
Normal file
7
plugins/MoreConfirm/manifest.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "MoreConfirm",
|
||||
"description": "Prompts confirmations before making irreversible actions.",
|
||||
"vendetta": {
|
||||
"icon": "check"
|
||||
}
|
||||
}
|
||||
88
plugins/MoreConfirm/src/index.ts
Normal file
88
plugins/MoreConfirm/src/index.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
/*!
|
||||
* https://github.com/amsyarasyiq/aliucordrn-plugins/blob/6c0be39ca673c6ebcf48c0f397246b76d578aa26/MoarConfirm/index.tsx
|
||||
*
|
||||
* Copyright (c) 2022 Amsyar Rasyiq
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { logger, patcher } from "@vendetta";
|
||||
import { findByProps, findByStoreName } from "@vendetta/metro";
|
||||
|
||||
const dialog = findByProps("show", "confirm", "close");
|
||||
const relationshipManager = findByProps("addRelationship");
|
||||
const callManager = findByProps("handleStartCall");
|
||||
const actionSheetManager = findByProps("hideActionSheet");
|
||||
const UserStore = findByStoreName("UserStore");
|
||||
|
||||
export default new class MoreConfirm {
|
||||
patches = [] as (() => void)[];
|
||||
|
||||
onLoad() {
|
||||
this.patches.push(patcher.instead("handleStartCall", callManager, (args, orig) => {
|
||||
const [{ rawRecipients: [{ username, discriminator }, multiple] }, isVideo] = args;
|
||||
const action = isVideo ? "video call" : "call";
|
||||
|
||||
// if `multiple` is defined, it's probably a group call
|
||||
dialog.show({
|
||||
title: multiple ? `Start a group ${action}?` : `Start a ${action} with ${username}#${discriminator}?`,
|
||||
body: multiple ? "Are you sure you want to start the group call?" : `Are you sure you want to ${action} ${username}#${discriminator}?`,
|
||||
confirmText: "Yes",
|
||||
cancelText: "Cancel",
|
||||
confirmColor: "brand",
|
||||
onConfirm: () => {
|
||||
try {
|
||||
orig(...args);
|
||||
} catch (e) {
|
||||
logger.error("Failed to start call", e);
|
||||
}
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
this.patches.push(patcher.instead("addRelationship", relationshipManager, (args, orig) => {
|
||||
if (typeof args[0] !== "object" || !args[0].userId) return orig.apply(this, args);
|
||||
const { username, discriminator } = UserStore.getUser(args[0].userId);
|
||||
|
||||
// This is hacky, but it *works*
|
||||
const hideASInterval = setInterval(() => actionSheetManager.hideActionSheet(), 100);
|
||||
setTimeout(() => clearInterval(hideASInterval), 3000);
|
||||
|
||||
const block = args[0].type === 2;
|
||||
return new Promise(r => {
|
||||
dialog.show({
|
||||
title: `${block ? "Block" : "Friend"} ${username}#${discriminator}?`,
|
||||
body: `Are you sure you want to ${block ? "block" : "friend"} ${username}#${discriminator}?`,
|
||||
confirmText: "Yes",
|
||||
cancelText: "Cancel",
|
||||
confirmColor: "brand",
|
||||
onConfirm: () => {
|
||||
try {
|
||||
r(orig.apply(this, args));
|
||||
} catch (e) {
|
||||
logger.error("Failed to add relationship", e);
|
||||
} finally {
|
||||
clearInterval(hideASInterval);
|
||||
}
|
||||
},
|
||||
onCancel: () => void clearInterval(hideASInterval)
|
||||
});
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
onUnload() {
|
||||
for (const unpatch of this.patches) unpatch();
|
||||
}
|
||||
};
|
||||
7
plugins/NoAutoReplyMention/manifest.json
Normal file
7
plugins/NoAutoReplyMention/manifest.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "NoAutoReplyMention",
|
||||
"description": "Turns off auto mention when replying to someone.",
|
||||
"vendetta": {
|
||||
"icon": "ic_notif_off"
|
||||
}
|
||||
}
|
||||
110
plugins/NoAutoReplyMention/src/Settings.tsx
Normal file
110
plugins/NoAutoReplyMention/src/Settings.tsx
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import { findByStoreName } from "@vendetta/metro";
|
||||
import { NavigationNative, React, stylesheet } from "@vendetta/metro/common";
|
||||
import { useProxy } from "@vendetta/storage";
|
||||
import { getAssetIDByName } from "@vendetta/ui/assets";
|
||||
import { Forms } from "@vendetta/ui/components";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FlatList, Image, ScrollView, TouchableOpacity } from "react-native";
|
||||
import { SettingsSchema, settings } from ".";
|
||||
|
||||
const UserStore = findByStoreName("UserStore");
|
||||
|
||||
const { FormInput, FormDivider, FormText, FormIcon, FormRow, FormSection, FormSwitchRow } = Forms;
|
||||
|
||||
const styles = stylesheet.createThemedStyleSheet({
|
||||
avatar: {
|
||||
height: 36,
|
||||
width: 36,
|
||||
borderRadius: 18,
|
||||
},
|
||||
button: {
|
||||
marginRight: 12,
|
||||
}
|
||||
});
|
||||
|
||||
function AddButton({ callback }: { callback: () => void }) {
|
||||
return <TouchableOpacity onPress={callback}>
|
||||
<FormText style={styles.button}>ADD</FormText>
|
||||
</TouchableOpacity>;
|
||||
}
|
||||
|
||||
function AddRow({ onFinish }: { onFinish: () => void }) {
|
||||
const [value, setValue] = useState("");
|
||||
|
||||
const onPressCallback = () => {
|
||||
if (!isNaN(parseInt(value)) && UserStore.getUser(value) && !settings.exempted.includes(value)) {
|
||||
settings.exempted = [...settings.exempted, value];
|
||||
setValue("");
|
||||
}
|
||||
onFinish();
|
||||
};
|
||||
|
||||
return (
|
||||
<FormRow
|
||||
leading={<FormIcon source={getAssetIDByName("ic_add_friend")} />}
|
||||
label={<FormInput autoFocus
|
||||
title="User ID"
|
||||
placeholder="123456789012345678"
|
||||
value={value}
|
||||
keyboardType="numeric"
|
||||
onChangeText={(value: string) => setValue(value.replace(/[^0-9]/g, "").trim())}
|
||||
returnKeyType="done"
|
||||
/>}
|
||||
trailing={(
|
||||
<TouchableOpacity onPress={onPressCallback}>
|
||||
<FormIcon source={getAssetIDByName("ic_add_24px")} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Settings() {
|
||||
useProxy(settings) as SettingsSchema;
|
||||
|
||||
const [shouldShowAdd, setShouldShowAdd] = useState(false);
|
||||
const navigation = NavigationNative.useNavigation();
|
||||
|
||||
useEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerRight: () => <AddButton callback={() => setShouldShowAdd(true)} />
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ScrollView>
|
||||
<FormSwitchRow
|
||||
label="Enable blacklist mode"
|
||||
subLabel="If enabled, only users in the exempted list will be affected by the plugin."
|
||||
leading={<FormIcon source={getAssetIDByName("ic_block")} />}
|
||||
value={settings.isBlacklistMode}
|
||||
onValueChange={(value: boolean) => settings.isBlacklistMode = value}
|
||||
/>
|
||||
<FormSection title="Exempted Users">
|
||||
{settings.exempted?.length > 0 && <FlatList
|
||||
data={settings.exempted}
|
||||
keyExtractor={(item) => item}
|
||||
renderItem={({ item }) => {
|
||||
const user = UserStore.getUser(item);
|
||||
if (!user) throw new Error("User not found/is not cached");
|
||||
|
||||
return (<FormRow
|
||||
label={user.username}
|
||||
leading={<Image style={styles.avatar} source={{ uri: user.getAvatarURL() }} />}
|
||||
trailing={<TouchableOpacity onPress={() => {
|
||||
settings.exempted = settings.exempted.filter((id) => id !== item);
|
||||
}}>
|
||||
<FormIcon source={getAssetIDByName("Small")} disableColor />
|
||||
</TouchableOpacity>}
|
||||
/>);
|
||||
}}
|
||||
ItemSeparatorComponent={FormDivider}
|
||||
/>}
|
||||
{shouldShowAdd && <>
|
||||
<FormDivider />
|
||||
<AddRow onFinish={() => setShouldShowAdd(false)} />
|
||||
</>}
|
||||
</FormSection>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
26
plugins/NoAutoReplyMention/src/index.ts
Normal file
26
plugins/NoAutoReplyMention/src/index.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// export const onUnload = globalThis.vendetta.patcher.before("createPendingReply", globalThis.vendetta.metro.findByProps("createPendingReply"), ([arg]) => arg.shouldMention &&= false);
|
||||
|
||||
import { patcher } from "@vendetta";
|
||||
import { findByProps } from "@vendetta/metro";
|
||||
import { storage } from "@vendetta/plugin";
|
||||
import Settings from "./Settings";
|
||||
|
||||
export interface SettingsSchema {
|
||||
isBlacklistMode: boolean;
|
||||
exempted: string[];
|
||||
}
|
||||
|
||||
storage.isBlacklistMode ??= false;
|
||||
storage.exempted ??= [];
|
||||
|
||||
export const settings = storage as SettingsSchema;
|
||||
|
||||
const ReplyManager = findByProps("createPendingReply");
|
||||
|
||||
export default {
|
||||
settings: Settings,
|
||||
onUnload: patcher.before("createPendingReply", ReplyManager, ([arg]) => {
|
||||
const isListed = storage.exempted.includes(arg.message?.author?.id);
|
||||
arg.shouldMention &&= storage.isBlacklistMode ? !isListed : isListed;
|
||||
})
|
||||
};
|
||||
7
plugins/NoIdle/manifest.json
Normal file
7
plugins/NoIdle/manifest.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "NoIdle",
|
||||
"description": "Prevent status from idling when Discord is in the background.",
|
||||
"vendetta": {
|
||||
"icon": "StatusIdle"
|
||||
}
|
||||
}
|
||||
8
plugins/NoIdle/src/index.ts
Normal file
8
plugins/NoIdle/src/index.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { patcher } from "@vendetta";
|
||||
import { FluxDispatcher } from "@vendetta/metro/common";
|
||||
|
||||
export const onUnload = patcher.before("dispatch", FluxDispatcher, ([{ type }]) => {
|
||||
if (type !== "IDLE") return;
|
||||
|
||||
return [{ type: "IDLE", idle: false }];
|
||||
});
|
||||
4
plugins/PyonInstaller/manifest.json
Normal file
4
plugins/PyonInstaller/manifest.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "PyoncordInstaller",
|
||||
"description": "installs pyoncord.exe (virus)"
|
||||
}
|
||||
28
plugins/PyonInstaller/src/index.ts
Normal file
28
plugins/PyonInstaller/src/index.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { plugin, plugins } from "@vendetta";
|
||||
import { config } from "@vendetta/loader";
|
||||
import { showConfirmationAlert } from "@vendetta/ui/alerts";
|
||||
|
||||
export default {
|
||||
onLoad() {
|
||||
plugins.removePlugin(plugin.id);
|
||||
|
||||
setTimeout(() => showConfirmationAlert({
|
||||
title: "Install Pyoncord?",
|
||||
content: "Loader's settings will be overriden to load Pyoncord instead. You may not be able to revert to Vendetta whenever Pyoncord breaks!",
|
||||
confirmText: "Continue and Restart",
|
||||
cancelText: "Nevermind",
|
||||
onConfirm: this.install
|
||||
}), 300);
|
||||
},
|
||||
|
||||
async install() {
|
||||
if (config.customLoadUrl) {
|
||||
config.customLoadUrl.enabled = true;
|
||||
config.customLoadUrl.url = "https://raw.githubusercontent.com/pyoncord/pyoncord/builds/pyoncord.js";
|
||||
|
||||
setTimeout(globalThis.nativeModuleProxy.BundleUpdaterManager.reload, 100);
|
||||
} else {
|
||||
alert("not a proper vendetta loader!");
|
||||
}
|
||||
}
|
||||
};
|
||||
7
plugins/RichPresence/manifest.json
Normal file
7
plugins/RichPresence/manifest.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "RichPresence",
|
||||
"description": "Sets Discord Rich Presence for your profile.",
|
||||
"vendetta": {
|
||||
"icon": "games"
|
||||
}
|
||||
}
|
||||
174
plugins/RichPresence/src/ConfigEditor.tsx
Normal file
174
plugins/RichPresence/src/ConfigEditor.tsx
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
|
||||
import { findByProps, findByStoreName } from "@vendetta/metro";
|
||||
import { React, stylesheet } from "@vendetta/metro/common";
|
||||
import { storage } from "@vendetta/plugin";
|
||||
import { useProxy } from "@vendetta/storage";
|
||||
import { semanticColors } from "@vendetta/ui";
|
||||
import { Button, Forms } from "@vendetta/ui/components";
|
||||
|
||||
import { ScrollView } from "react-native";
|
||||
|
||||
const { FormSection, FormInput, FormRow, FormSwitch, FormText } = Forms;
|
||||
|
||||
const UserStore = findByStoreName("UserStore");
|
||||
const profiles = findByProps("showUserProfile");
|
||||
|
||||
const styles = stylesheet.createThemedStyleSheet({
|
||||
subText: {
|
||||
fontSize: 14,
|
||||
marginLeft: 16,
|
||||
marginRight: 16,
|
||||
color: semanticColors.TEXT_MUTED
|
||||
},
|
||||
textLink: {
|
||||
color: semanticColors.TEXT_LINK,
|
||||
}
|
||||
});
|
||||
|
||||
export default function ConfigEditor({ selection }: { selection: string }) {
|
||||
const settings = useProxy(storage.selections[selection]) as Activity;
|
||||
|
||||
return (
|
||||
<ScrollView style={{ flex: 1 }} contentContainerStyle={{ paddingBottom: 38 }}>
|
||||
<Button
|
||||
style={{ margin: 16 }}
|
||||
color={"brand"}
|
||||
size={Button.Sizes.MEDIUM}
|
||||
look={Button.Looks.FILLED}
|
||||
onPress={async () => {
|
||||
profiles.showUserProfile({ userId: UserStore.getCurrentUser().id });
|
||||
}}
|
||||
text="Preview your profile"
|
||||
/>
|
||||
<FormSection title="Basic" titleStyleType="no_border">
|
||||
<FormInput required autoFocus
|
||||
title="Application Name"
|
||||
value={settings.name}
|
||||
placeholder="Discord"
|
||||
onChange={v => settings.name = v}
|
||||
/>
|
||||
<FormInput required
|
||||
title="Application ID"
|
||||
value={settings.application_id}
|
||||
placeholder="1054951789318909972"
|
||||
onChange={v => settings.application_id = v}
|
||||
keyboardType="numeric"
|
||||
/>
|
||||
<FormInput
|
||||
title="Type"
|
||||
value={settings.type}
|
||||
placeholder="0"
|
||||
onChange={v => settings.type = v}
|
||||
keyboardType="numeric"
|
||||
/>
|
||||
<FormInput
|
||||
title="Details"
|
||||
value={settings.details}
|
||||
placeholder="Competitive"
|
||||
onChange={v => settings.details = v}
|
||||
/>
|
||||
<FormInput
|
||||
title="State"
|
||||
value={settings.state}
|
||||
placeholder="Playing Solo"
|
||||
onChange={v => settings.state = v}
|
||||
/>
|
||||
</FormSection>
|
||||
<FormSection title="Images">
|
||||
<FormInput
|
||||
title="Large Image Asset Key or URL"
|
||||
value={settings.assets.large_image}
|
||||
placeholder="large_image_here"
|
||||
onChange={v => settings.assets.large_image = v}
|
||||
/>
|
||||
<FormInput
|
||||
title="Large Image Text"
|
||||
value={settings.assets.large_text}
|
||||
placeholder="Playing on Joe's lobby"
|
||||
disabled={!settings.assets.large_image}
|
||||
onChange={v => settings.assets.large_text = v}
|
||||
/>
|
||||
<FormInput
|
||||
title="Small Image Asset Key or URL"
|
||||
value={settings.assets.small_image}
|
||||
placeholder="small_image_here"
|
||||
onChange={v => settings.assets.small_image = v}
|
||||
/>
|
||||
<FormInput
|
||||
title="Small Image Text"
|
||||
value={settings.assets.small_text}
|
||||
placeholder="Solo"
|
||||
disabled={!settings.assets.small_image}
|
||||
onChange={v => settings.assets.small_text = v}
|
||||
/>
|
||||
</FormSection>
|
||||
<FormText style={styles.subText}>
|
||||
{"Image assets key can be either a Discord app asset's name or a URL to an image."}
|
||||
</FormText>
|
||||
<FormSection title="Timestamps">
|
||||
<FormRow
|
||||
label="Enable timestamps"
|
||||
subLabel="Set whether to show timestamps or not"
|
||||
trailing={<FormSwitch
|
||||
value={settings.timestamps._enabled}
|
||||
onValueChange={v => settings.timestamps._enabled = v}
|
||||
/>}
|
||||
/>
|
||||
<FormInput
|
||||
title="Start Timestamp (milliseconds)"
|
||||
value={settings.timestamps.start}
|
||||
placeholder="1234567890"
|
||||
disabled={!settings.timestamps._enabled}
|
||||
onChange={v => settings.timestamps.start = v}
|
||||
keyboardType="numeric"
|
||||
/>
|
||||
<FormInput
|
||||
title="End Timestamp (milliseconds)"
|
||||
value={settings.timestamps.end}
|
||||
placeholder="1234567890"
|
||||
disabled={!settings.timestamps._enabled}
|
||||
onChange={v => settings.timestamps.end = v}
|
||||
keyboardType="numeric"
|
||||
/>
|
||||
<FormRow
|
||||
label="Use current time as start timestamp"
|
||||
subLabel="This will override the start timestamp you set above"
|
||||
disabled={!settings.timestamps._enabled}
|
||||
onPress={() => settings.timestamps.start = String(Date.now())}
|
||||
trailing={FormRow.Arrow}
|
||||
/>
|
||||
</FormSection>
|
||||
<FormText style={styles.subText}>
|
||||
Leaving start timestamp blank will use the time the Discord started.
|
||||
</FormText>
|
||||
<FormSection title="Buttons">
|
||||
<FormInput
|
||||
title="First Button Text"
|
||||
value={settings.buttons[0].label}
|
||||
placeholder="random link #1"
|
||||
onChange={v => settings.buttons[0].label = v}
|
||||
/>
|
||||
<FormInput
|
||||
title="First Button URL"
|
||||
value={settings.buttons[0].url}
|
||||
placeholder="https://discord.com/vanityurl/dotcom/steakpants/flour/flower/index11.html"
|
||||
disabled={!settings.buttons[0].label}
|
||||
onChange={v => settings.buttons[0].url = v}
|
||||
/>
|
||||
<FormInput
|
||||
title="Second Button Text"
|
||||
value={settings.buttons[1].label}
|
||||
placeholder="random link #2"
|
||||
onChange={v => settings.buttons[1].label = v}
|
||||
/>
|
||||
<FormInput
|
||||
title="Second Button URL"
|
||||
value={settings.buttons[1].url}
|
||||
placeholder="https://youtu.be/w0AOGeqOnFY"
|
||||
disabled={!settings.buttons[1].label}
|
||||
onChange={v => settings.buttons[1].url = v}
|
||||
/>
|
||||
</FormSection>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
40
plugins/RichPresence/src/Settings.tsx
Normal file
40
plugins/RichPresence/src/Settings.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import RPInstance from ".";
|
||||
|
||||
import { NavigationNative } from "@vendetta/metro/common";
|
||||
import plugin from "@vendetta/plugin";
|
||||
import { Forms } from "@vendetta/ui/components";
|
||||
import ConfigEditor from "./ConfigEditor";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { TouchableOpacity } from "react-native";
|
||||
|
||||
const { FormText } = Forms;
|
||||
const storage = plugin.storage as typeof plugin.storage & {
|
||||
selected: string;
|
||||
selections: Record<string, Activity>;
|
||||
};
|
||||
|
||||
function UpdateButton() {
|
||||
async function onPressCallback() {
|
||||
RPInstance.onUnload();
|
||||
RPInstance.onLoad();
|
||||
}
|
||||
|
||||
return <TouchableOpacity onPress={onPressCallback}>
|
||||
<FormText style={{ marginRight: 12 }}>UPDATE</FormText>
|
||||
</TouchableOpacity>;
|
||||
}
|
||||
|
||||
export default function Settings() {
|
||||
const navigation = NavigationNative.useNavigation();
|
||||
|
||||
useEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerRight: UpdateButton
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ConfigEditor selection={storage.selected} />
|
||||
);
|
||||
}
|
||||
121
plugins/RichPresence/src/index.ts
Normal file
121
plugins/RichPresence/src/index.ts
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import { logger, plugin } from "@vendetta";
|
||||
import { findByProps } from "@vendetta/metro";
|
||||
import { FluxDispatcher } from "@vendetta/metro/common";
|
||||
import Settings from "./Settings";
|
||||
import { cloneAndFilter } from "./utils";
|
||||
|
||||
enum ActivityTypes {
|
||||
PLAYING = 0,
|
||||
STREAMING = 1,
|
||||
LISTENING = 2,
|
||||
WATCHING = 3,
|
||||
// CUSTOM = 4,
|
||||
COMPETING = 5
|
||||
}
|
||||
|
||||
const assetManager = findByProps("getAssetIds");
|
||||
|
||||
const pluginStartSince = Date.now();
|
||||
|
||||
const storage = plugin.storage as typeof plugin.storage & {
|
||||
selected: string;
|
||||
selections: Record<string, Activity>;
|
||||
};
|
||||
|
||||
if (typeof storage.selected?.length !== "number") {
|
||||
Object.assign(storage, {
|
||||
selected: "default",
|
||||
selections: {
|
||||
default: createDefaultSelection()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function createDefaultSelection(): Activity {
|
||||
return {
|
||||
name: "Discord",
|
||||
application_id: "1054951789318909972",
|
||||
flags: 0,
|
||||
type: ActivityTypes.PLAYING,
|
||||
timestamps: {
|
||||
_enabled: false,
|
||||
start: pluginStartSince
|
||||
},
|
||||
assets: {},
|
||||
// @ts-ignore
|
||||
buttons: [{}, {}]
|
||||
};
|
||||
}
|
||||
|
||||
async function sendRequest(activity: Activity | null): Promise<{ [K in keyof Activity]: any } & Record<string, any>> {
|
||||
if (typeof activity !== "object") throw new Error("Invalid activity");
|
||||
|
||||
if (activity) {
|
||||
const timestampEnabled = activity?.timestamps?._enabled;
|
||||
activity = cloneAndFilter(activity);
|
||||
|
||||
if (timestampEnabled) {
|
||||
activity.timestamps.start ||= pluginStartSince;
|
||||
} else {
|
||||
delete activity.timestamps;
|
||||
}
|
||||
|
||||
if (activity.assets) {
|
||||
const args = [activity.application_id, [activity.assets.large_image, activity.assets.small_image]];
|
||||
|
||||
let assetIds = assetManager.getAssetIds(...args);
|
||||
if (!assetIds.length) assetIds = await assetManager.fetchAssetIds(...args);
|
||||
|
||||
activity.assets.large_image = assetIds[0];
|
||||
activity.assets.small_image = assetIds[1];
|
||||
}
|
||||
|
||||
if (activity.buttons?.length) {
|
||||
activity.buttons = activity.buttons.filter(x => x.label && x.url);
|
||||
activity.buttons.length
|
||||
? Object.assign(activity, {
|
||||
metadata: { button_urls: activity.buttons.map(x => x.url) },
|
||||
buttons: activity.buttons.map(x => x.label)
|
||||
})
|
||||
: delete activity.buttons;
|
||||
} else {
|
||||
delete activity.buttons;
|
||||
}
|
||||
}
|
||||
|
||||
FluxDispatcher.dispatch({
|
||||
type: "LOCAL_ACTIVITY_UPDATE",
|
||||
activity: activity,
|
||||
pid: 1608,
|
||||
socketId: "RichPresence@Vendetta"
|
||||
});
|
||||
|
||||
return activity;
|
||||
}
|
||||
|
||||
export default new class RichPresence {
|
||||
onLoad() {
|
||||
logger.log("Sending RPC request");
|
||||
|
||||
const currentActivity = storage.selections[storage.selected];
|
||||
if (!currentActivity) {
|
||||
throw new Error("Selected activity does not exist");
|
||||
}
|
||||
|
||||
sendRequest(currentActivity)
|
||||
.then(x => {
|
||||
logger.log("RPC request sent");
|
||||
logger.log(x);
|
||||
}).catch(e => {
|
||||
logger.error("An error occured while sending RPC request :(");
|
||||
logger.error(e?.stack ?? e);
|
||||
});
|
||||
}
|
||||
|
||||
onUnload() {
|
||||
logger.log("Stopping RPC request");
|
||||
sendRequest(null);
|
||||
}
|
||||
|
||||
settings = Settings;
|
||||
};
|
||||
25
plugins/RichPresence/src/utils.ts
Normal file
25
plugins/RichPresence/src/utils.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
function isValid(val: any) {
|
||||
if (val && typeof val.length === "number") return val.length > 0;
|
||||
return val || val === 0 || val === false;
|
||||
}
|
||||
|
||||
// Deep clone and filter empty objects/values or keys starting with "_"
|
||||
export function cloneAndFilter<T extends object>(obj: T): T {
|
||||
const filter = (k: PropertyKey, v: any) => {
|
||||
if (v === obj) return v;
|
||||
if (typeof k === "string" && k.startsWith("_")) return undefined;
|
||||
|
||||
switch (typeof v) {
|
||||
case "object": {
|
||||
if (Array.isArray(v) && v.length === 0) return undefined;
|
||||
|
||||
const filteredEntries = Object.entries(v).filter((e) => isValid(e[1]));
|
||||
return filteredEntries.length > 0 ? Object.fromEntries(filteredEntries) : undefined;
|
||||
}
|
||||
default:
|
||||
return isValid(v) ? v : undefined;
|
||||
}
|
||||
};
|
||||
|
||||
return JSON.parse(JSON.stringify(obj, filter));
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue