vd-plugins/plugins/Commands/settings/components/Header.tsx

294 lines
10 KiB
TypeScript
Raw Normal View History

import { storage } from "@vendetta/plugin";
import { React, ReactNative as RN } from "@vendetta/metro/common";
import { semanticColors } from "@vendetta/ui";
import { getAssetIDByName } from "@vendetta/ui/assets";
import { showToast } from "@vendetta/ui/toasts";
import { findByProps, findByStoreName } from "@vendetta/metro";
const TextStyleSheet = findByProps("TextStyleSheet").TextStyleSheet;
const ThemeStore = findByStoreName("ThemeStore");
const colorModule = findByProps("colors", "unsafe_rawColors");
const colorResolver = colorModule?.internal ?? colorModule?.meta;
function resolveSemanticColor(color: any, theme?: string) {
const t = theme ?? ThemeStore?.theme;
return (color && colorResolver?.resolveSemanticColor(t, color)) || "#000000";
}
export default function Header() {
2026-05-09 10:20:55 +02:00
const [clickCounter, setClickCounter] = React.useState(0);
const [clickTimeout, setClickTimeout] = React.useState<NodeJS.Timeout | null>(
null,
);
const [avatarUrl, setAvatarUrl] = React.useState<string | null>(null);
const [isAnimatedAvatar, setIsAnimatedAvatar] = React.useState(false);
2026-05-09 10:20:55 +02:00
// Initialize sidebar enabled setting if not exists
React.useEffect(() => {
if (storage.sidebarEnabled === undefined) {
storage.sidebarEnabled = true;
}
}, []);
// Get current user from Discord's user store with error handling
let currentUser = null;
try {
const users = findByProps("getUser", "getCurrentUser");
currentUser = users?.getCurrentUser?.();
} catch (e) {
console.warn("[Commands Header] Failed to get current user:", e);
}
// Get avatar URL utilities with error handling
let getUserAvatarURL = null;
let getDefaultAvatarURL = null;
try {
const userAvatarModule = findByProps("getUserAvatarURL");
getUserAvatarURL = userAvatarModule?.getUserAvatarURL;
} catch (e) {
console.warn("[Commands Header] Failed to get getUserAvatarURL:", e);
}
try {
const defaultAvatarModule = findByProps("getDefaultAvatarURL");
getDefaultAvatarURL = defaultAvatarModule?.getDefaultAvatarURL;
} catch (e) {
console.warn("[Commands Header] Failed to get getDefaultAvatarURL:", e);
}
const styles = RN.StyleSheet.create({
2026-05-09 10:20:55 +02:00
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,
justifyContent: "center",
alignItems: "center",
overflow: "hidden",
borderWidth: 2,
borderColor: semanticColors.MOBILE_CHATINPUT_BORDER_DEFAULT,
2026-05-09 10:20:55 +02:00
},
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,
justifyContent: "center",
},
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",
},
avatarPlaceholder: {
width: 48,
height: 48,
tintColor: semanticColors.INTERACTIVE_NORMAL,
},
});
2026-05-09 10:20:55 +02:00
// Fetch user's avatar on component mount
React.useEffect(() => {
if (currentUser) {
let url;
let animated = false;
2026-05-09 10:20:55 +02:00
// Check if avatar is animated (starts with "a_")
const hasAnimatedAvatar = currentUser.avatar?.startsWith("a_");
2026-05-09 10:20:55 +02:00
// Try to get the user's custom avatar first
if (getUserAvatarURL && currentUser.avatar) {
try {
// Use animated parameter for GIF avatars
url = getUserAvatarURL(currentUser, hasAnimatedAvatar);
animated = hasAnimatedAvatar;
} catch (e) {
console.warn("[Commands Header] Error getting custom avatar:", e);
url = null;
}
2026-05-09 10:20:55 +02:00
}
// Fall back to default avatar
if (!url && getDefaultAvatarURL && typeof getDefaultAvatarURL === "function") {
2026-05-09 10:20:55 +02:00
try {
url = getDefaultAvatarURL(currentUser);
} catch (error) {
console.warn("[Commands Header] Error getting default avatar URL:", error);
2026-05-09 10:20:55 +02:00
url = null;
}
}
// Last resort: Construct Discord CDN URL
if (!url && currentUser.avatar) {
2026-05-09 10:20:55 +02:00
const isGif = currentUser.avatar.startsWith("a_");
url = `https://cdn.discordapp.com/avatars/${currentUser.id}/${currentUser.avatar}.${isGif ? "gif" : "png"}?size=256`;
animated = isGif;
} else if (!url) {
2026-05-09 10:20:55 +02:00
// 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`;
}
2026-05-09 10:20:55 +02:00
setAvatarUrl(url);
setIsAnimatedAvatar(animated);
}
}, [currentUser]);
2026-05-09 10:20:55 +02:00
const handleAvatarPress = () => {
if (storage.hiddenSettings?.enabled) {
storage.hiddenSettings.visible = !storage.hiddenSettings.visible;
showToast(
`Hidden settings ${storage.hiddenSettings.visible ? "visible" : "hidden"}`,
getAssetIDByName("SettingsIcon"),
);
return;
}
2026-05-09 10:20:55 +02:00
if (clickTimeout) {
clearTimeout(clickTimeout);
}
2026-05-09 10:20:55 +02:00
const newTimeout = setTimeout(() => {
setClickCounter(0);
}, 1000);
setClickTimeout(newTimeout);
2026-05-09 10:20:55 +02:00
const newCounter = clickCounter + 1;
setClickCounter(newCounter);
2026-05-09 10:20:55 +02:00
if (newCounter < 10) {
return;
}
2026-05-09 10:20:55 +02:00
showToast("Hidden settings unlocked!", getAssetIDByName("CheckmarkIcon"));
storage.hiddenSettings = {
...storage.hiddenSettings,
enabled: true,
visible: true,
};
setClickCounter(0);
};
2026-05-09 10:20:55 +02:00
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"),
);
};
2026-05-09 10:20:55 +02:00
const [showCogwheel, setShowCogwheel] = React.useState(false);
2026-05-09 10:20:55 +02:00
return (
<RN.View style={styles.container}>
<RN.View style={styles.leftSection}>
<RN.Pressable
style={styles.avatarContainer}
onPress={handleAvatarPress}
onPressIn={() => setShowCogwheel(true)}
onPressOut={() => setShowCogwheel(false)}
delayLongPress={500}
onLongPress={handleAvatarLongPress}
>
<RN.View style={styles.avatarWrapper}>
{avatarUrl ? (
<>
<RN.Image
source={{ uri: avatarUrl }}
style={styles.avatar}
resizeMode="cover"
/>
{isAnimatedAvatar && (
<RN.View style={styles.animatedIndicator}>
</RN.View>
)}
</>
) : (
<RN.Image
source={getAssetIDByName("ic_account_circle_24px")}
style={styles.avatarPlaceholder}
/>
)}
</RN.View>
2026-05-09 10:20:55 +02:00
{/* Cogwheel background overlay (appears on press) */}
<RN.View
style={[
styles.cogwheelBackground,
{ opacity: showCogwheel ? 0.8 : 0 },
]}
>
<RN.Image
source={getAssetIDByName("SettingsIcon")}
style={styles.cogwheelIcon}
/>
</RN.View>
</RN.Pressable>
2026-05-09 10:20:55 +02:00
<RN.View style={styles.textContainer}>
<RN.Text style={[TextStyleSheet["display-md"], { color: resolveSemanticColor(semanticColors["TEXT_DEFAULT"]) }]}>
Commands
</RN.Text>
<RN.Text style={[TextStyleSheet["text-md/bold"], { color: resolveSemanticColor(semanticColors["TEXT_MUTED"]) }]}>
A collection of useful commands for Vendetta like clients
</RN.Text>
2026-05-09 10:20:55 +02:00
</RN.View>
</RN.View>
</RN.View>
2026-05-09 10:20:55 +02:00
);
}