diff --git a/plugins/Commands/index.ts b/plugins/Commands/index.ts index d51f42c..8edbfb1 100644 --- a/plugins/Commands/index.ts +++ b/plugins/Commands/index.ts @@ -17,6 +17,7 @@ import { spotifyCoverCommand } from "./src/commands/spotify"; import { garyCommand } from "./src/commands/gary"; +import { lovefemboysCommand } from "./src/commands/lovefemboys"; import settings from "./settings"; // Initialize storage with default values @@ -57,6 +58,15 @@ if (!storage.enabledCommands) { spotifyArtists: true, spotifyCover: true, gary: true, + lovefemboys: false, // Hidden command - disabled by default + }; +} + +// Initialize hidden settings storage +if (!storage.hiddenSettings) { + storage.hiddenSettings = { + enabled: false, + visible: false, }; } @@ -77,6 +87,7 @@ const commandMap = { spotifyArtists: spotifyArtistsCommand, spotifyCover: spotifyCoverCommand, gary: garyCommand, + lovefemboys: lovefemboysCommand, // Add hidden command to map }; // Store registered commands for cleanup @@ -90,6 +101,7 @@ export default { for (const [key, command] of Object.entries(commandMap)) { if (storage.enabledCommands[key]) { commands.push(registerCommand(command)); + console.log(`[All-In-One Commands] Registered command: ${key}`); } } diff --git a/plugins/Commands/settings.tsx b/plugins/Commands/settings.tsx index 3c31aff..2d2a7b4 100644 --- a/plugins/Commands/settings.tsx +++ b/plugins/Commands/settings.tsx @@ -6,6 +6,7 @@ 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; @@ -41,20 +42,29 @@ storage.enabledCommands ??= { spotifyArtists: true, spotifyCover: true, gary: true, + lovefemboys: false, // Hidden command - disabled by default }; storage.pendingRestart ??= false; +// Hidden settings storage +storage.hiddenSettings ??= { + enabled: false, + visible: false, +}; + // Better Table Row Group Component function BetterTableRowGroup({ title, icon, children, padding = false, + onTitlePress, }: React.PropsWithChildren<{ title?: string; icon?: number; padding?: boolean; + onTitlePress?: () => void; }>) { const styles = stylesheet.createThemedStyleSheet({ main: { @@ -73,6 +83,11 @@ function BetterTableRowGroup({ flexDirection: "row", alignItems: "center", }, + titlePressable: { + flexDirection: "row", + alignItems: "center", + gap: 4, + }, icon: { width: 16, height: 16, @@ -90,16 +105,33 @@ function BetterTableRowGroup({ {title && ( - {icon && ( - + {onTitlePress ? ( + + {icon && ( + + )} + + {title.toUpperCase()} + + + ) : ( + <> + {icon && ( + + )} + + {title.toUpperCase()} + + )} - - {title.toUpperCase()} - )} @@ -113,8 +145,11 @@ function BetterTableRowGroup({ ); } -// Header Component +// Header Component with SILENT secret unlock function Header() { + const [clickCounter, setClickCounter] = React.useState(0); + const [clickTimeout, setClickTimeout] = React.useState(null); + const styles = stylesheet.createThemedStyleSheet({ container: { flexDirection: "column", @@ -146,20 +181,142 @@ function Header() { }, }); + const handleIconPress = () => { + 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); + + // Silent counting - no progress notifications + if (newCounter < 10) { + return; // Just count silently + } + + // Unlocked! Only show this notification + showToast("🔓 Hidden settings unlocked!", getAssetIDByName("CheckmarkIcon")); + storage.hiddenSettings.enabled = true; + storage.hiddenSettings.visible = true; + setClickCounter(0); + }; + return ( - + - + Commands A collection of useful commands ); } +// Hidden Commands Settings Page +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_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.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.enabledCommands.lovefemboys = false; + storage.pendingRestart = true; + showToast("Hidden settings reset", getAssetIDByName("CheckmarkIcon")); + }, + cancelText: "Cancel", + }); + }} + /> + + + + ); +} + // Facts Commands Settings Page - FIXED with local force update function FactsSettingsPage() { useProxy(storage); @@ -198,7 +355,7 @@ function FactsSettingsPage() { onValueChange={(v) => { storage.enabledCommands.catfact = v; storage.pendingRestart = true; - forceUpdate(); // Force this page to re-render + forceUpdate(); }} /> { storage.enabledCommands.dogfact = v; storage.pendingRestart = true; - forceUpdate(); // Force this page to re-render + forceUpdate(); }} /> { storage.enabledCommands.useless = v; storage.pendingRestart = true; - forceUpdate(); // Force this page to re-render + forceUpdate(); }} /> @@ -267,7 +424,7 @@ function GaryAPIPage() { onValueChange={(v) => { storage.enabledCommands.gary = v; storage.pendingRestart = true; - forceUpdate(); // Force this page to re-render + forceUpdate(); }} /> @@ -287,7 +444,7 @@ function GaryAPIPage() { onValueChange={(v) => { if (v) { storage.garySettings.imageSource = "gary"; - forceUpdate(); // Force this page to re-render + forceUpdate(); } }} /> @@ -299,7 +456,7 @@ function GaryAPIPage() { onValueChange={(v) => { if (v) { storage.garySettings.imageSource = "catapi"; - forceUpdate(); // Force this page to re-render + forceUpdate(); } }} /> @@ -311,7 +468,7 @@ function GaryAPIPage() { onValueChange={(v) => { if (v) { storage.garySettings.imageSource = "minker"; - forceUpdate(); // Force this page to re-render + forceUpdate(); } }} /> @@ -323,7 +480,7 @@ function GaryAPIPage() { onValueChange={(v) => { if (v) { storage.garySettings.imageSource = "goober"; - forceUpdate(); // Force this page to re-render + forceUpdate(); } }} /> @@ -382,7 +539,7 @@ function ListSettingsPage() { onValueChange={(v) => { storage.enabledCommands.pluginList = v; storage.pendingRestart = true; - forceUpdate(); // Force this page to re-render + forceUpdate(); }} /> { storage.enabledCommands.themeList = v; storage.pendingRestart = true; - forceUpdate(); // Force this page to re-render + forceUpdate(); }} /> @@ -419,7 +576,7 @@ function ImageSettingsPage() { onValueChange={(v) => { storage.enabledCommands.petpet = v; storage.pendingRestart = true; - forceUpdate(); // Force this page to re-render + forceUpdate(); }} /> @@ -433,7 +590,7 @@ function ImageSettingsPage() { onValueChange={(v) => { storage.enabledCommands.konoself = v; storage.pendingRestart = true; - forceUpdate(); // Force this page to re-render + forceUpdate(); }} /> { storage.enabledCommands.konosend = v; storage.pendingRestart = true; - forceUpdate(); // Force this page to re-render + forceUpdate(); }} /> @@ -470,7 +627,7 @@ function SpotifySettingsPage() { onValueChange={(v) => { storage.enabledCommands.spotifyTrack = v; storage.pendingRestart = true; - forceUpdate(); // Force this page to re-render + forceUpdate(); }} /> { storage.enabledCommands.spotifyAlbum = v; storage.pendingRestart = true; - forceUpdate(); // Force this page to re-render + forceUpdate(); }} /> { storage.enabledCommands.spotifyArtists = v; storage.pendingRestart = true; - forceUpdate(); // Force this page to re-render + forceUpdate(); }} /> { storage.enabledCommands.spotifyCover = v; storage.pendingRestart = true; - forceUpdate(); // Force this page to re-render + forceUpdate(); }} /> @@ -540,7 +697,7 @@ function OtherSettingsPage() { onValueChange={(v) => { storage.enabledCommands.firstmessage = v; storage.pendingRestart = true; - forceUpdate(); // Force this page to re-render + forceUpdate(); }} /> @@ -554,7 +711,7 @@ function OtherSettingsPage() { onValueChange={(v) => { storage.enabledCommands.sysinfo = v; storage.pendingRestart = true; - forceUpdate(); // Force this page to re-render + forceUpdate(); }} /> @@ -722,7 +879,7 @@ function CreditsPage() { ); } -// MAIN SETTINGS COMPONENT +// MAIN SETTINGS COMPONENT with hidden settings export default function Settings() { useProxy(storage); const navigation = NavigationNative.useNavigation(); @@ -814,6 +971,22 @@ export default function Settings() { /> + {/* Hidden Settings Section - Only visible when unlocked */} + {storage.hiddenSettings?.enabled && storage.hiddenSettings?.visible && ( + + } + trailing={} + onPress={() => navigation.push("VendettaCustomPage", { + title: "Hidden Settings", + render: HiddenSettingsPage, + })} + /> + + )} + {/* More Options */} ; + }; + url_overridden_by_dest?: string; + }; +} + +interface RedditResponse { + data: { + children: RedditPost[]; + }; +} + +async function getFemboyImage(includeNsfw: boolean = false): Promise { + try { + const response = await fetch('https://reddit.com/r/femboys.json'); + if (!response.ok) { + console.log('[LoveFemboys] Failed to fetch from Reddit API'); + return null; + } + + const data: RedditResponse = await response.json(); + + // Filter posts that are images and match NSFW preference + const imagePosts = data.data.children.filter(post => { + const postData = post.data; + + // Skip videos + if (postData.is_video) return false; + + // Check NSFW filter + if (!includeNsfw && postData.over_18) return false; + + // Check if it's an image URL + const url = postData.url_overridden_by_dest || postData.url; + return url && ( + url.includes('.jpg') || + url.includes('.jpeg') || + url.includes('.png') || + url.includes('.gif') || + url.includes('i.redd.it') || + url.includes('i.imgur.com') + ); + }); + + if (imagePosts.length === 0) { + console.log('[LoveFemboys] No suitable images found'); + return null; + } + + // Get random image + const randomPost = imagePosts[Math.floor(Math.random() * imagePosts.length)]; + const imageUrl = randomPost.data.url_overridden_by_dest || randomPost.data.url; + + // Handle Reddit preview images + if (imageUrl.includes('reddit.com') && randomPost.data.preview?.images?.[0]?.source?.url) { + return randomPost.data.preview.images[0].source.url.replace(/&/g, '&'); + } + + return imageUrl; + } catch (error) { + console.error('[LoveFemboys] Error fetching image:', error); + return null; + } +} + +export const lovefemboysCommand = { + name: "lovefemboys", + displayName: "lovefemboys", + description: "Get a random femboy image from r/femboys", + displayDescription: "Get a random femboy image from r/femboys", + options: [ + { + name: "nsfw", + displayName: "nsfw", + description: "Include NSFW images", + displayDescription: "Include NSFW images", + type: 5, // Boolean + required: false, + }, + { + name: "ephemeral", + displayName: "ephemeral", + description: "Send as ephemeral message (only you can see)", + displayDescription: "Send as ephemeral message (only you can see)", + type: 5, // Boolean + required: false, + } + ], + execute: async (args: any, ctx: any) => { + try { + const includeNsfw = args.find((arg: any) => arg.name === "nsfw")?.value || false; + const isEphemeral = args.find((arg: any) => arg.name === "ephemeral")?.value || false; + + const imageUrl = await getFemboyImage(includeNsfw); + + if (!imageUrl) { + const errorMessage = "❌ Failed to fetch femboy image. Try again later!"; + + if (isEphemeral) { + return { + type: 4, + data: { + content: errorMessage, + flags: 64, // Ephemeral flag + }, + }; + } else { + const fixNonce = Date.now().toString(); + MessageActions.sendMessage(ctx.channel.id, { content: errorMessage }, void 0, { + nonce: fixNonce, + }); + return { type: 4 }; + } + } + + const content = `💖 **Femboy Image** ${includeNsfw ? '(NSFW included)' : '(SFW only)'}\n${imageUrl}`; + + if (isEphemeral) { + return { + type: 4, + data: { + content, + flags: 64, // Ephemeral flag + }, + }; + } else { + const fixNonce = Date.now().toString(); + MessageActions.sendMessage(ctx.channel.id, { content }, void 0, { + nonce: fixNonce, + }); + return { type: 4 }; + } + } catch (error) { + console.error('[LoveFemboys] Command error:', error); + const errorMessage = "❌ An error occurred while fetching femboy image."; + + const isEphemeral = args.find((arg: any) => arg.name === "ephemeral")?.value || false; + + if (isEphemeral) { + return { + type: 4, + data: { + content: errorMessage, + flags: 64, // Ephemeral flag + }, + }; + } else { + const fixNonce = Date.now().toString(); + MessageActions.sendMessage(ctx.channel.id, { content: errorMessage }, void 0, { + nonce: fixNonce, + }); + return { type: 4 }; + } + } + }, + applicationId: "-1", + inputType: 1, + type: 1, +};