Remove old plugins and add all-in-one commands plugin
This commit is contained in:
parent
e213c562a5
commit
dd931ab617
26 changed files with 299 additions and 791 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -7,3 +7,4 @@ package-lock.json
|
|||
Themer
|
||||
RealThemer
|
||||
ChatMesser
|
||||
allcommands
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"name": "BetterSpotifyIntegration",
|
||||
"description": "Shrimply better integration with Spotify",
|
||||
"vendetta": { },
|
||||
"main": "src/index.tsx"
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
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>;
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
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?.();
|
||||
}
|
||||
};
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
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)) : [];
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"name": "HideGiftButton",
|
||||
"description": "Hides the gift button in the chat input.",
|
||||
"vendetta": {
|
||||
"icon": "ic_gift_24px"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
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
|
||||
};
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"name": "HoldGIFSend",
|
||||
"description": "Hold GIF links to chat input before sending them.",
|
||||
"vendetta": {
|
||||
"icon": "gif"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
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);
|
||||
});
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"name": "MoreConfirm",
|
||||
"description": "Prompts confirmations before making irreversible actions.",
|
||||
"vendetta": {
|
||||
"icon": "check"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
/*!
|
||||
* 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();
|
||||
}
|
||||
};
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"name": "NoAutoReplyMention",
|
||||
"description": "Turns off auto mention when replying to someone.",
|
||||
"vendetta": {
|
||||
"icon": "ic_notif_off"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
// 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;
|
||||
})
|
||||
};
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"name": "NoIdle",
|
||||
"description": "Prevent status from idling when Discord is in the background.",
|
||||
"vendetta": {
|
||||
"icon": "StatusIdle"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
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 }];
|
||||
});
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"name": "PyoncordInstaller",
|
||||
"description": "installs pyoncord.exe (virus)"
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
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!");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"name": "RichPresence",
|
||||
"description": "Sets Discord Rich Presence for your profile.",
|
||||
"vendetta": {
|
||||
"icon": "games"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,174 +0,0 @@
|
|||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
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} />
|
||||
);
|
||||
}
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
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;
|
||||
};
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
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));
|
||||
}
|
||||
198
plugins/all-in-one/index.ts
Normal file
198
plugins/all-in-one/index.ts
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
import { registerCommand } from "@vendetta/commands";
|
||||
import { findByStoreName, findByProps } from "@vendetta/metro";
|
||||
import { storage } from "@vendetta/plugin";
|
||||
import settings from "./settings";
|
||||
|
||||
const UserStore = findByStoreName("UserStore");
|
||||
const MessageActions = findByProps("sendMessage");
|
||||
|
||||
let commands: Array<() => void> = [];
|
||||
|
||||
// Initialize storage with default values if not set
|
||||
if (!storage.factSettings) {
|
||||
storage.factSettings = {
|
||||
sendAsReply: true,
|
||||
includeCitation: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (!storage.listSettings) {
|
||||
storage.listSettings = {
|
||||
pluginListAlwaysDetailed: false,
|
||||
themeListAlwaysDetailed: false,
|
||||
};
|
||||
}
|
||||
|
||||
// API Functions
|
||||
const uselessFact = async () => {
|
||||
const response = await fetch(
|
||||
"https://uselessfacts.jsph.pl/api/v2/facts/random",
|
||||
);
|
||||
const resp = await response.json();
|
||||
return {
|
||||
text: resp["text"],
|
||||
source: resp["source"],
|
||||
language: resp["language"],
|
||||
};
|
||||
};
|
||||
|
||||
const dogFact = async () => {
|
||||
const response = await fetch("https://dogapi.dog/api/v2/facts?limit=1");
|
||||
const resp = await response.json();
|
||||
return {
|
||||
text: resp["data"]["0"]["attributes"]["body"],
|
||||
};
|
||||
};
|
||||
|
||||
const catFact = async () => {
|
||||
const response = await fetch("https://catfact.ninja/fact");
|
||||
const resp = await response.json();
|
||||
return {
|
||||
text: resp["fact"],
|
||||
length: resp["length"],
|
||||
};
|
||||
};
|
||||
|
||||
async function getPetPetData(image: string) {
|
||||
const data = await fetch(
|
||||
`https://api.obamabot.me/v2/image/petpet?image=${image.replace("webp", "png")}`,
|
||||
);
|
||||
const body = await data.json();
|
||||
return body;
|
||||
}
|
||||
|
||||
// Helper function to format fact response
|
||||
const formatFactResponse = (fact: { text: string; source?: string }) => {
|
||||
let response = fact.text;
|
||||
if (storage.factSettings.includeCitation && fact.source) {
|
||||
response += `\n\nSource: ${fact.source}`;
|
||||
}
|
||||
return response;
|
||||
};
|
||||
|
||||
// Helper function to send message
|
||||
const sendMessage = (
|
||||
channelId: string,
|
||||
content: string,
|
||||
replyToId?: string,
|
||||
) => {
|
||||
const fixNonce = Date.now().toString();
|
||||
const message = {
|
||||
content,
|
||||
...(replyToId && storage.factSettings.sendAsReply
|
||||
? { messageReference: { messageId: replyToId } }
|
||||
: {}),
|
||||
};
|
||||
MessageActions.sendMessage(channelId, message, undefined, {
|
||||
nonce: fixNonce,
|
||||
});
|
||||
return { type: 4 }; // Acknowledge the command
|
||||
};
|
||||
|
||||
// Command Definitions
|
||||
const registerCommands = () => {
|
||||
// Cat Facts Command
|
||||
commands.push(
|
||||
registerCommand({
|
||||
name: "catfact",
|
||||
displayName: "catfact",
|
||||
description: "Sends a random cat fact.",
|
||||
displayDescription: "Sends a random cat fact.",
|
||||
applicationId: "-1",
|
||||
inputType: 1,
|
||||
type: 1,
|
||||
execute: async (args, ctx) => {
|
||||
const fact = await catFact();
|
||||
return sendMessage(
|
||||
ctx.channel.id,
|
||||
formatFactResponse(fact),
|
||||
ctx.message?.id,
|
||||
);
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// Dog Facts Command
|
||||
commands.push(
|
||||
registerCommand({
|
||||
name: "dogfact",
|
||||
displayName: "dogfact",
|
||||
description: "Sends a dog fact.",
|
||||
displayDescription: "Sends a dog fact.",
|
||||
applicationId: "-1",
|
||||
inputType: 1,
|
||||
type: 1,
|
||||
execute: async (args, ctx) => {
|
||||
const fact = await dogFact();
|
||||
return sendMessage(
|
||||
ctx.channel.id,
|
||||
formatFactResponse(fact),
|
||||
ctx.message?.id,
|
||||
);
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// Useless Facts Command
|
||||
commands.push(
|
||||
registerCommand({
|
||||
name: "useless",
|
||||
displayName: "useless",
|
||||
description: "Sends a useless fact.",
|
||||
displayDescription: "Sends a useless fact.",
|
||||
applicationId: "-1",
|
||||
inputType: 1,
|
||||
type: 1,
|
||||
execute: async (args, ctx) => {
|
||||
const fact = await uselessFact();
|
||||
return sendMessage(
|
||||
ctx.channel.id,
|
||||
formatFactResponse(fact),
|
||||
ctx.message?.id,
|
||||
);
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// PetPet Command
|
||||
commands.push(
|
||||
registerCommand({
|
||||
name: "petpet",
|
||||
displayName: "petpet",
|
||||
displayDescription: "PetPet someone",
|
||||
description: "PetPet someone",
|
||||
options: [
|
||||
{
|
||||
name: "user",
|
||||
description: "The user(or their id) to be patted",
|
||||
type: 6,
|
||||
required: true,
|
||||
displayName: "user",
|
||||
displayDescription: "The user(or their id) to be patted",
|
||||
},
|
||||
],
|
||||
execute: async (args: any, ctx: any) => {
|
||||
const user = await UserStore.getUser(args[0].value);
|
||||
const image = user.getAvatarURL(512);
|
||||
const data = await getPetPetData(image);
|
||||
return sendMessage(ctx.channel.id, data.url, ctx.message?.id);
|
||||
},
|
||||
applicationId: "-1",
|
||||
inputType: 1,
|
||||
type: 1,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
export default {
|
||||
onLoad: () => {
|
||||
console.log("[All-In-One Commands] Plugin loaded!");
|
||||
registerCommands();
|
||||
},
|
||||
onUnload: () => {
|
||||
console.log("[All-In-One Commands] Plugin unloaded!");
|
||||
commands.forEach((unregister) => unregister());
|
||||
commands = [];
|
||||
},
|
||||
settings,
|
||||
};
|
||||
16
plugins/all-in-one/manifest.json
Normal file
16
plugins/all-in-one/manifest.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "All Commands",
|
||||
"description": "A collection of useful commands including facts, petpet, and more.",
|
||||
"authors": [
|
||||
{
|
||||
"name": "kmmiio99o",
|
||||
"id": "879393496627306587"
|
||||
}
|
||||
],
|
||||
"main": "index.ts",
|
||||
"vendetta": {
|
||||
"icon": "ic_badge_staff",
|
||||
"original": "plugins/all-in-one"
|
||||
},
|
||||
"version": "1.0.0"
|
||||
}
|
||||
84
plugins/all-in-one/settings.tsx
Normal file
84
plugins/all-in-one/settings.tsx
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import { ReactNative } from "@vendetta/metro/common";
|
||||
import { storage } from "@vendetta/plugin";
|
||||
import { useProxy } from "@vendetta/storage";
|
||||
import { getAssetIDByName } from "@vendetta/ui/assets";
|
||||
import { ErrorBoundary, Forms } from "@vendetta/ui/components";
|
||||
|
||||
const { FormSwitchRow, FormIcon, FormSection, FormDivider } = Forms;
|
||||
const Icons = {
|
||||
List: getAssetIDByName('ic_list'),
|
||||
Copy: getAssetIDByName('ic_copy_message_link'),
|
||||
Settings: getAssetIDByName('ic_settings'),
|
||||
Fact: getAssetIDByName('ic_info')
|
||||
};
|
||||
|
||||
// Initialize storage with default values if not set
|
||||
if (!storage.factSettings) {
|
||||
storage.factSettings = {
|
||||
sendAsReply: true,
|
||||
includeCitation: false
|
||||
};
|
||||
}
|
||||
|
||||
if (!storage.listSettings) {
|
||||
storage.listSettings = {
|
||||
pluginListAlwaysDetailed: false,
|
||||
themeListAlwaysDetailed: false
|
||||
};
|
||||
}
|
||||
|
||||
export default () => {
|
||||
useProxy(storage);
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<ReactNative.ScrollView style={{ flex: 1 }}>
|
||||
{/* Plugin List Settings */}
|
||||
<FormSection title="Plugin List Settings" titleStyleType="no_border">
|
||||
<FormSwitchRow
|
||||
label="Always send detailed plugin list"
|
||||
subLabel='Always use detailed mode when listing plugins'
|
||||
leading={<FormIcon source={Icons.List} />}
|
||||
onValueChange={(value: boolean) => {
|
||||
storage.listSettings.pluginListAlwaysDetailed = value;
|
||||
}}
|
||||
value={storage.listSettings.pluginListAlwaysDetailed}
|
||||
/>
|
||||
<FormDivider />
|
||||
<FormSwitchRow
|
||||
label="Always send detailed theme list"
|
||||
subLabel='Always use detailed mode when listing themes'
|
||||
leading={<FormIcon source={Icons.List} />}
|
||||
onValueChange={(value: boolean) => {
|
||||
storage.listSettings.themeListAlwaysDetailed = value;
|
||||
}}
|
||||
value={storage.listSettings.themeListAlwaysDetailed}
|
||||
/>
|
||||
</FormSection>
|
||||
|
||||
{/* Fact Command Settings */}
|
||||
<FormSection title="Fact Commands Settings" titleStyleType="no_border">
|
||||
<FormSwitchRow
|
||||
label="Send as Reply"
|
||||
subLabel='Send facts as a reply to the command message'
|
||||
leading={<FormIcon source={Icons.Copy} />}
|
||||
onValueChange={(value: boolean) => {
|
||||
storage.factSettings.sendAsReply = value;
|
||||
}}
|
||||
value={storage.factSettings.sendAsReply}
|
||||
/>
|
||||
<FormDivider />
|
||||
<FormSwitchRow
|
||||
label="Include Source Citation"
|
||||
subLabel='Include the source of the fact when available'
|
||||
leading={<FormIcon source={Icons.Fact} />}
|
||||
onValueChange={(value: boolean) => {
|
||||
storage.factSettings.includeCitation = value;
|
||||
}}
|
||||
value={storage.factSettings.includeCitation}
|
||||
/>
|
||||
</FormSection>
|
||||
</ReactNative.ScrollView>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue