From 22bbcbf91d7fd97e060e67190f0b3731e65a5aea Mon Sep 17 00:00:00 2001 From: kmmiio99o <164557248+kmmiio99o@users.noreply.github.com> Date: Tue, 9 Dec 2025 22:28:25 +0100 Subject: [PATCH] feat: Re-do Commands plugin settings page - Re-do fully the Commands plugin settings page - Add sidebar based of Multi Scrobbler one - Rework header (because why not) --- plugins/Commands/index.ts | 53 +- plugins/Commands/settings.tsx | 1455 ----------------- .../components/BetterTableRowGroup.tsx | 72 + .../Commands/settings/components/Header.tsx | 269 +++ plugins/Commands/settings/exports.ts | 12 + .../Commands/settings/pages/AliucordPage.tsx | 76 + .../Commands/settings/pages/CreditsPage.tsx | 184 +++ .../settings/pages/FactsSettingsPage.tsx | 90 + .../Commands/settings/pages/GaryAPIPage.tsx | 146 ++ .../settings/pages/HiddenSettingsPage.tsx | 152 ++ .../settings/pages/ImageSettingsPage.tsx | 72 + .../settings/pages/ListSettingsPage.tsx | 85 + .../pages/NekosLifeCategoriesPage.tsx | 161 ++ .../settings/pages/OtherSettingsPage.tsx | 95 ++ .../settings/pages/SpotifySettingsPage.tsx | 104 ++ plugins/Commands/settings/settings.tsx | 264 +++ plugins/Commands/settings/types/index.ts | 52 + plugins/Commands/sidebar.tsx | 235 +++ 18 files changed, 2119 insertions(+), 1458 deletions(-) delete mode 100644 plugins/Commands/settings.tsx create mode 100644 plugins/Commands/settings/components/BetterTableRowGroup.tsx create mode 100644 plugins/Commands/settings/components/Header.tsx create mode 100644 plugins/Commands/settings/exports.ts create mode 100644 plugins/Commands/settings/pages/AliucordPage.tsx create mode 100644 plugins/Commands/settings/pages/CreditsPage.tsx create mode 100644 plugins/Commands/settings/pages/FactsSettingsPage.tsx create mode 100644 plugins/Commands/settings/pages/GaryAPIPage.tsx create mode 100644 plugins/Commands/settings/pages/HiddenSettingsPage.tsx create mode 100644 plugins/Commands/settings/pages/ImageSettingsPage.tsx create mode 100644 plugins/Commands/settings/pages/ListSettingsPage.tsx create mode 100644 plugins/Commands/settings/pages/NekosLifeCategoriesPage.tsx create mode 100644 plugins/Commands/settings/pages/OtherSettingsPage.tsx create mode 100644 plugins/Commands/settings/pages/SpotifySettingsPage.tsx create mode 100644 plugins/Commands/settings/settings.tsx create mode 100644 plugins/Commands/settings/types/index.ts create mode 100644 plugins/Commands/sidebar.tsx 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 () => {}; + } +}