diff --git a/plugins/Commands/index.ts b/plugins/Commands/index.ts
index 4b68e62..9848ce6 100644
--- a/plugins/Commands/index.ts
+++ b/plugins/Commands/index.ts
@@ -25,7 +25,8 @@ import {
friendInviteViewCommand,
friendInviteRevokeCommand,
} from "./src/commands/friendinvites";
-import settings from "./settings";
+import settings from "./settings/settings";
+import patchSidebar from "./sidebar";
if (!storage.factSettings) {
storage.factSettings = {
@@ -76,6 +77,10 @@ if (!storage.hiddenSettings) {
konochanBypassNsfw: false,
};
}
+// sidebar enabled setting
+if (storage.sidebarEnabled === undefined) {
+ storage.sidebarEnabled = true;
+}
const commandMap = {
catfact: catFactCommand,
@@ -102,18 +107,60 @@ const commandMap = {
};
let commands: Array<() => void> = [];
+let sidebarUnpatch: (() => void) | undefined;
export default {
onLoad: () => {
+ console.log("[Commands Plugin] Loading...");
+
+ // Patch sidebar if enabled
+ if (storage.sidebarEnabled !== false) {
+ try {
+ sidebarUnpatch = patchSidebar();
+ console.log("[Commands Plugin] Sidebar patched successfully");
+ } catch (error) {
+ console.error("[Commands Plugin] Failed to patch sidebar:", error);
+ }
+ }
+
+ // Register commands
for (const [key, command] of Object.entries(commandMap)) {
if (storage.enabledCommands[key]) {
- commands.push(registerCommand(command));
+ try {
+ commands.push(registerCommand(command));
+ console.log(`[Commands Plugin] Registered command: ${key}`);
+ } catch (error) {
+ console.error(
+ `[Commands Plugin] Failed to register command ${key}:`,
+ error,
+ );
+ }
}
}
},
onUnload: () => {
- commands.forEach((unregister) => unregister());
+ console.log("[Commands Plugin] Unloading...");
+
+ // Unregister commands
+ commands.forEach((unregister) => {
+ try {
+ unregister();
+ } catch (error) {
+ // Ignore errors during cleanup
+ }
+ });
commands = [];
+
+ // Unpatch sidebar
+ if (sidebarUnpatch) {
+ try {
+ sidebarUnpatch();
+ sidebarUnpatch = undefined;
+ console.log("[Commands Plugin] Sidebar unpatched");
+ } catch (error) {
+ console.error("[Commands Plugin] Failed to unpatch sidebar:", error);
+ }
+ }
},
settings,
};
diff --git a/plugins/Commands/settings.tsx b/plugins/Commands/settings.tsx
deleted file mode 100644
index 378a298..0000000
--- a/plugins/Commands/settings.tsx
+++ /dev/null
@@ -1,1455 +0,0 @@
-import { storage } from "@vendetta/plugin";
-import { useProxy } from "@vendetta/storage";
-import { React, ReactNative as RN, stylesheet } from "@vendetta/metro/common";
-import { NavigationNative } from "@vendetta/metro/common";
-import { semanticColors } from "@vendetta/ui";
-import { getAssetIDByName } from "@vendetta/ui/assets";
-import { Forms } from "@vendetta/ui/components";
-import { alerts } from "@vendetta/ui";
-import { showToast } from "@vendetta/ui/toasts";
-
-const { ScrollView } = RN;
-const { FormRow, FormSwitchRow } = Forms;
-
-// Initialize storage with default values
-storage.factSettings ??= {
- sendAsReply: true,
- includeCitation: false,
-};
-
-storage.listSettings ??= {
- pluginListAlwaysDetailed: false,
- themeListAlwaysDetailed: false,
-};
-
-storage.garySettings ??= {
- imageSource: "gary",
-};
-
-storage.enabledCommands ??= {
- catfact: true,
- dogfact: true,
- useless: true,
- petpet: true,
- pluginList: true,
- themeList: true,
- konoself: true,
- konosend: true,
- firstmessage: true,
- sysinfo: true,
- spotifyTrack: true,
- spotifyAlbum: true,
- spotifyArtists: true,
- spotifyCover: true,
- gary: true,
- ip: true,
- lovefemboys: false,
- nekoslife: false,
- friendInviteCreate: true,
- friendInviteView: true,
- friendInviteRevoke: true,
-};
-
-storage.pendingRestart ??= false;
-
-// Hidden settings storage with NSFW bypass option
-storage.hiddenSettings ??= {
- enabled: false,
- visible: false,
- konochanBypassNsfw: false,
-};
-
-// Better Table Row Group Component
-function BetterTableRowGroup({
- title,
- icon,
- children,
- padding = false,
-}: React.PropsWithChildren<{
- title?: string;
- icon?: number;
- padding?: boolean;
-}>) {
- const styles = stylesheet.createThemedStyleSheet({
- main: {
- backgroundColor: semanticColors.CARD_PRIMARY_BG,
- borderColor: semanticColors.BORDER_FAINT,
- borderWidth: 1,
- borderRadius: 16,
- overflow: "hidden",
- flex: 1,
- },
- titleContainer: {
- marginBottom: 8,
- marginHorizontal: 0,
- marginTop: 8,
- gap: 4,
- flexDirection: "row",
- alignItems: "center",
- },
- icon: {
- width: 16,
- height: 16,
- marginTop: 1.5,
- tintColor: semanticColors.TEXT_MUTED,
- },
- titleText: {
- fontSize: 14,
- fontWeight: "600",
- color: semanticColors.TEXT_MUTED,
- },
- });
-
- return (
-
- {title && (
-
- {icon && (
-
- )}
- {title.toUpperCase()}
-
- )}
-
- {padding ? (
-
- {children}
-
- ) : (
- children
- )}
-
-
- );
-}
-
-// Header Component with secret unlock
-function Header({ onHiddenUnlock }: { onHiddenUnlock?: () => void }) {
- const [clickCounter, setClickCounter] = React.useState(0);
- const [clickTimeout, setClickTimeout] = React.useState(
- null,
- );
-
- const styles = stylesheet.createThemedStyleSheet({
- container: {
- flexDirection: "column",
- alignItems: "center",
- paddingVertical: 24,
- paddingHorizontal: 16,
- },
- title: {
- fontSize: 28,
- fontWeight: "700",
- color: semanticColors.HEADER_PRIMARY,
- marginBottom: 4,
- },
- subtitle: {
- fontSize: 16,
- fontWeight: "500",
- color: semanticColors.HEADER_SECONDARY,
- },
- iconContainer: {
- backgroundColor: semanticColors.BACKGROUND_SECONDARY,
- borderRadius: 50,
- width: 64,
- height: 64,
- justifyContent: "center",
- alignItems: "center",
- marginBottom: 16,
- },
- icon: {
- width: 32,
- height: 32,
- tintColor: semanticColors.INTERACTIVE_NORMAL,
- },
- });
-
- const handleIconPress = () => {
- if (storage.hiddenSettings.enabled) {
- storage.hiddenSettings.visible = !storage.hiddenSettings.visible;
- showToast(
- `Hidden settings ${storage.hiddenSettings.visible ? "visible" : "hidden"}`,
- getAssetIDByName("SettingsIcon"),
- );
- onHiddenUnlock?.();
- return;
- }
-
- if (clickTimeout) {
- clearTimeout(clickTimeout);
- }
-
- const newTimeout = setTimeout(() => {
- setClickCounter(0);
- }, 1000);
- setClickTimeout(newTimeout);
-
- const newCounter = clickCounter + 1;
- setClickCounter(newCounter);
-
- if (newCounter < 10) {
- return;
- }
-
- showToast(
- "🔓 Hidden settings unlocked!",
- getAssetIDByName("CheckmarkIcon"),
- );
- storage.hiddenSettings.enabled = true;
- storage.hiddenSettings.visible = true;
- setClickCounter(0);
-
- onHiddenUnlock?.();
- };
-
- return (
-
-
-
-
- Commands
- A collection of useful commands
-
- );
-}
-
-// NekosLife Categories Reference Page - SFW ONLY
-function NekosLifeCategoriesPage() {
- const styles = stylesheet.createThemedStyleSheet({
- container: {
- flex: 1,
- backgroundColor: semanticColors.BACKGROUND_PRIMARY,
- },
- tableHeader: {
- flexDirection: "row",
- alignItems: "center",
- paddingVertical: 12,
- paddingHorizontal: 16,
- backgroundColor: semanticColors.BACKGROUND_MODIFIER_ACCENT,
- borderBottomWidth: 2,
- borderBottomColor: semanticColors.BORDER_STRONG,
- },
- headerText: {
- fontSize: 14,
- fontWeight: "700",
- color: semanticColors.HEADER_PRIMARY,
- textTransform: "uppercase",
- },
- nameHeader: {
- flex: 2,
- },
- codeHeader: {
- flex: 2,
- textAlign: "center",
- },
- badgeHeader: {
- flex: 1,
- textAlign: "center",
- },
- categoryItem: {
- flexDirection: "row",
- alignItems: "center",
- paddingVertical: 12,
- paddingHorizontal: 16,
- borderBottomWidth: 1,
- borderBottomColor: semanticColors.BORDER_FAINT,
- },
- categoryName: {
- fontSize: 16,
- fontWeight: "500",
- color: semanticColors.TEXT_NORMAL,
- flex: 2,
- },
- categoryValue: {
- fontSize: 14,
- color: semanticColors.TEXT_MUTED,
- fontFamily: "monospace",
- flex: 2,
- textAlign: "center",
- },
- sfwBadgeContainer: {
- flex: 1,
- alignItems: "center",
- },
- sfwBadge: {
- backgroundColor: semanticColors.STATUS_POSITIVE,
- paddingHorizontal: 8,
- paddingVertical: 4,
- borderRadius: 12,
- },
- badgeText: {
- fontSize: 12,
- fontWeight: "600",
- color: semanticColors.TEXT_NORMAL,
- },
- infoText: {
- fontSize: 14,
- color: semanticColors.TEXT_MUTED,
- textAlign: "center",
- lineHeight: 20,
- },
- });
-
- // nekos.life categories
- const sfwCategories = [
- { name: "Avatar", value: "avatar" },
- { name: "Classic", value: "classic" },
- { name: "Cuddle", value: "cuddle" },
- { name: "Fox Girl", value: "fox_girl" },
- { name: "Gecg", value: "gecg" },
- { name: "Holo", value: "holo" },
- { name: "Kemonomimi", value: "kemonomimi" },
- { name: "Kiss", value: "kiss" },
- { name: "Neko", value: "neko" },
- { name: "Neko GIF", value: "ngif" },
- { name: "Smug", value: "smug" },
- { name: "Spank", value: "spank" },
- { name: "Tickle", value: "tickle" },
- { name: "Waifu", value: "waifu" },
- { name: "Wallpaper", value: "wallpaper" },
- { name: "Woof", value: "woof" },
- ];
-
- // Sort categories alphabetically
- const sortedCategories = sfwCategories.sort((a, b) =>
- a.name.localeCompare(b.name),
- );
-
- return (
-
-
-
-
- Total SFW Categories: {sfwCategories.length}
- {"\n\n"}
- Use these category names with the /nekoslife command.{"\n"}
- All categories are Safe For Work (SFW).
-
-
-
-
- {/* Table Header */}
-
-
- Category Name
-
-
- Command Code
-
-
- Type
-
-
-
- {/* Table Rows */}
- {sortedCategories.map((category, index) => (
-
- {category.name}
- {category.value}
-
-
- SFW
-
-
-
- ))}
-
-
-
- );
-}
-
-// Hidden Commands Settings Page
-function HiddenSettingsPage() {
- useProxy(storage);
- const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
- const navigation = NavigationNative.useNavigation();
-
- const styles = stylesheet.createThemedStyleSheet({
- container: {
- flex: 1,
- backgroundColor: semanticColors.BACKGROUND_PRIMARY,
- },
- warningText: {
- fontSize: 14,
- color: semanticColors.TEXT_DANGER,
- textAlign: "center",
- lineHeight: 20,
- fontWeight: "600",
- },
- infoText: {
- fontSize: 14,
- color: semanticColors.TEXT_MUTED,
- textAlign: "center",
- lineHeight: 20,
- marginTop: 8,
- },
- });
-
- return (
-
-
-
-
- These are hidden commands that may contain mature content or
- experimental features.
-
-
- Use at your own discretion. Commands in this section are disabled by
- default.
-
-
-
-
- }
- value={storage.enabledCommands.lovefemboys}
- onValueChange={(v) => {
- storage.enabledCommands.lovefemboys = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
-
-
- }
- value={storage.hiddenSettings.konochanBypassNsfw}
- onValueChange={(v) => {
- if (v) {
- alerts.showConfirmationAlert({
- title: "⚠️ NSFW Bypass Warning",
- content:
- "Enabling this allows NSFW content from KonoChan to be sent in any channel, including non-NSFW channels. This could violate server rules or Discord ToS. Use responsibly!",
- confirmText: "I Understand",
- cancelText: "Cancel",
- onConfirm: () => {
- storage.hiddenSettings.konochanBypassNsfw = true;
- forceUpdate();
- },
- onCancel: () => {},
- });
- } else {
- storage.hiddenSettings.konochanBypassNsfw = false;
- forceUpdate();
- }
- }}
- />
-
-
-
- }
- value={storage.hiddenSettings.visible}
- onValueChange={(v) => {
- storage.hiddenSettings.visible = v;
- }}
- />
- }
- onPress={() => {
- alerts.showConfirmationAlert({
- title: "Reset Hidden Settings",
- content:
- "This will hide the hidden settings section and disable all hidden commands. Are you sure?",
- confirmText: "Reset",
- onConfirm: () => {
- storage.hiddenSettings.enabled = false;
- storage.hiddenSettings.visible = false;
- storage.hiddenSettings.konochanBypassNsfw = false;
- storage.enabledCommands.lovefemboys = false;
-
- storage.pendingRestart = true;
- showToast(
- "Hidden settings reset",
- getAssetIDByName("CheckmarkIcon"),
- );
- },
- cancelText: "Cancel",
- });
- }}
- />
-
-
-
- );
-}
-
-// Facts Commands Settings Page
-function FactsSettingsPage() {
- useProxy(storage);
- const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
-
- return (
-
-
-
-
- }
- value={storage.factSettings.sendAsReply}
- onValueChange={(v) => {
- storage.factSettings.sendAsReply = v;
- }}
- />
- }
- value={storage.factSettings.includeCitation}
- onValueChange={(v) => {
- storage.factSettings.includeCitation = v;
- }}
- />
-
-
-
-
- }
- value={storage.enabledCommands.catfact}
- onValueChange={(v) => {
- storage.enabledCommands.catfact = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
- }
- value={storage.enabledCommands.dogfact}
- onValueChange={(v) => {
- storage.enabledCommands.dogfact = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
- }
- value={storage.enabledCommands.useless}
- onValueChange={(v) => {
- storage.enabledCommands.useless = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
-
-
- );
-}
-
-// Gary API Settings Page
-function GaryAPIPage() {
- useProxy(storage);
- const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
-
- const styles = stylesheet.createThemedStyleSheet({
- container: {
- flex: 1,
- backgroundColor: semanticColors.BACKGROUND_PRIMARY,
- },
- infoText: {
- fontSize: 14,
- color: semanticColors.TEXT_MUTED,
- marginBottom: 16,
- textAlign: "center",
- },
- currentText: {
- fontSize: 14,
- color: semanticColors.TEXT_NORMAL,
- textAlign: "center",
- fontWeight: "600",
- },
- });
-
- const currentSource = storage.garySettings.imageSource;
-
- return (
-
-
-
-
- }
- value={storage.enabledCommands.gary}
- onValueChange={(v) => {
- storage.enabledCommands.gary = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
-
-
-
- Choose which API the /gary command should use to fetch images.
-
-
-
-
- }
- value={currentSource === "gary"}
- onValueChange={(v) => {
- if (v) {
- storage.garySettings.imageSource = "gary";
- forceUpdate();
- }
- }}
- />
- }
- value={currentSource === "catapi"}
- onValueChange={(v) => {
- if (v) {
- storage.garySettings.imageSource = "catapi";
- forceUpdate();
- }
- }}
- />
- }
- value={currentSource === "minker"}
- onValueChange={(v) => {
- if (v) {
- storage.garySettings.imageSource = "minker";
- forceUpdate();
- }
- }}
- />
- }
- value={currentSource === "goober"}
- onValueChange={(v) => {
- if (v) {
- storage.garySettings.imageSource = "goober";
- forceUpdate();
- }
- }}
- />
-
-
-
-
- Currently using:{" "}
- {currentSource === "gary"
- ? "Gary API"
- : currentSource === "catapi"
- ? "Cat API"
- : currentSource === "minker"
- ? "Minker API"
- : currentSource === "goober"
- ? "Goober API"
- : "Gary API"}
-
-
-
-
- );
-}
-
-// List Settings Page
-function ListSettingsPage() {
- useProxy(storage);
- const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
-
- return (
-
-
-
-
- }
- value={storage.listSettings.pluginListAlwaysDetailed}
- onValueChange={(v) => {
- storage.listSettings.pluginListAlwaysDetailed = v;
- }}
- />
-
- }
- value={storage.listSettings.themeListAlwaysDetailed}
- onValueChange={(v) => {
- storage.listSettings.themeListAlwaysDetailed = v;
- }}
- />
-
-
-
-
- }
- value={storage.enabledCommands.pluginList}
- onValueChange={(v) => {
- storage.enabledCommands.pluginList = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
- }
- value={storage.enabledCommands.themeList}
- onValueChange={(v) => {
- storage.enabledCommands.themeList = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
-
-
- );
-}
-
-// Image Settings Page
-function ImageSettingsPage() {
- useProxy(storage);
- const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
-
- return (
-
-
-
-
- }
- value={storage.enabledCommands.petpet}
- onValueChange={(v) => {
- storage.enabledCommands.petpet = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
-
-
- }
- value={storage.enabledCommands.konoself}
- onValueChange={(v) => {
- storage.enabledCommands.konoself = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
- }
- value={storage.enabledCommands.konosend}
- onValueChange={(v) => {
- storage.enabledCommands.konosend = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
-
-
- );
-}
-
-// Spotify Settings Page
-function SpotifySettingsPage() {
- useProxy(storage);
- const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
-
- const styles = stylesheet.createThemedStyleSheet({
- infoText: {
- fontSize: 14,
- color: semanticColors.TEXT_MUTED,
- textAlign: "center",
- lineHeight: 20,
- },
- });
-
- return (
-
-
-
-
- }
- value={storage.enabledCommands.spotifyTrack}
- onValueChange={(v) => {
- storage.enabledCommands.spotifyTrack = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
- }
- value={storage.enabledCommands.spotifyAlbum}
- onValueChange={(v) => {
- storage.enabledCommands.spotifyAlbum = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
- }
- value={storage.enabledCommands.spotifyArtists}
- onValueChange={(v) => {
- storage.enabledCommands.spotifyArtists = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
- }
- value={storage.enabledCommands.spotifyCover}
- onValueChange={(v) => {
- storage.enabledCommands.spotifyCover = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
-
-
-
- These commands allow you to share your current Spotify activity in
- Discord. Make sure you have Spotify connected to Discord for these
- commands to work properly.
-
-
-
-
- );
-}
-
-// Other Settings Page - Updated with friend invite commands
-function OtherSettingsPage() {
- useProxy(storage);
- const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
-
- return (
-
-
-
- }
- value={storage.enabledCommands.firstmessage}
- onValueChange={(v) => {
- storage.enabledCommands.firstmessage = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
-
-
- }
- value={storage.enabledCommands.sysinfo}
- onValueChange={(v) => {
- storage.enabledCommands.sysinfo = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
-
-
- {
- storage.enabledCommands.friendInviteCreate = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
- {
- storage.enabledCommands.friendInviteView = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
- {
- storage.enabledCommands.friendInviteRevoke = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
-
-
- );
-}
-
-// Credits Page
-function AliucordPage() {
- useProxy(storage);
- const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
- const navigation = NavigationNative.useNavigation();
-
- return (
-
-
- }
- value={storage.enabledCommands.ip}
- onValueChange={(v) => {
- storage.enabledCommands.ip = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
-
-
- }
- value={storage.enabledCommands.nekoslife}
- onValueChange={(v) => {
- storage.enabledCommands.nekoslife = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
- }
- trailing={}
- onPress={() =>
- navigation.push("VendettaCustomPage", {
- title: "NekosLife Categories",
- render: NekosLifeCategoriesPage,
- })
- }
- />
-
-
-
-
- );
-}
-
-function CreditsPage() {
- const styles = stylesheet.createThemedStyleSheet({
- container: {
- flex: 1,
- backgroundColor: semanticColors.BACKGROUND_PRIMARY,
- },
- creditItem: {
- flexDirection: "row",
- alignItems: "center",
- paddingVertical: 16,
- paddingHorizontal: 20,
- backgroundColor: semanticColors.CARD_PRIMARY_BG,
- borderRadius: 12,
- marginHorizontal: 16,
- marginVertical: 6,
- },
- avatar: {
- width: 48,
- height: 48,
- borderRadius: 24,
- marginRight: 16,
- backgroundColor: semanticColors.BACKGROUND_SECONDARY,
- },
- textContainer: {
- flex: 1,
- },
- commandText: {
- fontSize: 17,
- fontWeight: "600",
- color: semanticColors.TEXT_NORMAL,
- marginBottom: 4,
- },
- authorText: {
- fontSize: 15,
- color: semanticColors.TEXT_MUTED,
- marginBottom: 2,
- },
- linkText: {
- fontSize: 13,
- color: semanticColors.TEXT_LINK,
- fontWeight: "500",
- },
- infoText: {
- fontSize: 14,
- color: semanticColors.TEXT_NORMAL,
- marginBottom: 20,
- textAlign: "center",
- lineHeight: 20,
- },
- versionText: {
- fontSize: 15,
- color: semanticColors.TEXT_NORMAL,
- textAlign: "center",
- fontWeight: "600",
- lineHeight: 22,
- },
- });
-
- const credits = [
- {
- command: "Facts Commands",
- author: "jdev082",
- avatar: "https://github.com/jdev082.png",
- github: "https://github.com/jdev082",
- },
- {
- command: "List Commands",
- author: "Kitomanari",
- avatar: "https://github.com/Kitosight.png",
- github: "https://github.com/Kitosight",
- },
- {
- command: "PetPet",
- author: "wolfieeee",
- avatar: "https://github.com/WolfPlugs.png",
- github: "https://github.com/WolfPlugs",
- },
- {
- command: "KonoChan Commands",
- author: "btmc727 & Rico040",
- avatar: "https://github.com/OTKUSteyler.png",
- github: "https://github.com/OTKUSteyler",
- },
- {
- command: "FirstMessage Command",
- author: "sapphire",
- avatar: "https://github.com/aeongdesu.png",
- github: "https://github.com/aeongdesu",
- },
- {
- command: "Sysinfo Command",
- author: "mugman",
- avatar: "https://github.com/mugman174.png",
- github: "https://github.com/mugman174",
- },
- {
- command: "Spotify Commands",
- author: "Kitomanari",
- avatar: "https://github.com/Kitosight.png",
- github: "https://github.com/Kitosight",
- },
- {
- command: "Gary Command",
- author: "Zach Orange",
- avatar: "https://github.com/IAmGaryTheCat.png",
- github: "https://github.com/IAmGaryTheCat",
- },
- {
- command: "IP & NekosLife Commands",
- author: "scruzism",
- avatar: "https://github.com/scrazzz.png",
- github: "https://github.com/scrazzz",
- },
- {
- command: "FriendInvites",
- author: "nikosszzz",
- avatar: "https://github.com/nikosszzz.png",
- github: "https://github.com/nikosszzz",
- },
- ];
-
- const handleProfilePress = (githubUrl: string) => {
- RN.Linking.openURL(githubUrl);
- };
-
- return (
-
-
-
-
- Thanks to this developers for creating such a nice plugins!{"\n"}
- Tap on any developer to visit their GitHub profile.
-
-
-
- {credits.map((credit, index) => (
- handleProfilePress(credit.github)}
- android_ripple={{
- color: semanticColors.ANDROID_RIPPLE,
- radius: 200,
- }}
- >
-
-
- {credit.command}
- by {credit.author}
-
- {credit.github.replace("https://github.com/", "@")}
-
-
-
- ))}
-
-
-
- Commands Plugin Collection{"\n"}
- Version 1.2.1
-
-
-
-
- );
-}
-
-// MAIN SETTINGS COMPONENT
-export default function Settings() {
- useProxy(storage);
- const navigation = NavigationNative.useNavigation();
- const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
-
- React.useEffect(() => {
- return () => {
- if (storage.pendingRestart) {
- storage.pendingRestart = false;
- alerts.showConfirmationAlert({
- title: "Restart Required",
- content:
- "You have made changes to commands. Please restart Discord to apply these changes.",
- confirmText: "Okay",
- cancelText: null,
- onConfirm: () => {}, // Fixed TypeScript error: added onConfirm handler
- });
- }
- };
- }, []);
-
- return (
-
-
-
-
- }
- trailing={}
- onPress={() =>
- navigation.push("VendettaCustomPage", {
- title: "Facts Commands",
- render: FactsSettingsPage,
- })
- }
- />
- }
- trailing={}
- onPress={() =>
- navigation.push("VendettaCustomPage", {
- title: "List Commands",
- render: ListSettingsPage,
- })
- }
- />
- }
- trailing={}
- onPress={() =>
- navigation.push("VendettaCustomPage", {
- title: "Image Commands",
- render: ImageSettingsPage,
- })
- }
- />
- }
- trailing={}
- onPress={() =>
- navigation.push("VendettaCustomPage", {
- title: "Gary Commands",
- render: GaryAPIPage,
- })
- }
- />
-
- }
- trailing={}
- onPress={() =>
- navigation.push("VendettaCustomPage", {
- title: "Spotify Commands",
- render: SpotifySettingsPage,
- })
- }
- />
-
- }
- trailing={}
- onPress={() =>
- navigation.push("VendettaCustomPage", {
- title: "Aliucord",
- render: AliucordPage,
- })
- }
- />
- }
- trailing={}
- onPress={() =>
- navigation.push("VendettaCustomPage", {
- title: "Other Commands",
- render: OtherSettingsPage,
- })
- }
- />
-
-
- {storage.hiddenSettings?.visible && (
-
- }
- trailing={}
- onPress={() =>
- navigation.push("VendettaCustomPage", {
- title: "Hidden Settings",
- render: HiddenSettingsPage,
- })
- }
- />
-
- )}
-
-
- }
- trailing={}
- onPress={() =>
- navigation.push("VendettaCustomPage", {
- title: "Credits",
- render: CreditsPage,
- })
- }
- />
-
-
-
-
- );
-}
diff --git a/plugins/Commands/settings/components/BetterTableRowGroup.tsx b/plugins/Commands/settings/components/BetterTableRowGroup.tsx
new file mode 100644
index 0000000..42fdec7
--- /dev/null
+++ b/plugins/Commands/settings/components/BetterTableRowGroup.tsx
@@ -0,0 +1,72 @@
+import { React, ReactNative as RN, stylesheet } from "@vendetta/metro/common";
+import { semanticColors } from "@vendetta/ui";
+import { getAssetIDByName } from "@vendetta/ui/assets";
+
+export default function BetterTableRowGroup({
+ title,
+ icon,
+ children,
+ padding = false,
+}: React.PropsWithChildren<{
+ title?: string;
+ icon?: number;
+ padding?: boolean;
+}>) {
+ const styles = stylesheet.createThemedStyleSheet({
+ main: {
+ backgroundColor: semanticColors.CARD_PRIMARY_BG,
+ borderColor: semanticColors.BORDER_FAINT,
+ borderWidth: 1,
+ borderRadius: 16,
+ overflow: "hidden",
+ flex: 1,
+ maxWidth: "100%",
+ },
+ titleContainer: {
+ marginBottom: 8,
+ marginHorizontal: 16,
+ marginTop: 8,
+ gap: 4,
+ flexDirection: "row",
+ alignItems: "center",
+ },
+ icon: {
+ width: 16,
+ height: 16,
+ marginTop: 1.5,
+ tintColor: semanticColors.TEXT_MUTED,
+ },
+ titleText: {
+ fontSize: 14,
+ fontWeight: "600",
+ color: semanticColors.TEXT_MUTED,
+ },
+ wrapper: {
+ marginHorizontal: 16,
+ marginTop: 16,
+ maxWidth: "100%",
+ },
+ });
+
+ return (
+
+ {title && (
+
+ {icon && (
+
+ )}
+ {title.toUpperCase()}
+
+ )}
+
+ {padding ? (
+
+ {children}
+
+ ) : (
+ children
+ )}
+
+
+ );
+}
diff --git a/plugins/Commands/settings/components/Header.tsx b/plugins/Commands/settings/components/Header.tsx
new file mode 100644
index 0000000..bf6007d
--- /dev/null
+++ b/plugins/Commands/settings/components/Header.tsx
@@ -0,0 +1,269 @@
+import { storage } from "@vendetta/plugin";
+import { useProxy } from "@vendetta/storage";
+import { React, ReactNative as RN, stylesheet } from "@vendetta/metro/common";
+import { semanticColors } from "@vendetta/ui";
+import { getAssetIDByName } from "@vendetta/ui/assets";
+import { showToast } from "@vendetta/ui/toasts";
+import { findByProps } from "@vendetta/metro";
+
+export default function Header() {
+ const [clickCounter, setClickCounter] = React.useState(0);
+ const [clickTimeout, setClickTimeout] = React.useState(
+ null,
+ );
+ const [avatarUrl, setAvatarUrl] = React.useState(null);
+ const [isAnimatedAvatar, setIsAnimatedAvatar] = React.useState(false);
+
+ // Initialize sidebar enabled setting if not exists
+ React.useEffect(() => {
+ if (storage.sidebarEnabled === undefined) {
+ storage.sidebarEnabled = true;
+ }
+ }, []);
+
+ // Get current user from Discord's user store
+ const users = findByProps("getUser", "getCurrentUser");
+ const currentUser = users?.getCurrentUser?.();
+
+ // Get avatar URL utility
+ const { getUserAvatarURL } = findByProps("getUserAvatarURL") || {};
+ const { getDefaultAvatarURL } = findByProps("getDefaultAvatarURL") || {};
+
+ const styles = stylesheet.createThemedStyleSheet({
+ container: {
+ flexDirection: "row",
+ alignItems: "center",
+ paddingVertical: 24,
+ paddingHorizontal: 16,
+ width: "100%",
+ justifyContent: "center",
+ },
+ leftSection: {
+ flexDirection: "row",
+ alignItems: "center",
+ flex: 1,
+ gap: 20,
+ maxWidth: 500,
+ },
+ avatarContainer: {
+ position: "relative",
+ width: 96,
+ height: 96,
+ justifyContent: "center",
+ alignItems: "center",
+ },
+ avatarWrapper: {
+ width: 96,
+ height: 96,
+ borderRadius: 24,
+ backgroundColor: semanticColors.BACKGROUND_SECONDARY,
+ justifyContent: "center",
+ alignItems: "center",
+ overflow: "hidden",
+ borderWidth: 2,
+ borderColor: semanticColors.BACKGROUND_TERTIARY,
+ },
+ avatar: {
+ width: 92,
+ height: 92,
+ borderRadius: 22,
+ },
+ cogwheelBackground: {
+ position: "absolute",
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
+ borderRadius: 24,
+ justifyContent: "center",
+ alignItems: "center",
+ opacity: 0,
+ },
+ cogwheelIcon: {
+ width: 32,
+ height: 32,
+ tintColor: semanticColors.INTERACTIVE_ACTIVE,
+ },
+ textContainer: {
+ flex: 1,
+ marginLeft: 4,
+ },
+ title: {
+ fontSize: 28,
+ fontWeight: "700",
+ color: semanticColors.HEADER_PRIMARY,
+ marginBottom: 4,
+ textAlign: "left",
+ },
+ subtitle: {
+ fontSize: 14,
+ color: semanticColors.TEXT_MUTED,
+ textAlign: "left",
+ lineHeight: 20,
+ },
+ avatarPlaceholder: {
+ width: 48,
+ height: 48,
+ tintColor: semanticColors.INTERACTIVE_NORMAL,
+ },
+ animatedIndicator: {
+ position: "absolute",
+ top: 8,
+ right: 8,
+ backgroundColor: "rgba(0, 0, 0, 0.7)",
+ borderRadius: 10,
+ width: 20,
+ height: 20,
+ justifyContent: "center",
+ alignItems: "center",
+ zIndex: 1,
+ },
+ gifText: {
+ color: "#FFFFFF",
+ fontSize: 10,
+ fontWeight: "bold",
+ },
+ });
+
+ // Fetch user's avatar on component mount
+ React.useEffect(() => {
+ if (currentUser) {
+ let url;
+ let animated = false;
+
+ // Check if avatar is animated (starts with "a_")
+ const hasAnimatedAvatar = currentUser.avatar?.startsWith("a_");
+
+ // Try to get the user's custom avatar first
+ if (getUserAvatarURL && currentUser.avatar) {
+ // Use animated parameter for GIF avatars
+ url = getUserAvatarURL(currentUser, hasAnimatedAvatar);
+ animated = hasAnimatedAvatar;
+ }
+ // Fall back to default avatar
+ else if (getDefaultAvatarURL) {
+ url = getDefaultAvatarURL(currentUser);
+ }
+ // Last resort: Construct Discord CDN URL
+ else if (currentUser.avatar) {
+ const isGif = currentUser.avatar.startsWith("a_");
+ url = `https://cdn.discordapp.com/avatars/${currentUser.id}/${currentUser.avatar}.${isGif ? "gif" : "png"}?size=256`;
+ animated = isGif;
+ } else {
+ // Default Discord avatar based on discriminator or id
+ const defaultAvatarIndex = currentUser.discriminator
+ ? parseInt(currentUser.discriminator) % 5
+ : (parseInt(currentUser.id) >> 22) % 6;
+ url = `https://cdn.discordapp.com/embed/avatars/${defaultAvatarIndex}.png`;
+ }
+
+ setAvatarUrl(url);
+ setIsAnimatedAvatar(animated);
+ }
+ }, [currentUser]);
+
+ const handleAvatarPress = () => {
+ if (storage.hiddenSettings?.enabled) {
+ storage.hiddenSettings.visible = !storage.hiddenSettings.visible;
+ showToast(
+ `Hidden settings ${storage.hiddenSettings.visible ? "visible" : "hidden"}`,
+ getAssetIDByName("SettingsIcon"),
+ );
+ return;
+ }
+
+ if (clickTimeout) {
+ clearTimeout(clickTimeout);
+ }
+
+ const newTimeout = setTimeout(() => {
+ setClickCounter(0);
+ }, 1000);
+ setClickTimeout(newTimeout);
+
+ const newCounter = clickCounter + 1;
+ setClickCounter(newCounter);
+
+ if (newCounter < 10) {
+ return;
+ }
+
+ showToast("Hidden settings unlocked!", getAssetIDByName("CheckmarkIcon"));
+ storage.hiddenSettings = {
+ ...storage.hiddenSettings,
+ enabled: true,
+ visible: true,
+ };
+ setClickCounter(0);
+ };
+
+ const handleAvatarLongPress = () => {
+ const newState = !storage.sidebarEnabled;
+ storage.sidebarEnabled = newState;
+ storage.pendingRestart = true;
+ showToast(
+ `Sidebar ${newState ? "enabled" : "disabled"}. Restart Discord to apply.`,
+ getAssetIDByName(newState ? "CheckmarkIcon" : "XIcon"),
+ );
+ };
+
+ const [showCogwheel, setShowCogwheel] = React.useState(false);
+
+ return (
+
+
+ setShowCogwheel(true)}
+ onPressOut={() => setShowCogwheel(false)}
+ delayLongPress={500}
+ onLongPress={handleAvatarLongPress}
+ >
+
+ {avatarUrl ? (
+ <>
+
+ {isAnimatedAvatar && (
+
+ GIF
+
+ )}
+ >
+ ) : (
+
+ )}
+
+
+ {/* Cogwheel background overlay (appears on press) */}
+
+
+
+
+
+
+ Commands
+
+ A collection of useful commands for Vendetta like clients
+
+
+
+
+ );
+}
diff --git a/plugins/Commands/settings/exports.ts b/plugins/Commands/settings/exports.ts
new file mode 100644
index 0000000..5fb589d
--- /dev/null
+++ b/plugins/Commands/settings/exports.ts
@@ -0,0 +1,12 @@
+export { default as Header } from "./components/Header";
+export { default as BetterTableRowGroup } from "./components/BetterTableRowGroup";
+export { default as FactsSettingsPage } from "./pages/FactsSettingsPage";
+export { default as ListSettingsPage } from "./pages/ListSettingsPage";
+export { default as ImageSettingsPage } from "./pages/ImageSettingsPage";
+export { default as GaryAPIPage } from "./pages/GaryAPIPage";
+export { default as SpotifySettingsPage } from "./pages/SpotifySettingsPage";
+export { default as OtherSettingsPage } from "./pages/OtherSettingsPage";
+export { default as AliucordPage } from "./pages/AliucordPage";
+export { default as HiddenSettingsPage } from "./pages/HiddenSettingsPage";
+export { default as CreditsPage } from "./pages/CreditsPage";
+export { default as NekosLifeCategoriesPage } from "./pages/NekosLifeCategoriesPage";
diff --git a/plugins/Commands/settings/pages/AliucordPage.tsx b/plugins/Commands/settings/pages/AliucordPage.tsx
new file mode 100644
index 0000000..91db655
--- /dev/null
+++ b/plugins/Commands/settings/pages/AliucordPage.tsx
@@ -0,0 +1,76 @@
+import { storage } from "@vendetta/plugin";
+import { useProxy } from "@vendetta/storage";
+import { React, ReactNative as RN, stylesheet } from "@vendetta/metro/common";
+import { NavigationNative } from "@vendetta/metro/common";
+import { semanticColors } from "@vendetta/ui";
+import { getAssetIDByName } from "@vendetta/ui/assets";
+import { Forms } from "@vendetta/ui/components";
+import BetterTableRowGroup from "../components/BetterTableRowGroup";
+import NekosLifeCategoriesPage from "./NekosLifeCategoriesPage";
+
+const { FormSwitchRow, FormRow } = Forms;
+
+export default function AliucordPage() {
+ useProxy(storage);
+ const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
+ const navigation = NavigationNative.useNavigation();
+
+ return (
+
+
+ }
+ value={storage.enabledCommands.ip}
+ onValueChange={(v) => {
+ storage.enabledCommands.ip = v;
+ storage.pendingRestart = true;
+ forceUpdate();
+ }}
+ />
+
+
+
+ }
+ value={storage.enabledCommands.nekoslife}
+ onValueChange={(v) => {
+ storage.enabledCommands.nekoslife = v;
+ storage.pendingRestart = true;
+ forceUpdate();
+ }}
+ />
+ }
+ trailing={}
+ onPress={() =>
+ navigation.push("VendettaCustomPage", {
+ title: "NekosLife Categories",
+ render: NekosLifeCategoriesPage,
+ })
+ }
+ />
+
+
+
+
+ );
+}
diff --git a/plugins/Commands/settings/pages/CreditsPage.tsx b/plugins/Commands/settings/pages/CreditsPage.tsx
new file mode 100644
index 0000000..652ee46
--- /dev/null
+++ b/plugins/Commands/settings/pages/CreditsPage.tsx
@@ -0,0 +1,184 @@
+import { React, ReactNative as RN, stylesheet } from "@vendetta/metro/common";
+import { semanticColors } from "@vendetta/ui";
+import { getAssetIDByName } from "@vendetta/ui/assets";
+import BetterTableRowGroup from "../components/BetterTableRowGroup";
+
+export default function CreditsPage() {
+ const styles = stylesheet.createThemedStyleSheet({
+ container: {
+ flex: 1,
+ backgroundColor: semanticColors.BACKGROUND_PRIMARY,
+ },
+ creditItem: {
+ flexDirection: "row",
+ alignItems: "center",
+ paddingVertical: 16,
+ paddingHorizontal: 20,
+ backgroundColor: semanticColors.CARD_PRIMARY_BG,
+ borderRadius: 12,
+ marginHorizontal: 16,
+ marginVertical: 6,
+ },
+ avatar: {
+ width: 48,
+ height: 48,
+ borderRadius: 24,
+ marginRight: 16,
+ backgroundColor: semanticColors.BACKGROUND_SECONDARY,
+ },
+ textContainer: {
+ flex: 1,
+ },
+ commandText: {
+ fontSize: 17,
+ fontWeight: "600",
+ color: semanticColors.HEADER_PRIMARY,
+ marginBottom: 4,
+ },
+ authorText: {
+ fontSize: 15,
+ color: semanticColors.TEXT_MUTED,
+ marginBottom: 2,
+ },
+ linkText: {
+ fontSize: 13,
+ color: semanticColors.TEXT_MUTED,
+ fontWeight: "500",
+ },
+ infoText: {
+ fontSize: 14,
+ color: semanticColors.TEXT_MUTED,
+ marginBottom: 20,
+ textAlign: "center",
+ lineHeight: 20,
+ },
+ versionText: {
+ fontSize: 15,
+ color: semanticColors.TEXT_MUTED,
+ textAlign: "center",
+ fontWeight: "600",
+ lineHeight: 22,
+ },
+ });
+
+ const credits = [
+ {
+ command: "Facts Commands",
+ author: "jdev082",
+ avatar: "https://github.com/jdev082.png",
+ github: "https://github.com/jdev082",
+ },
+ {
+ command: "List Commands",
+ author: "Kitomanari",
+ avatar: "https://github.com/Kitosight.png",
+ github: "https://github.com/Kitosight",
+ },
+ {
+ command: "PetPet",
+ author: "wolfieeee",
+ avatar: "https://github.com/WolfPlugs.png",
+ github: "https://github.com/WolfPlugs",
+ },
+ {
+ command: "KonoChan Commands",
+ author: "btmc727 & Rico040",
+ avatar: "https://github.com/OTKUSteyler.png",
+ github: "https://github.com/OTKUSteyler",
+ },
+ {
+ command: "FirstMessage Command",
+ author: "sapphire",
+ avatar: "https://github.com/aeongdesu.png",
+ github: "https://github.com/aeongdesu",
+ },
+ {
+ command: "Sysinfo Command",
+ author: "mugman",
+ avatar: "https://github.com/mugman174.png",
+ github: "https://github.com/mugman174",
+ },
+ {
+ command: "Spotify Commands",
+ author: "Kitomanari",
+ avatar: "https://github.com/Kitosight.png",
+ github: "https://github.com/Kitosight",
+ },
+ {
+ command: "Gary Command",
+ author: "Zach Orange",
+ avatar: "https://github.com/IAmGaryTheCat.png",
+ github: "https://github.com/IAmGaryTheCat",
+ },
+ {
+ command: "IP & NekosLife Commands",
+ author: "scruzism",
+ avatar: "https://github.com/scrazzz.png",
+ github: "https://github.com/scrazzz",
+ },
+ {
+ command: "FriendInvites",
+ author: "nikosszzz",
+ avatar: "https://github.com/nikosszzz.png",
+ github: "https://github.com/nikosszzz",
+ },
+ ];
+
+ const handleProfilePress = (githubUrl: string) => {
+ RN.Linking.openURL(githubUrl);
+ };
+
+ return (
+
+
+
+ Thanks to this developers for creating such a nice plugins!{"\n"}
+ Tap on any developer to visit their GitHub profile.
+
+
+
+ {credits.map((credit, index) => (
+ handleProfilePress(credit.github)}
+ android_ripple={{
+ color: semanticColors.ANDROID_RIPPLE,
+ radius: 200,
+ }}
+ >
+
+
+ {credit.command}
+ by {credit.author}
+
+ {credit.github.replace("https://github.com/", "@")}
+
+
+
+ ))}
+
+
+
+ Commands Plugin Collection{"\n"}
+ Version 1.3.0
+
+
+
+ );
+}
diff --git a/plugins/Commands/settings/pages/FactsSettingsPage.tsx b/plugins/Commands/settings/pages/FactsSettingsPage.tsx
new file mode 100644
index 0000000..f6adc23
--- /dev/null
+++ b/plugins/Commands/settings/pages/FactsSettingsPage.tsx
@@ -0,0 +1,90 @@
+import { storage } from "@vendetta/plugin";
+import { useProxy } from "@vendetta/storage";
+import { React, ReactNative as RN, stylesheet } from "@vendetta/metro/common";
+import { semanticColors } from "@vendetta/ui";
+import { getAssetIDByName } from "@vendetta/ui/assets";
+import { Forms } from "@vendetta/ui/components";
+import BetterTableRowGroup from "../components/BetterTableRowGroup";
+
+const { FormSwitchRow, FormRow } = Forms;
+
+export default function FactsSettingsPage() {
+ useProxy(storage);
+ const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
+
+ return (
+
+
+
+ }
+ value={storage.factSettings.sendAsReply}
+ onValueChange={(v) => {
+ storage.factSettings.sendAsReply = v;
+ }}
+ />
+ }
+ value={storage.factSettings.includeCitation}
+ onValueChange={(v) => {
+ storage.factSettings.includeCitation = v;
+ }}
+ />
+
+
+
+ }
+ value={storage.enabledCommands.catfact}
+ onValueChange={(v) => {
+ storage.enabledCommands.catfact = v;
+ storage.pendingRestart = true;
+ forceUpdate();
+ }}
+ />
+ }
+ value={storage.enabledCommands.dogfact}
+ onValueChange={(v) => {
+ storage.enabledCommands.dogfact = v;
+ storage.pendingRestart = true;
+ forceUpdate();
+ }}
+ />
+ }
+ value={storage.enabledCommands.useless}
+ onValueChange={(v) => {
+ storage.enabledCommands.useless = v;
+ storage.pendingRestart = true;
+ forceUpdate();
+ }}
+ />
+
+
+ );
+}
diff --git a/plugins/Commands/settings/pages/GaryAPIPage.tsx b/plugins/Commands/settings/pages/GaryAPIPage.tsx
new file mode 100644
index 0000000..e398040
--- /dev/null
+++ b/plugins/Commands/settings/pages/GaryAPIPage.tsx
@@ -0,0 +1,146 @@
+import { storage } from "@vendetta/plugin";
+import { useProxy } from "@vendetta/storage";
+import { React, ReactNative as RN, stylesheet } from "@vendetta/metro/common";
+import { semanticColors } from "@vendetta/ui";
+import { getAssetIDByName } from "@vendetta/ui/assets";
+import { Forms } from "@vendetta/ui/components";
+import BetterTableRowGroup from "../components/BetterTableRowGroup";
+
+const { FormSwitchRow, FormRow } = Forms;
+
+export default function GaryAPIPage() {
+ useProxy(storage);
+ const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
+
+ const styles = stylesheet.createThemedStyleSheet({
+ container: {
+ flex: 1,
+ backgroundColor: semanticColors.BACKGROUND_PRIMARY,
+ },
+ infoText: {
+ fontSize: 14,
+ color: semanticColors.TEXT_MUTED,
+ marginBottom: 16,
+ textAlign: "center",
+ },
+ currentText: {
+ fontSize: 14,
+ color: semanticColors.TEXT_MUTED,
+ textAlign: "center",
+ fontWeight: "600",
+ },
+ });
+
+ const currentSource = storage.garySettings.imageSource;
+
+ return (
+
+
+ }
+ value={storage.enabledCommands.gary}
+ onValueChange={(v) => {
+ storage.enabledCommands.gary = v;
+ storage.pendingRestart = true;
+ forceUpdate();
+ }}
+ />
+
+
+
+
+ Choose which API the /gary command should use to fetch images.
+
+
+
+
+ }
+ value={currentSource === "gary"}
+ onValueChange={(v) => {
+ if (v) {
+ storage.garySettings.imageSource = "gary";
+ forceUpdate();
+ }
+ }}
+ />
+ }
+ value={currentSource === "catapi"}
+ onValueChange={(v) => {
+ if (v) {
+ storage.garySettings.imageSource = "catapi";
+ forceUpdate();
+ }
+ }}
+ />
+ }
+ value={currentSource === "minker"}
+ onValueChange={(v) => {
+ if (v) {
+ storage.garySettings.imageSource = "minker";
+ forceUpdate();
+ }
+ }}
+ />
+ }
+ value={currentSource === "goober"}
+ onValueChange={(v) => {
+ if (v) {
+ storage.garySettings.imageSource = "goober";
+ forceUpdate();
+ }
+ }}
+ />
+
+
+
+
+ Currently using:{" "}
+ {currentSource === "gary"
+ ? "Gary API"
+ : currentSource === "catapi"
+ ? "Cat API"
+ : currentSource === "minker"
+ ? "Minker API"
+ : currentSource === "goober"
+ ? "Goober API"
+ : "Gary API"}
+
+
+
+ );
+}
diff --git a/plugins/Commands/settings/pages/HiddenSettingsPage.tsx b/plugins/Commands/settings/pages/HiddenSettingsPage.tsx
new file mode 100644
index 0000000..20636ec
--- /dev/null
+++ b/plugins/Commands/settings/pages/HiddenSettingsPage.tsx
@@ -0,0 +1,152 @@
+import { storage } from "@vendetta/plugin";
+import { useProxy } from "@vendetta/storage";
+import { React, ReactNative as RN, stylesheet } from "@vendetta/metro/common";
+import { semanticColors } from "@vendetta/ui";
+import { getAssetIDByName } from "@vendetta/ui/assets";
+import { Forms } from "@vendetta/ui/components";
+import { alerts } from "@vendetta/ui";
+import { showToast } from "@vendetta/ui/toasts";
+import BetterTableRowGroup from "../components/BetterTableRowGroup";
+
+const { FormSwitchRow, FormRow } = Forms;
+
+export default function HiddenSettingsPage() {
+ useProxy(storage);
+ const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
+
+ const styles = stylesheet.createThemedStyleSheet({
+ container: {
+ flex: 1,
+ backgroundColor: semanticColors.BACKGROUND_PRIMARY,
+ },
+ warningText: {
+ fontSize: 14,
+ color: semanticColors.TEXT_MUTED,
+ textAlign: "center",
+ lineHeight: 20,
+ fontWeight: "600",
+ },
+ infoText: {
+ fontSize: 14,
+ color: semanticColors.TEXT_MUTED,
+ textAlign: "center",
+ lineHeight: 20,
+ marginTop: 8,
+ },
+ });
+
+ return (
+
+
+
+ These are hidden commands that may contain mature content or
+ experimental features.
+
+
+ Use at your own discretion. Commands in this section are disabled by
+ default.
+
+
+
+
+ }
+ value={storage.enabledCommands.lovefemboys}
+ onValueChange={(v) => {
+ storage.enabledCommands.lovefemboys = v;
+ storage.pendingRestart = true;
+ forceUpdate();
+ }}
+ />
+
+
+
+ }
+ value={storage.hiddenSettings.konochanBypassNsfw}
+ onValueChange={(v) => {
+ if (v) {
+ alerts.showConfirmationAlert({
+ title: "⚠️ NSFW Bypass Warning",
+ content:
+ "Enabling this allows NSFW content from KonoChan to be sent in any channel, including non-NSFW channels. This could violate server rules or Discord ToS. Use responsibly!",
+ confirmText: "I Understand",
+ cancelText: "Cancel",
+ onConfirm: () => {
+ storage.hiddenSettings.konochanBypassNsfw = true;
+ forceUpdate();
+ },
+ onCancel: () => {},
+ });
+ } else {
+ storage.hiddenSettings.konochanBypassNsfw = false;
+ forceUpdate();
+ }
+ }}
+ />
+
+
+
+ }
+ value={storage.hiddenSettings.visible}
+ onValueChange={(v) => {
+ storage.hiddenSettings.visible = v;
+ }}
+ />
+ }
+ onPress={() => {
+ alerts.showConfirmationAlert({
+ title: "Reset Hidden Settings",
+ content:
+ "This will hide the hidden settings section and disable all hidden commands. Are you sure?",
+ confirmText: "Reset",
+ onConfirm: () => {
+ storage.hiddenSettings.enabled = false;
+ storage.hiddenSettings.visible = false;
+ storage.hiddenSettings.konochanBypassNsfw = false;
+ storage.enabledCommands.lovefemboys = false;
+
+ storage.pendingRestart = true;
+ showToast(
+ "Hidden settings reset",
+ getAssetIDByName("CheckmarkIcon"),
+ );
+ },
+ cancelText: "Cancel",
+ });
+ }}
+ />
+
+
+ );
+}
diff --git a/plugins/Commands/settings/pages/ImageSettingsPage.tsx b/plugins/Commands/settings/pages/ImageSettingsPage.tsx
new file mode 100644
index 0000000..57df089
--- /dev/null
+++ b/plugins/Commands/settings/pages/ImageSettingsPage.tsx
@@ -0,0 +1,72 @@
+import { storage } from "@vendetta/plugin";
+import { useProxy } from "@vendetta/storage";
+import { React, ReactNative as RN, stylesheet } from "@vendetta/metro/common";
+import { semanticColors } from "@vendetta/ui";
+import { getAssetIDByName } from "@vendetta/ui/assets";
+import { Forms } from "@vendetta/ui/components";
+import BetterTableRowGroup from "../components/BetterTableRowGroup";
+
+const { FormSwitchRow, FormRow } = Forms;
+
+export default function ImageSettingsPage() {
+ useProxy(storage);
+ const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
+
+ return (
+
+
+
+ }
+ value={storage.enabledCommands.petpet}
+ onValueChange={(v) => {
+ storage.enabledCommands.petpet = v;
+ storage.pendingRestart = true;
+ forceUpdate();
+ }}
+ />
+
+
+
+ }
+ value={storage.enabledCommands.konoself}
+ onValueChange={(v) => {
+ storage.enabledCommands.konoself = v;
+ storage.pendingRestart = true;
+ forceUpdate();
+ }}
+ />
+ }
+ value={storage.enabledCommands.konosend}
+ onValueChange={(v) => {
+ storage.enabledCommands.konosend = v;
+ storage.pendingRestart = true;
+ forceUpdate();
+ }}
+ />
+
+
+ );
+}
diff --git a/plugins/Commands/settings/pages/ListSettingsPage.tsx b/plugins/Commands/settings/pages/ListSettingsPage.tsx
new file mode 100644
index 0000000..5acacf2
--- /dev/null
+++ b/plugins/Commands/settings/pages/ListSettingsPage.tsx
@@ -0,0 +1,85 @@
+import { storage } from "@vendetta/plugin";
+import { useProxy } from "@vendetta/storage";
+import { React, ReactNative as RN, stylesheet } from "@vendetta/metro/common";
+import { semanticColors } from "@vendetta/ui";
+import { getAssetIDByName } from "@vendetta/ui/assets";
+import { Forms } from "@vendetta/ui/components";
+import BetterTableRowGroup from "../components/BetterTableRowGroup";
+
+const { FormSwitchRow, FormRow } = Forms;
+
+export default function ListSettingsPage() {
+ useProxy(storage);
+ const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
+
+ return (
+
+
+
+ }
+ value={storage.listSettings.pluginListAlwaysDetailed}
+ onValueChange={(v) => {
+ storage.listSettings.pluginListAlwaysDetailed = v;
+ }}
+ />
+
+ }
+ value={storage.listSettings.themeListAlwaysDetailed}
+ onValueChange={(v) => {
+ storage.listSettings.themeListAlwaysDetailed = v;
+ }}
+ />
+
+
+
+
+ }
+ value={storage.enabledCommands.pluginList}
+ onValueChange={(v) => {
+ storage.enabledCommands.pluginList = v;
+ storage.pendingRestart = true;
+ forceUpdate();
+ }}
+ />
+
+ }
+ value={storage.enabledCommands.themeList}
+ onValueChange={(v) => {
+ storage.enabledCommands.themeList = v;
+ storage.pendingRestart = true;
+ forceUpdate();
+ }}
+ />
+
+
+ );
+}
diff --git a/plugins/Commands/settings/pages/NekosLifeCategoriesPage.tsx b/plugins/Commands/settings/pages/NekosLifeCategoriesPage.tsx
new file mode 100644
index 0000000..400ed2c
--- /dev/null
+++ b/plugins/Commands/settings/pages/NekosLifeCategoriesPage.tsx
@@ -0,0 +1,161 @@
+import { React, ReactNative as RN, stylesheet } from "@vendetta/metro/common";
+import { semanticColors } from "@vendetta/ui";
+import { getAssetIDByName } from "@vendetta/ui/assets";
+import BetterTableRowGroup from "../components/BetterTableRowGroup";
+
+export default function NekosLifeCategoriesPage() {
+ const styles = stylesheet.createThemedStyleSheet({
+ container: {
+ flex: 1,
+ backgroundColor: semanticColors.BACKGROUND_PRIMARY,
+ },
+ tableHeader: {
+ flexDirection: "row",
+ alignItems: "center",
+ paddingVertical: 12,
+ paddingHorizontal: 16,
+ backgroundColor: semanticColors.BACKGROUND_MODIFIER_ACCENT,
+ borderBottomWidth: 2,
+ borderBottomColor: semanticColors.BORDER_STRONG,
+ },
+ headerText: {
+ fontSize: 14,
+ fontWeight: "700",
+ color: semanticColors.HEADER_PRIMARY,
+ textTransform: "uppercase",
+ },
+ nameHeader: {
+ flex: 2,
+ },
+ codeHeader: {
+ flex: 2,
+ textAlign: "center",
+ },
+ badgeHeader: {
+ flex: 1,
+ textAlign: "center",
+ },
+ categoryItem: {
+ flexDirection: "row",
+ alignItems: "center",
+ paddingVertical: 12,
+ paddingHorizontal: 16,
+ borderBottomWidth: 1,
+ borderBottomColor: semanticColors.BORDER_FAINT,
+ },
+ categoryName: {
+ fontSize: 16,
+ fontWeight: "500",
+ color: semanticColors.HEADER_PRIMARY,
+ flex: 2,
+ },
+ categoryValue: {
+ fontSize: 14,
+ color: semanticColors.TEXT_MUTED,
+ fontFamily: "monospace",
+ flex: 2,
+ textAlign: "center",
+ },
+ sfwBadgeContainer: {
+ flex: 1,
+ alignItems: "center",
+ },
+ sfwBadge: {
+ backgroundColor: semanticColors.STATUS_POSITIVE,
+ paddingHorizontal: 8,
+ paddingVertical: 4,
+ borderRadius: 12,
+ },
+ badgeText: {
+ fontSize: 12,
+ fontWeight: "600",
+ color: semanticColors.TEXT_NORMAL,
+ },
+ infoText: {
+ fontSize: 14,
+ color: semanticColors.TEXT_MUTED,
+ textAlign: "center",
+ lineHeight: 20,
+ },
+ });
+
+ // nekos.life categories
+ const sfwCategories = [
+ { name: "Avatar", value: "avatar" },
+ { name: "Classic", value: "classic" },
+ { name: "Cuddle", value: "cuddle" },
+ { name: "Fox Girl", value: "fox_girl" },
+ { name: "Gecg", value: "gecg" },
+ { name: "Holo", value: "holo" },
+ { name: "Kemonomimi", value: "kemonomimi" },
+ { name: "Kiss", value: "kiss" },
+ { name: "Neko", value: "neko" },
+ { name: "Neko GIF", value: "ngif" },
+ { name: "Smug", value: "smug" },
+ { name: "Spank", value: "spank" },
+ { name: "Tickle", value: "tickle" },
+ { name: "Waifu", value: "waifu" },
+ { name: "Wallpaper", value: "wallpaper" },
+ { name: "Woof", value: "woof" },
+ ];
+
+ // Sort categories alphabetically
+ const sortedCategories = sfwCategories.sort((a, b) =>
+ a.name.localeCompare(b.name),
+ );
+
+ return (
+
+
+
+ Total SFW Categories: {sfwCategories.length}
+ {"\n\n"}
+ Use these category names with the /nekoslife command.{"\n"}
+ All categories are Safe For Work (SFW).
+
+
+
+
+ {/* Table Header */}
+
+
+ Category Name
+
+
+ Command Code
+
+
+ Type
+
+
+
+ {/* Table Rows */}
+ {sortedCategories.map((category, index) => (
+
+ {category.name}
+ {category.value}
+
+
+ SFW
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/plugins/Commands/settings/pages/OtherSettingsPage.tsx b/plugins/Commands/settings/pages/OtherSettingsPage.tsx
new file mode 100644
index 0000000..969135d
--- /dev/null
+++ b/plugins/Commands/settings/pages/OtherSettingsPage.tsx
@@ -0,0 +1,95 @@
+import { storage } from "@vendetta/plugin";
+import { useProxy } from "@vendetta/storage";
+import { React, ReactNative as RN, stylesheet } from "@vendetta/metro/common";
+import { semanticColors } from "@vendetta/ui";
+import { getAssetIDByName } from "@vendetta/ui/assets";
+import { Forms } from "@vendetta/ui/components";
+import BetterTableRowGroup from "../components/BetterTableRowGroup";
+
+const { FormSwitchRow, FormRow } = Forms;
+
+export default function OtherSettingsPage() {
+ useProxy(storage);
+ const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
+
+ return (
+
+
+ }
+ value={storage.enabledCommands.firstmessage}
+ onValueChange={(v) => {
+ storage.enabledCommands.firstmessage = v;
+ storage.pendingRestart = true;
+ forceUpdate();
+ }}
+ />
+
+
+
+ }
+ value={storage.enabledCommands.sysinfo}
+ onValueChange={(v) => {
+ storage.enabledCommands.sysinfo = v;
+ storage.pendingRestart = true;
+ forceUpdate();
+ }}
+ />
+
+
+
+ {
+ storage.enabledCommands.friendInviteCreate = v;
+ storage.pendingRestart = true;
+ forceUpdate();
+ }}
+ />
+ {
+ storage.enabledCommands.friendInviteView = v;
+ storage.pendingRestart = true;
+ forceUpdate();
+ }}
+ />
+ {
+ storage.enabledCommands.friendInviteRevoke = v;
+ storage.pendingRestart = true;
+ forceUpdate();
+ }}
+ />
+
+
+ );
+}
diff --git a/plugins/Commands/settings/pages/SpotifySettingsPage.tsx b/plugins/Commands/settings/pages/SpotifySettingsPage.tsx
new file mode 100644
index 0000000..a4c0cd9
--- /dev/null
+++ b/plugins/Commands/settings/pages/SpotifySettingsPage.tsx
@@ -0,0 +1,104 @@
+import { storage } from "@vendetta/plugin";
+import { useProxy } from "@vendetta/storage";
+import { React, ReactNative as RN, stylesheet } from "@vendetta/metro/common";
+import { semanticColors } from "@vendetta/ui";
+import { getAssetIDByName } from "@vendetta/ui/assets";
+import { Forms } from "@vendetta/ui/components";
+import BetterTableRowGroup from "../components/BetterTableRowGroup";
+
+const { FormSwitchRow, FormRow } = Forms;
+
+export default function SpotifySettingsPage() {
+ useProxy(storage);
+ const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
+
+ const styles = stylesheet.createThemedStyleSheet({
+ infoText: {
+ fontSize: 14,
+ color: semanticColors.TEXT_MUTED,
+ textAlign: "center",
+ lineHeight: 20,
+ },
+ });
+
+ return (
+
+
+
+ }
+ value={storage.enabledCommands.spotifyTrack}
+ onValueChange={(v) => {
+ storage.enabledCommands.spotifyTrack = v;
+ storage.pendingRestart = true;
+ forceUpdate();
+ }}
+ />
+
+ }
+ value={storage.enabledCommands.spotifyAlbum}
+ onValueChange={(v) => {
+ storage.enabledCommands.spotifyAlbum = v;
+ storage.pendingRestart = true;
+ forceUpdate();
+ }}
+ />
+
+ }
+ value={storage.enabledCommands.spotifyArtists}
+ onValueChange={(v) => {
+ storage.enabledCommands.spotifyArtists = v;
+ storage.pendingRestart = true;
+ forceUpdate();
+ }}
+ />
+
+ }
+ value={storage.enabledCommands.spotifyCover}
+ onValueChange={(v) => {
+ storage.enabledCommands.spotifyCover = v;
+ storage.pendingRestart = true;
+ forceUpdate();
+ }}
+ />
+
+
+
+
+ These commands allow you to share your current Spotify activity in
+ Discord. Make sure you have Spotify connected to Discord for these
+ commands to work properly.
+
+
+
+ );
+}
diff --git a/plugins/Commands/settings/settings.tsx b/plugins/Commands/settings/settings.tsx
new file mode 100644
index 0000000..7568ca3
--- /dev/null
+++ b/plugins/Commands/settings/settings.tsx
@@ -0,0 +1,264 @@
+import { storage } from "@vendetta/plugin";
+import { useProxy } from "@vendetta/storage";
+import { React, ReactNative, stylesheet } from "@vendetta/metro/common";
+import { NavigationNative } from "@vendetta/metro/common";
+import { semanticColors } from "@vendetta/ui";
+import { getAssetIDByName } from "@vendetta/ui/assets";
+import { Forms } from "@vendetta/ui/components";
+import { alerts } from "@vendetta/ui";
+import { showToast } from "@vendetta/ui/toasts";
+import Header from "./components/Header";
+import BetterTableRowGroup from "./components/BetterTableRowGroup";
+
+// Import all pages directly
+import FactsSettingsPage from "./pages/FactsSettingsPage";
+import ListSettingsPage from "./pages/ListSettingsPage";
+import ImageSettingsPage from "./pages/ImageSettingsPage";
+import GaryAPIPage from "./pages/GaryAPIPage";
+import SpotifySettingsPage from "./pages/SpotifySettingsPage";
+import AliucordPage from "./pages/AliucordPage";
+import OtherSettingsPage from "./pages/OtherSettingsPage";
+import HiddenSettingsPage from "./pages/HiddenSettingsPage";
+import CreditsPage from "./pages/CreditsPage";
+
+const { FormRow } = Forms;
+
+// Initialize storage with default values
+storage.factSettings ??= {
+ sendAsReply: true,
+ includeCitation: false,
+};
+
+storage.listSettings ??= {
+ pluginListAlwaysDetailed: false,
+ themeListAlwaysDetailed: false,
+};
+
+storage.garySettings ??= {
+ imageSource: "gary",
+};
+
+storage.enabledCommands ??= {
+ catfact: true,
+ dogfact: true,
+ useless: true,
+ petpet: true,
+ pluginList: true,
+ themeList: true,
+ konoself: true,
+ konosend: true,
+ firstmessage: true,
+ sysinfo: true,
+ spotifyTrack: true,
+ spotifyAlbum: true,
+ spotifyArtists: true,
+ spotifyCover: true,
+ gary: true,
+ ip: true,
+ lovefemboys: false,
+ nekoslife: false,
+ friendInviteCreate: true,
+ friendInviteView: true,
+ friendInviteRevoke: true,
+};
+
+storage.pendingRestart ??= false;
+
+// Hidden settings storage with NSFW bypass option
+storage.hiddenSettings ??= {
+ enabled: false,
+ visible: false,
+ konochanBypassNsfw: false,
+};
+
+// Sidebar setting (enabled by default)
+storage.sidebarEnabled ??= true;
+
+// MAIN SETTINGS COMPONENT
+export default function Settings() {
+ useProxy(storage);
+ const navigation = NavigationNative.useNavigation();
+
+ React.useEffect(() => {
+ return () => {
+ if (storage.pendingRestart) {
+ storage.pendingRestart = false;
+ alerts.showConfirmationAlert({
+ title: "Restart Required",
+ content:
+ "You have made changes to commands. Please restart Discord to apply these changes.",
+ confirmText: "Okay",
+ cancelText: null,
+ onConfirm: () => {},
+ });
+ }
+ };
+ }, []);
+
+ return (
+
+
+
+
+
+ }
+ trailing={}
+ onPress={() =>
+ navigation.push("VendettaCustomPage", {
+ title: "Facts Commands",
+ render: FactsSettingsPage,
+ })
+ }
+ />
+ }
+ trailing={}
+ onPress={() =>
+ navigation.push("VendettaCustomPage", {
+ title: "List Commands",
+ render: ListSettingsPage,
+ })
+ }
+ />
+ }
+ trailing={}
+ onPress={() =>
+ navigation.push("VendettaCustomPage", {
+ title: "Image Commands",
+ render: ImageSettingsPage,
+ })
+ }
+ />
+ }
+ trailing={}
+ onPress={() =>
+ navigation.push("VendettaCustomPage", {
+ title: "Gary Commands",
+ render: GaryAPIPage,
+ })
+ }
+ />
+
+ }
+ trailing={}
+ onPress={() =>
+ navigation.push("VendettaCustomPage", {
+ title: "Spotify Commands",
+ render: SpotifySettingsPage,
+ })
+ }
+ />
+
+ }
+ trailing={}
+ onPress={() =>
+ navigation.push("VendettaCustomPage", {
+ title: "Aliucord",
+ render: AliucordPage,
+ })
+ }
+ />
+ }
+ trailing={}
+ onPress={() =>
+ navigation.push("VendettaCustomPage", {
+ title: "Other Commands",
+ render: OtherSettingsPage,
+ })
+ }
+ />
+
+
+ {storage.hiddenSettings?.visible && (
+
+ }
+ trailing={}
+ onPress={() =>
+ navigation.push("VendettaCustomPage", {
+ title: "Hidden Settings",
+ render: HiddenSettingsPage,
+ })
+ }
+ />
+
+ )}
+
+
+ }
+ trailing={}
+ onPress={() =>
+ navigation.push("VendettaCustomPage", {
+ title: "Credits",
+ render: CreditsPage,
+ })
+ }
+ />
+
+
+ {/* Add sufficient bottom padding for scrolling */}
+
+
+
+ );
+}
diff --git a/plugins/Commands/settings/types/index.ts b/plugins/Commands/settings/types/index.ts
new file mode 100644
index 0000000..951beb9
--- /dev/null
+++ b/plugins/Commands/settings/types/index.ts
@@ -0,0 +1,52 @@
+export interface FactSettings {
+ sendAsReply: boolean;
+ includeCitation: boolean;
+}
+
+export interface ListSettings {
+ pluginListAlwaysDetailed: boolean;
+ themeListAlwaysDetailed: boolean;
+}
+
+export interface GarySettings {
+ imageSource: "gary" | "catapi" | "minker" | "goober";
+}
+
+export interface EnabledCommands {
+ catfact: boolean;
+ dogfact: boolean;
+ useless: boolean;
+ petpet: boolean;
+ pluginList: boolean;
+ themeList: boolean;
+ konoself: boolean;
+ konosend: boolean;
+ firstmessage: boolean;
+ sysinfo: boolean;
+ spotifyTrack: boolean;
+ spotifyAlbum: boolean;
+ spotifyArtists: boolean;
+ spotifyCover: boolean;
+ gary: boolean;
+ ip: boolean;
+ lovefemboys: boolean;
+ nekoslife: boolean;
+ friendInviteCreate: boolean;
+ friendInviteView: boolean;
+ friendInviteRevoke: boolean;
+}
+
+export interface HiddenSettings {
+ enabled: boolean;
+ visible: boolean;
+ konochanBypassNsfw: boolean;
+}
+
+export interface StorageType {
+ factSettings: FactSettings;
+ listSettings: ListSettings;
+ garySettings: GarySettings;
+ enabledCommands: EnabledCommands;
+ pendingRestart: boolean;
+ hiddenSettings: HiddenSettings;
+}
diff --git a/plugins/Commands/sidebar.tsx b/plugins/Commands/sidebar.tsx
new file mode 100644
index 0000000..fb693da
--- /dev/null
+++ b/plugins/Commands/sidebar.tsx
@@ -0,0 +1,235 @@
+import { storage } from "@vendetta/plugin";
+import { React, NavigationNative } from "@vendetta/metro/common";
+import { getAssetIDByName } from "@vendetta/ui/assets";
+import { after } from "@vendetta/patcher";
+import { Forms } from "@vendetta/ui/components";
+import { findInReactTree } from "@vendetta/utils";
+import { findByProps } from "@vendetta/metro";
+import { logger } from "@vendetta";
+import Settings from "./settings/settings";
+
+const { FormSection, FormRow } = Forms;
+const { TableRowIcon } = findByProps("TableRowIcon");
+const bunny = window.bunny;
+declare global {
+ interface Window {
+ bunny: any;
+ }
+}
+
+const tabsNavigationRef = bunny?.metro?.findByPropsLazy("getRootNavigationRef");
+const settingConstants = bunny?.metro?.findByPropsLazy(
+ "SETTING_RENDERER_CONFIG",
+);
+const createListModule = bunny?.metro?.findByPropsLazy("createList");
+const SettingsOverviewScreen = bunny?.metro?.findByNameLazy(
+ "SettingsOverviewScreen",
+ false,
+);
+
+function Section({ tabs }) {
+ const navigation = NavigationNative.useNavigation();
+
+ return React.createElement(FormRow, {
+ label: tabs.title(),
+ leading: React.createElement(FormRow.Icon, { source: tabs.icon }),
+ trailing: React.createElement(React.Fragment, {}, [
+ tabs.trailing ? tabs.trailing() : null,
+ React.createElement(FormRow.Arrow, { key: "arrow" }),
+ ]),
+ onPress: () => {
+ const Component = tabs.page;
+ navigation.navigate("VendettaCustomPage", {
+ title: tabs.title(),
+ render: () => React.createElement(Component),
+ });
+ },
+ });
+}
+
+function patchPanelUI(tabs, patches) {
+ try {
+ patches.push(
+ after(
+ "default",
+ bunny?.metro?.findByPropsLazy(["renderTitle", "sections"], false),
+ (_, ret) => {
+ const UserSettingsOverview = findInReactTree(
+ ret.props.children,
+ (n) => n.type?.name === "UserSettingsOverview",
+ );
+
+ if (UserSettingsOverview) {
+ patches.push(
+ after(
+ "render",
+ UserSettingsOverview.type.prototype,
+ (_args, res) => {
+ const sections = findInReactTree(
+ res.props.children,
+ (n) => n?.children?.[1]?.type === FormSection,
+ )?.children;
+
+ if (sections) {
+ const index = sections.findIndex((c) =>
+ ["BILLING_SETTINGS", "PREMIUM_SETTINGS"].includes(
+ c?.props?.label,
+ ),
+ );
+
+ sections.splice(
+ -~index || 4,
+ 0,
+ React.createElement(Section, { key: tabs.key, tabs }),
+ );
+ }
+ },
+ ),
+ );
+ }
+ },
+ true,
+ ),
+ );
+ } catch (error) {
+ logger.info("Panel UI patch failed graciously 💔", error);
+ }
+}
+
+function patchTabsUI(tabs, patches) {
+ if (!settingConstants || !tabsNavigationRef) {
+ console.warn(
+ "[Commands Plugin] Missing required constants for tabs UI patch",
+ );
+ return;
+ }
+
+ const row = {
+ [tabs.key]: {
+ type: "pressable",
+ title: tabs.title,
+ icon: tabs.icon,
+ IconComponent:
+ tabs.icon &&
+ (() => React.createElement(TableRowIcon, { source: tabs.icon })),
+ usePredicate: tabs.predicate,
+ useTrailing: tabs.trailing,
+ onPress: () => {
+ const navigation = tabsNavigationRef.getRootNavigationRef();
+ const Component = tabs.page;
+
+ navigation.navigate("VendettaCustomPage", {
+ title: tabs.title(),
+ render: () => React.createElement(Component),
+ });
+ },
+ withArrow: true,
+ },
+ };
+
+ let rendererConfigValue = settingConstants.SETTING_RENDERER_CONFIG;
+
+ Object.defineProperty(settingConstants, "SETTING_RENDERER_CONFIG", {
+ enumerable: true,
+ configurable: true,
+ get: () => ({
+ ...rendererConfigValue,
+ ...row,
+ }),
+ set: (v) => (rendererConfigValue = v),
+ });
+
+ const firstRender = Symbol("pinToSettings meow meow");
+
+ try {
+ if (!createListModule) return;
+ patches.push(
+ after("createList", createListModule, function (args, ret) {
+ if (!args[0][firstRender]) {
+ args[0][firstRender] = true;
+
+ const [config] = args;
+ const sections = config.sections;
+
+ const section = sections?.find((x: any) =>
+ ["Bunny", "Revenge", "Kettu", "Vencore", "ShiggyCord"].some(
+ (mod) => x.label === mod && x.title === mod,
+ ),
+ );
+
+ if (section?.settings) {
+ section.settings = [...section.settings, tabs.key];
+ }
+ }
+ }),
+ );
+ } catch {
+ if (!SettingsOverviewScreen) return;
+ patches.push(
+ after("default", SettingsOverviewScreen, (args, ret) => {
+ if (!args[0][firstRender]) {
+ args[0][firstRender] = true;
+
+ const { sections } = findInReactTree(
+ ret,
+ (i) => i.props?.sections,
+ ).props;
+ const section = sections?.find((x: any) =>
+ ["Bunny", "Revenge", "Kettu", "Vencore", "ShiggyCord"].some(
+ (mod) => x.label === mod && x.title === mod,
+ ),
+ );
+
+ if (section?.settings) {
+ section.settings = [...section.settings, tabs.key];
+ }
+ }
+ }),
+ );
+ }
+}
+
+function patchSettingsPin(tabs) {
+ const patches = [];
+
+ let disabled = false;
+
+ const realPredicate = tabs.predicate || (() => true);
+ tabs.predicate = () => (disabled ? false : realPredicate());
+
+ patchPanelUI(tabs, patches);
+ patchTabsUI(tabs, patches);
+ patches.push(() => (disabled = true));
+
+ return () => {
+ for (const x of patches) {
+ x();
+ }
+ };
+}
+
+export default function patchSidebar() {
+ // Check if sidebar is enabled in storage
+ if (storage.sidebarEnabled === false) {
+ console.log("[Commands Plugin] Sidebar disabled in settings");
+ return () => {};
+ }
+
+ console.log("[Commands Plugin] Patching sidebar...");
+
+ try {
+ const unpatch = patchSettingsPin({
+ key: "CommandsPlugin",
+ icon: getAssetIDByName("SettingsIcon"),
+ title: () => "Commands",
+ predicate: () => storage.sidebarEnabled !== false,
+ page: Settings,
+ });
+
+ console.log("[Commands Plugin] Successfully patched sidebar");
+ return unpatch;
+ } catch (error) {
+ console.error("[Commands Plugin] Failed to patch sidebar:", error);
+ return () => {};
+ }
+}