feat: add ServerDrawer Plugin
This commit is contained in:
parent
f9a22de40b
commit
1b27bf2970
17 changed files with 1104 additions and 2 deletions
|
|
@ -11,12 +11,13 @@ A collection of plugins for [**Kettu**](https://codeberg.org/cocobo1/Kettu), [**
|
|||
| **Multi Scrobbler** | Advanced scrobbling support for multiple music services |
|
||||
| **ChatboxAvatar** | Quickly view your profile from the chatbox. |
|
||||
| **Moyai** | 🗿 - Plays a sound on message send/react |
|
||||
| **Server Drawer** | Server Drawer above the YouBar, manage your servers differently! |
|
||||
|
||||
## 🚀 Installation
|
||||
|
||||
1. Copy plugin link from here
|
||||
2. Open **Vendetta** and go to the **Plugins** page
|
||||
3. Click the "Add Plugin" or "Install from URL" button
|
||||
2. Open **Discord Settings** and go to the **Plugins** page
|
||||
3. Click the "+" in the corner and paste the plugin link
|
||||
|
||||
## 🔗 Plugin Links
|
||||
|
||||
|
|
@ -40,6 +41,10 @@ https://rksv.kmmiio99o.dev/ChatboxAvatar
|
|||
```
|
||||
https://rksv.kmmiio99o.dev/Moyai
|
||||
```
|
||||
- **Server Drawer**:
|
||||
```
|
||||
https://rksv.kmmiio99o.dev/ServerDrawer
|
||||
```
|
||||
|
||||
## ℹ️ Notes
|
||||
|
||||
|
|
|
|||
62
index.html
Normal file
62
index.html
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>kmmiio99o's Plugins</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
background: #0d1117;
|
||||
background-image: radial-gradient(circle at 20% 50%, rgba(88, 166, 255, 0.06) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, rgba(163, 113, 247, 0.05) 0%, transparent 50%);
|
||||
color: #e6edf3;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
padding: 32px;
|
||||
max-width: 860px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
h1 { font-size: 2em; border-bottom: 1px solid #30363d; padding-bottom: 0.3em; margin-bottom: 16px; }
|
||||
h2 { font-size: 1.5em; border-bottom: 1px solid #30363d; padding-bottom: 0.3em; margin-top: 24px; margin-bottom: 16px; }
|
||||
a { color: #58a6ff; text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 16px 0; }
|
||||
th, td { border: 1px solid #30363d; padding: 8px 16px; text-align: left; }
|
||||
th { background: #161b22; }
|
||||
tr:nth-child(even) { background: #161b22; }
|
||||
code {
|
||||
background: #161b22;
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: 6px;
|
||||
font-size: 85%;
|
||||
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
||||
}
|
||||
pre {
|
||||
background: #161b22;
|
||||
border: 1px solid #30363d;
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
overflow-x: auto;
|
||||
margin: 16px 0;
|
||||
}
|
||||
pre code { background: none; padding: 0; font-size: 85%; }
|
||||
ul, ol { padding-left: 2em; margin: 8px 0; }
|
||||
li { margin: 4px 0; }
|
||||
em { color: #8b949e; }
|
||||
strong { color: #f0f6fc; }
|
||||
hr { border: none; border-top: 1px solid #30363d; margin: 24px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="content"></div>
|
||||
<script>
|
||||
fetch("README.md")
|
||||
.then(r => r.text())
|
||||
.then(md => {
|
||||
document.getElementById("content").innerHTML = marked.parse(md);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
14
plugins/ServerDrawer/manifest.json
Normal file
14
plugins/ServerDrawer/manifest.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"name": "Server Drawer",
|
||||
"description": "Server Drawer above the YouBar, manage your servers differently!",
|
||||
"authors": [
|
||||
{
|
||||
"name": "kmmiio99o",
|
||||
"id": "879393496627306587"
|
||||
}
|
||||
],
|
||||
"main": "src/index.tsx",
|
||||
"vendetta": {
|
||||
"icon": "ServerIcon"
|
||||
}
|
||||
}
|
||||
213
plugins/ServerDrawer/src/components/ContextMenuModal.tsx
Normal file
213
plugins/ServerDrawer/src/components/ContextMenuModal.tsx
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
import React from "react";
|
||||
import { View, Text, Pressable, Image, Modal, StyleSheet, Dimensions, Animated } from "react-native";
|
||||
import { semanticColors } from "@vendetta/ui";
|
||||
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;
|
||||
const Haptic = findByProps("triggerHapticFeedback", "HapticFeedbackTypes");
|
||||
|
||||
function resolve(color: any): string {
|
||||
const theme = ThemeStore?.theme;
|
||||
return (color && colorResolver?.resolveSemanticColor(theme, color)) || "#000000";
|
||||
}
|
||||
|
||||
const EDGE = 12;
|
||||
const PAD = 12;
|
||||
const ITEM_H = 42;
|
||||
const MIN_W = 220;
|
||||
const DIVIDER_H = 4;
|
||||
const RADIUS = 16;
|
||||
const ICON_SIZE = 20;
|
||||
|
||||
const SPRING_CONFIG = { tension: 200, friction: 20 };
|
||||
|
||||
export interface ContextMenuItem {
|
||||
label: string;
|
||||
action?: () => void;
|
||||
danger?: boolean;
|
||||
iconSource?: number;
|
||||
IconComponent?: React.ComponentType<any>;
|
||||
}
|
||||
|
||||
export function ContextMenuModal({
|
||||
visible,
|
||||
items,
|
||||
title,
|
||||
anchorX,
|
||||
anchorY,
|
||||
onClose,
|
||||
}: {
|
||||
visible: boolean;
|
||||
items: ContextMenuItem[];
|
||||
title: string;
|
||||
anchorX: number;
|
||||
anchorY: number;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const { width: winW, height: winH } = Dimensions.get("window");
|
||||
|
||||
const titleH = title ? PAD + 20 + DIVIDER_H : 0;
|
||||
const menuH = titleH + items.length * ITEM_H;
|
||||
|
||||
let left = anchorX;
|
||||
let top = anchorY + 48 + 10;
|
||||
if (left + MIN_W > winW - EDGE) left = winW - MIN_W - EDGE;
|
||||
if (left < EDGE) left = EDGE;
|
||||
if (top + menuH > winH - EDGE) top = anchorY - menuH - 10;
|
||||
if (top < EDGE) top = EDGE;
|
||||
|
||||
const bgContainer = resolve(semanticColors?.BACKGROUND_SURFACE_HIGHEST);
|
||||
const borderColor = resolve(semanticColors?.BORDER_SUBTLE);
|
||||
const textColor = resolve(semanticColors?.TEXT_STRONG);
|
||||
const pressedColor = resolve(semanticColors?.BACKGROUND_MOD_SUBTLE);
|
||||
|
||||
const backdropOpacity = React.useRef(new Animated.Value(0)).current;
|
||||
const menuScale = React.useRef(new Animated.Value(0.5)).current;
|
||||
const menuOpacity = React.useRef(new Animated.Value(0)).current;
|
||||
|
||||
const [show, setShow] = React.useState(false);
|
||||
const closing = React.useRef(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (visible && !closing.current) {
|
||||
setShow(true);
|
||||
backdropOpacity.setValue(0);
|
||||
menuScale.setValue(0.5);
|
||||
menuOpacity.setValue(0);
|
||||
Animated.parallel([
|
||||
Animated.timing(backdropOpacity, { toValue: 1, duration: 150, useNativeDriver: true }),
|
||||
Animated.spring(menuScale, { toValue: 1, useNativeDriver: true, ...SPRING_CONFIG }),
|
||||
Animated.spring(menuOpacity, { toValue: 1, useNativeDriver: true, ...SPRING_CONFIG }),
|
||||
]).start();
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
const handleClose = React.useCallback(() => {
|
||||
if (closing.current) return;
|
||||
closing.current = true;
|
||||
Animated.parallel([
|
||||
Animated.timing(backdropOpacity, { toValue: 0, duration: 120, useNativeDriver: true }),
|
||||
Animated.timing(menuScale, { toValue: 0.8, duration: 120, useNativeDriver: true }),
|
||||
Animated.timing(menuOpacity, { toValue: 0, duration: 120, useNativeDriver: true }),
|
||||
]).start(() => {
|
||||
closing.current = false;
|
||||
setShow(false);
|
||||
onClose();
|
||||
});
|
||||
}, [onClose]);
|
||||
|
||||
if (!show) return null;
|
||||
|
||||
return (
|
||||
<Modal transparent visible={show} onRequestClose={handleClose} statusBarTranslucent>
|
||||
<Pressable style={localStyles.root} onPress={handleClose}>
|
||||
<Animated.View style={[localStyles.backdrop, { opacity: backdropOpacity }]} />
|
||||
<Animated.View
|
||||
style={[
|
||||
localStyles.container,
|
||||
{
|
||||
left,
|
||||
top,
|
||||
backgroundColor: bgContainer,
|
||||
borderColor,
|
||||
minWidth: MIN_W,
|
||||
opacity: menuOpacity,
|
||||
transform: [{ scale: menuScale }],
|
||||
},
|
||||
]}
|
||||
>
|
||||
{title ? (
|
||||
<View>
|
||||
<Text
|
||||
style={[
|
||||
TextStyleSheet?.["text-md/bold"],
|
||||
{ color: textColor, paddingHorizontal: PAD, paddingTop: 13, paddingBottom: 12 },
|
||||
]}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
<View style={{ borderBottomWidth: DIVIDER_H, borderBottomColor: borderColor }} />
|
||||
</View>
|
||||
) : null}
|
||||
{items.map((item, i) => {
|
||||
const isFirst = i === 0 && !title;
|
||||
const isLast = i === items.length - 1;
|
||||
const hasIcon = !!(item.iconSource || item.IconComponent);
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
key={i}
|
||||
style={({ pressed }) => ({
|
||||
paddingHorizontal: PAD,
|
||||
paddingVertical: 10,
|
||||
minHeight: ITEM_H,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
backgroundColor: pressed ? pressedColor : "transparent",
|
||||
borderTopLeftRadius: isFirst ? RADIUS : 0,
|
||||
borderTopRightRadius: isFirst ? RADIUS : 0,
|
||||
borderBottomLeftRadius: isLast ? RADIUS : 0,
|
||||
borderBottomRightRadius: isLast ? RADIUS : 0,
|
||||
borderBottomWidth: !isFirst && !isLast ? 1 : 0,
|
||||
borderBottomColor: borderColor,
|
||||
})}
|
||||
onPress={() => {
|
||||
Haptic?.triggerHapticFeedback?.(Haptic.HapticFeedbackTypes.IMPACT_LIGHT);
|
||||
handleClose();
|
||||
item.action?.();
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
TextStyleSheet?.["text-md/medium"],
|
||||
{
|
||||
flexShrink: 1,
|
||||
color: textColor,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{item.label}
|
||||
</Text>
|
||||
{hasIcon ? (
|
||||
<View style={{ width: ICON_SIZE, height: ICON_SIZE, alignItems: "center", justifyContent: "center" }}>
|
||||
{item.IconComponent ? (
|
||||
<item.IconComponent size="sm" color={textColor} />
|
||||
) : item.iconSource ? (
|
||||
<Image source={item.iconSource} style={{ width: ICON_SIZE, height: ICON_SIZE, tintColor: textColor }} />
|
||||
) : null}
|
||||
</View>
|
||||
) : null}
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
const localStyles = StyleSheet.create({
|
||||
root: {
|
||||
flex: 1,
|
||||
},
|
||||
backdrop: {
|
||||
...StyleSheet.absoluteFill,
|
||||
backgroundColor: "rgba(0,0,0,0.55)",
|
||||
},
|
||||
container: {
|
||||
position: "absolute",
|
||||
borderWidth: 1,
|
||||
borderRadius: RADIUS,
|
||||
overflow: "hidden",
|
||||
elevation: 20,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 12 },
|
||||
shadowOpacity: 0.24,
|
||||
shadowRadius: 24,
|
||||
},
|
||||
});
|
||||
154
plugins/ServerDrawer/src/components/FolderItem.tsx
Normal file
154
plugins/ServerDrawer/src/components/FolderItem.tsx
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
import React from "react";
|
||||
import { View, Text, Animated, Pressable, Image, ViewStyle, StyleSheet } from "react-native";
|
||||
import { findByProps, findByStoreName } from "@vendetta/metro";
|
||||
import { getAssetIDByName } from "@vendetta/ui/assets";
|
||||
import { useFolderExpanded, GuildNode } from "../utils/theme";
|
||||
import GuildIcon from "./GuildIcon";
|
||||
import GuildItem from "./GuildItem";
|
||||
|
||||
const GuildActions = findByProps("toggleGuildFolderExpand");
|
||||
const Flux = findByProps("useStateFromStores");
|
||||
const GuildReadStateStore = findByStoreName("GuildReadStateStore");
|
||||
|
||||
const ICON = 48;
|
||||
const MINI = 16;
|
||||
|
||||
const POS: ViewStyle[] = [
|
||||
{ top: 6, left: 6 },
|
||||
{ top: 6, right: 6 },
|
||||
{ bottom: 6, left: 6 },
|
||||
{ bottom: 6, right: 6 },
|
||||
];
|
||||
|
||||
const FOLDER_ASSET = getAssetIDByName("FolderIcon");
|
||||
|
||||
function folderColor(color?: number | null): string {
|
||||
if (color == null) return "#5865f2";
|
||||
return `#${color.toString(16).padStart(6, "0")}`;
|
||||
}
|
||||
|
||||
function FolderBadge({ node }: { node: GuildNode }) {
|
||||
const total = Flux?.useStateFromStores?.(
|
||||
[GuildReadStateStore],
|
||||
() => {
|
||||
let sum = 0;
|
||||
for (const child of node.children) {
|
||||
sum += GuildReadStateStore?.getMentionCount?.(child.id) ?? 0;
|
||||
}
|
||||
return sum;
|
||||
},
|
||||
[node.children],
|
||||
) ?? 0;
|
||||
|
||||
if (total > 0) {
|
||||
return (
|
||||
<View style={fbd.outline}>
|
||||
<View style={fbd.badge}>
|
||||
<Text style={fbd.text}>{total > 99 ? "99+" : String(total)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function FolderCover({ node }: { node: GuildNode }) {
|
||||
const col = folderColor(node.color);
|
||||
return (
|
||||
<View style={fc.outer}>
|
||||
<View style={[fc.icon, { backgroundColor: col }]}>
|
||||
{node.children.slice(0, 4).map((ch, i) => (
|
||||
<View key={ch.id} style={[fc.cell, POS[i]]}>
|
||||
<GuildIcon id={ch.id as string} size={MINI} />
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
<FolderBadge node={node} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const fc = StyleSheet.create({
|
||||
outer: { width: ICON, height: ICON },
|
||||
icon: { width: ICON, height: ICON, borderRadius: 16, overflow: "hidden" },
|
||||
cell: { position: "absolute", width: MINI, height: MINI, borderRadius: 8, overflow: "hidden" },
|
||||
});
|
||||
|
||||
function FadeIn({ children }: { children: React.ReactNode }) {
|
||||
const opacity = React.useRef(new Animated.Value(0)).current;
|
||||
React.useEffect(() => {
|
||||
Animated.timing(opacity, { toValue: 1, duration: 180, useNativeDriver: true }).start();
|
||||
}, []);
|
||||
return <Animated.View style={{ opacity }}>{children}</Animated.View>;
|
||||
}
|
||||
|
||||
export default function FolderItem({ node, onPick }: { node: GuildNode; onPick: (id: string) => void }) {
|
||||
const open = useFolderExpanded(node.id);
|
||||
|
||||
const toggle = () => {
|
||||
GuildActions?.toggleGuildFolderExpand?.(node.id);
|
||||
};
|
||||
|
||||
if (open) {
|
||||
return (
|
||||
<>
|
||||
<Pressable onPress={toggle}>
|
||||
<View style={[fo.openIcon, { backgroundColor: folderColor(node.color) }]}>
|
||||
<Image source={FOLDER_ASSET} style={fo.folderImg} tintColor="#fff" />
|
||||
</View>
|
||||
</Pressable>
|
||||
{node.children.map((ch) => (
|
||||
<FadeIn key={ch.id}>
|
||||
<GuildItem node={ch} onPick={onPick} />
|
||||
</FadeIn>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Pressable onPress={toggle}>
|
||||
<FolderCover node={node} />
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
const fo = StyleSheet.create({
|
||||
openIcon: {
|
||||
width: ICON,
|
||||
height: ICON,
|
||||
borderRadius: 16,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
folderImg: { width: 24, height: 24 },
|
||||
});
|
||||
|
||||
const fbd = StyleSheet.create({
|
||||
outline: {
|
||||
position: "absolute",
|
||||
bottom: -3,
|
||||
right: -3,
|
||||
minWidth: 23,
|
||||
minHeight: 23,
|
||||
borderRadius: 12,
|
||||
backgroundColor: "#1a1a2e",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
badge: {
|
||||
minWidth: 19,
|
||||
height: 19,
|
||||
borderRadius: 9,
|
||||
backgroundColor: "#ed4245",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
paddingHorizontal: 5,
|
||||
},
|
||||
text: {
|
||||
color: "#fff",
|
||||
fontSize: 10,
|
||||
fontWeight: "700",
|
||||
lineHeight: 19,
|
||||
},
|
||||
});
|
||||
32
plugins/ServerDrawer/src/components/GuildIcon.tsx
Normal file
32
plugins/ServerDrawer/src/components/GuildIcon.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import React from "react";
|
||||
import { View, Text, Image, StyleSheet } from "react-native";
|
||||
import { findByStoreName, findByProps } from "@vendetta/metro";
|
||||
|
||||
const GuildStore = findByStoreName("GuildStore");
|
||||
const colors = findByProps("colors", "unsafe_rawColors")?.colors;
|
||||
|
||||
export default function GuildIcon({ id, size = 48 }: { id: string; size?: number }) {
|
||||
const g = GuildStore?.getGuild(id);
|
||||
if (!g) return null;
|
||||
const rad = size >= 40 ? 16 : 8;
|
||||
if (g.icon) {
|
||||
return (
|
||||
<Image
|
||||
source={{ uri: `https://cdn.discordapp.com/icons/${g.id}/${g.icon}.png?size=${size * 2}` }}
|
||||
style={{ width: size, height: size, borderRadius: rad }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<View style={[st.fallback, { width: size, height: size, borderRadius: rad, backgroundColor: colors?.BG_ACCENT ?? "#5865f2" }]}>
|
||||
<Text style={[st.letter, { fontSize: Math.max(10, size * 0.4) }]}>
|
||||
{g.name?.split(" ").map((w: string) => w[0]).join("").slice(0, 2).toUpperCase() || "?"}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const st = StyleSheet.create({
|
||||
fallback: { alignItems: "center", justifyContent: "center" },
|
||||
letter: { color: "#fff", fontWeight: "700" },
|
||||
});
|
||||
182
plugins/ServerDrawer/src/components/GuildItem.tsx
Normal file
182
plugins/ServerDrawer/src/components/GuildItem.tsx
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
import React from "react";
|
||||
import { View, Text, Pressable, Animated, StyleSheet } from "react-native";
|
||||
import { find, findByProps, findByStoreName } from "@vendetta/metro";
|
||||
import { GuildNode } from "../utils/theme";
|
||||
import GuildIcon from "./GuildIcon";
|
||||
import { ContextMenuModal, ContextMenuItem } from "./ContextMenuModal";
|
||||
|
||||
const ICON = 48;
|
||||
|
||||
const Flux = findByProps("useStateFromStores");
|
||||
const GuildReadStateStore = findByStoreName("GuildReadStateStore");
|
||||
const GuildStore = findByStoreName("GuildStore");
|
||||
const SortedGuildStore = findByStoreName("SortedGuildStore");
|
||||
const Haptic = findByProps("triggerHapticFeedback", "HapticFeedbackTypes");
|
||||
|
||||
function Badge({ guildId }: { guildId: string }) {
|
||||
const mentionCount = Flux?.useStateFromStores?.(
|
||||
[GuildReadStateStore],
|
||||
() => GuildReadStateStore?.getMentionCount?.(guildId) ?? 0,
|
||||
[guildId],
|
||||
) ?? 0;
|
||||
|
||||
const hasUnread = Flux?.useStateFromStores?.(
|
||||
[GuildReadStateStore],
|
||||
() => GuildReadStateStore?.hasUnread?.(guildId) ?? false,
|
||||
[guildId],
|
||||
) ?? false;
|
||||
|
||||
if (mentionCount > 0) {
|
||||
return (
|
||||
<View style={bd.outline}>
|
||||
<View style={bd.badge}>
|
||||
<Text style={bd.text}>{mentionCount > 99 ? "99+" : String(mentionCount)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (hasUnread) {
|
||||
return (
|
||||
<View style={bd.dotOutline}>
|
||||
<View style={bd.dot} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function GuildItem({ node, onPick }: { node: GuildNode; onPick: (id: string) => void }) {
|
||||
const viewRef = React.useRef<View>(null);
|
||||
const scale = React.useRef(new Animated.Value(1)).current;
|
||||
const scaleDown = () => Animated.spring(scale, { toValue: 0.85, useNativeDriver: true }).start();
|
||||
const scaleUp = () => Animated.spring(scale, { toValue: 1, useNativeDriver: true }).start();
|
||||
|
||||
const [menuState, setMenuState] = React.useState<{
|
||||
visible: boolean;
|
||||
items: ContextMenuItem[];
|
||||
title: string;
|
||||
x: number;
|
||||
y: number;
|
||||
}>({ visible: false, items: [], title: "", x: 0, y: 0 });
|
||||
|
||||
const showMenu = React.useCallback(() => {
|
||||
const guildId = node.id as string;
|
||||
const guild = GuildStore?.getGuild?.(guildId);
|
||||
const menuItemsFn = find((m) => m?.default?.name === "getGuildsBarGuildMenuItems")?.default
|
||||
?? find((m) => typeof m?.default === "function" && m.default.length <= 2 && /guild/i.test(m.default.name ?? ""))?.default;
|
||||
|
||||
if (!guild || !menuItemsFn) {
|
||||
return;
|
||||
}
|
||||
|
||||
const treeVersion = SortedGuildStore?.getGuildsTree?.()?.version;
|
||||
const rawItems = menuItemsFn(guildId, treeVersion);
|
||||
if (!rawItems?.length) return;
|
||||
|
||||
const ref = viewRef.current as any;
|
||||
if (!ref?.measure) return;
|
||||
|
||||
ref.measure((_fx: number, _fy: number, _w: number, _h: number, pageX: number, pageY: number) => {
|
||||
setMenuState({
|
||||
visible: true,
|
||||
items: rawItems.map((item: any) => ({
|
||||
label: item.label ?? item.title ?? "Unknown",
|
||||
action: item.action,
|
||||
danger: item.variant === "destructive",
|
||||
iconSource: item.iconSource,
|
||||
IconComponent: item.IconComponent,
|
||||
})),
|
||||
title: guild.name,
|
||||
x: pageX,
|
||||
y: pageY,
|
||||
});
|
||||
});
|
||||
}, [node.id]);
|
||||
|
||||
const handleLongPress = React.useCallback(() => {
|
||||
Haptic?.triggerHapticFeedback?.(Haptic.HapticFeedbackTypes.IMPACT_MEDIUM);
|
||||
showMenu();
|
||||
}, [showMenu]);
|
||||
|
||||
const guildId = node.id as string;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Pressable
|
||||
onPressIn={scaleDown}
|
||||
onPressOut={scaleUp}
|
||||
onPress={() => onPick(guildId)}
|
||||
onLongPress={handleLongPress}
|
||||
delayLongPress={500}
|
||||
>
|
||||
<View ref={viewRef} style={st.outer} collapsable={false}>
|
||||
<Animated.View style={[st.icon, { transform: [{ scale }] }]}>
|
||||
<GuildIcon id={guildId} />
|
||||
</Animated.View>
|
||||
<Badge guildId={guildId} />
|
||||
</View>
|
||||
</Pressable>
|
||||
<ContextMenuModal
|
||||
visible={menuState.visible}
|
||||
items={menuState.items}
|
||||
title={menuState.title}
|
||||
anchorX={menuState.x}
|
||||
anchorY={menuState.y}
|
||||
onClose={() => setMenuState((s) => ({ ...s, visible: false }))}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const st = StyleSheet.create({
|
||||
outer: { width: ICON, height: ICON },
|
||||
icon: { width: ICON, height: ICON, borderRadius: 16, overflow: "hidden" },
|
||||
});
|
||||
|
||||
const bd = StyleSheet.create({
|
||||
outline: {
|
||||
position: "absolute",
|
||||
bottom: -3,
|
||||
right: -3,
|
||||
minWidth: 23,
|
||||
minHeight: 23,
|
||||
borderRadius: 12,
|
||||
backgroundColor: "#1a1a2e",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
badge: {
|
||||
minWidth: 19,
|
||||
height: 19,
|
||||
borderRadius: 9,
|
||||
backgroundColor: "#ed4245",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
paddingHorizontal: 5,
|
||||
},
|
||||
text: {
|
||||
color: "#fff",
|
||||
fontSize: 10,
|
||||
fontWeight: "700",
|
||||
lineHeight: 19,
|
||||
},
|
||||
dotOutline: {
|
||||
position: "absolute",
|
||||
bottom: -2,
|
||||
right: -2,
|
||||
width: 14,
|
||||
height: 14,
|
||||
borderRadius: 7,
|
||||
backgroundColor: "#1a1a2e",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
dot: {
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: 5,
|
||||
backgroundColor: "#ed4245",
|
||||
},
|
||||
});
|
||||
97
plugins/ServerDrawer/src/components/ServerDrawerSheet.tsx
Normal file
97
plugins/ServerDrawer/src/components/ServerDrawerSheet.tsx
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import React from "react";
|
||||
import { View, Dimensions, StyleSheet, BackHandler } from "react-native";
|
||||
import { find, findByProps, findByStoreName } from "@vendetta/metro";
|
||||
import { GuildNode } from "../utils/theme";
|
||||
import GuildItem from "./GuildItem";
|
||||
import FolderItem from "./FolderItem";
|
||||
|
||||
const SortedGuildStore = findByStoreName("SortedGuildStore");
|
||||
const RootNav = findByProps("getRootNavigationRef");
|
||||
const Haptic = findByProps("triggerHapticFeedback", "HapticFeedbackTypes");
|
||||
const Routing = findByProps("transitionToGuild");
|
||||
|
||||
const ExternalCoordinationMod = find((m: any) => m?.QuestDockExternalCoordinationContext);
|
||||
const ExternalContext = ExternalCoordinationMod?.QuestDockExternalCoordinationContext;
|
||||
const QuestDockMode = find((m: any) => m?.QuestDockMode?.COLLAPSED != null)?.QuestDockMode;
|
||||
|
||||
const ICON = 48;
|
||||
const GAP = 6;
|
||||
const PAD = 12;
|
||||
|
||||
export default function ServerDrawerSheet({ gestureContext }: { gestureContext: any }) {
|
||||
const pick = React.useCallback((id: string) => {
|
||||
Haptic?.triggerHapticFeedback(Haptic.HapticFeedbackTypes.SOFT);
|
||||
if (Routing?.transitionToGuild) {
|
||||
Routing.transitionToGuild(id, null);
|
||||
} else {
|
||||
RootNav?.getRootNavigationRef()?.navigate("guilds", { guildId: id });
|
||||
}
|
||||
}, []);
|
||||
|
||||
const nodes: GuildNode[] = React.useMemo(() => {
|
||||
const t = SortedGuildStore?.getGuildsTree();
|
||||
return (t?.root?.children || []).filter((n: GuildNode) => n.type !== "root");
|
||||
}, []);
|
||||
|
||||
const ctx = (gestureContext ? React.useContext(gestureContext) : null) as any;
|
||||
const minH = ctx?.minExpandedContentHeight;
|
||||
|
||||
const onLayout = React.useCallback((e: any) => {
|
||||
if (!minH) return;
|
||||
const h = e.nativeEvent.layout.height;
|
||||
if (minH.get() !== h) minH.set(h);
|
||||
}, [minH]);
|
||||
|
||||
const extCtx = ExternalContext ? React.useContext(ExternalContext) as any : null;
|
||||
const setMode = extCtx?.setRestingQuestDockMode;
|
||||
|
||||
const specs = ctx?.questDockWrapperSpecs;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!setMode || !QuestDockMode || !specs) return;
|
||||
const sub = BackHandler.addEventListener("hardwareBackPress", () => {
|
||||
const h = specs.get()?.height ?? 56;
|
||||
if (h > 80) {
|
||||
setMode(QuestDockMode.COLLAPSED);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return () => sub.remove();
|
||||
}, [setMode, specs]);
|
||||
|
||||
const { width: winW } = Dimensions.get("window");
|
||||
|
||||
const cols = Math.max(3, Math.floor((winW - PAD * 2 + GAP) / (ICON + GAP)));
|
||||
const totalW = cols * ICON + (cols - 1) * GAP;
|
||||
const padX = Math.max(0, (winW - totalW) / 2);
|
||||
|
||||
return (
|
||||
<View style={st.alignTop}>
|
||||
<View
|
||||
style={[st.grid, { paddingHorizontal: padX, gap: GAP }]}
|
||||
onLayout={onLayout}
|
||||
>
|
||||
{nodes.map((node) =>
|
||||
node.type === "folder"
|
||||
? <FolderItem key={node.id} node={node} onPick={pick} />
|
||||
: <GuildItem key={node.id} node={node} onPick={pick} />
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const st = StyleSheet.create({
|
||||
alignTop: {
|
||||
flex: 1,
|
||||
justifyContent: "flex-start",
|
||||
alignItems: "flex-start",
|
||||
},
|
||||
grid: {
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
paddingTop: 4,
|
||||
paddingBottom: 16,
|
||||
},
|
||||
});
|
||||
40
plugins/ServerDrawer/src/index.tsx
Normal file
40
plugins/ServerDrawer/src/index.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { patchQuestDockRender } from "./patches/questDockRender";
|
||||
import { patchQuestDockBase } from "./patches/questDockBase";
|
||||
import { patchMobileQuestDock } from "./patches/mobileQuestDock";
|
||||
import { patchGetQuestAsset } from "./patches/getQuestAsset";
|
||||
import { patchExpanded, patchEmpty } from "./patches/contentPatch";
|
||||
import { patchHideGuildsBar } from "./patches/hideGuildsBar";
|
||||
import { patchCreateElement } from "./patches/createElementIntercept";
|
||||
|
||||
const TAG = "[ServerDrawer]";
|
||||
const cleanups: (() => void)[] = [];
|
||||
|
||||
export default {
|
||||
onLoad() {
|
||||
console.log(TAG, "onLoad");
|
||||
|
||||
let patched = 0;
|
||||
|
||||
patchCreateElement(cleanups);
|
||||
patched++;
|
||||
|
||||
if (patchQuestDockRender(cleanups)) patched++;
|
||||
if (patchQuestDockBase(cleanups)) patched++;
|
||||
if (patchMobileQuestDock(cleanups)) patched++;
|
||||
if (patchGetQuestAsset(cleanups)) patched++;
|
||||
if (patchExpanded(cleanups)) patched++;
|
||||
if (patchEmpty("QuestDockContentCollapsed", cleanups)) patched++;
|
||||
if (patchEmpty("QuestDockEnrolledHeader", cleanups)) patched++;
|
||||
if (patchEmpty("QuestDockUnenrolledHeader", cleanups)) patched++;
|
||||
if (patchEmpty("QuestDockEnrolledBody", cleanups)) patched++;
|
||||
if (patchEmpty("QuestDockUnenrolledBody", cleanups)) patched++;
|
||||
if (patchHideGuildsBar(cleanups)) patched++;
|
||||
|
||||
console.log(TAG, `onLoad done — ${patched} patches applied, ${cleanups.length} cleanups`);
|
||||
},
|
||||
onUnload() {
|
||||
console.log(TAG, "onUnload");
|
||||
for (const fn of cleanups) fn();
|
||||
cleanups.length = 0;
|
||||
},
|
||||
};
|
||||
45
plugins/ServerDrawer/src/patches/contentPatch.tsx
Normal file
45
plugins/ServerDrawer/src/patches/contentPatch.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { find } from "@vendetta/metro";
|
||||
import { React } from "@vendetta/metro/common";
|
||||
import ServerDrawerSheet from "../components/ServerDrawerSheet";
|
||||
import { registerIntercept } from "./createElementIntercept";
|
||||
|
||||
const TAG = "[ServerDrawer]";
|
||||
|
||||
const QuestDockGestureContext = find((m) => m?.QuestDockGestureContext)?.QuestDockGestureContext;
|
||||
|
||||
export function patchExpanded(
|
||||
cleanups: (() => void)[]
|
||||
): boolean {
|
||||
const mod = find((m) => m?.type?.displayName === "QuestDockContentExpanded" || m?.type?.name === "QuestDockContentExpanded");
|
||||
if (!mod?.type) {
|
||||
console.log(TAG, "WARN: QuestDockContentExpanded not found");
|
||||
return false;
|
||||
}
|
||||
const orig = mod.type;
|
||||
|
||||
registerIntercept(orig, ServerDrawerSheet, { gestureContext: QuestDockGestureContext });
|
||||
|
||||
mod.type = function ServerDrawerPatch() {
|
||||
return <ServerDrawerSheet gestureContext={QuestDockGestureContext} />;
|
||||
};
|
||||
cleanups.push(() => { mod.type = orig; });
|
||||
console.log(TAG, "PATCH: QuestDockContentExpanded replaced");
|
||||
return true;
|
||||
}
|
||||
|
||||
export function patchEmpty(
|
||||
name: string,
|
||||
cleanups: (() => void)[]
|
||||
): boolean {
|
||||
const mod = find((m) => m?.type?.displayName === name || m?.type?.name === name);
|
||||
if (!mod?.type) {
|
||||
console.log(TAG, `WARN: ${name} not found`);
|
||||
return false;
|
||||
}
|
||||
const orig = mod.type;
|
||||
mod.type = function EmptyPatch() {
|
||||
return null;
|
||||
};
|
||||
cleanups.push(() => { mod.type = orig; });
|
||||
return true;
|
||||
}
|
||||
45
plugins/ServerDrawer/src/patches/createElementIntercept.ts
Normal file
45
plugins/ServerDrawer/src/patches/createElementIntercept.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { React } from "@vendetta/metro/common";
|
||||
|
||||
interface Intercept {
|
||||
replacement: React.ComponentType<any>;
|
||||
extraProps?: Record<string, any>;
|
||||
}
|
||||
|
||||
const intercepts = new Map<React.ComponentType<any>, Intercept>();
|
||||
let origCreateElement: typeof React.createElement | null = null;
|
||||
|
||||
export function registerIntercept(
|
||||
original: React.ComponentType<any>,
|
||||
replacement: React.ComponentType<any>,
|
||||
extraProps?: Record<string, any>,
|
||||
) {
|
||||
intercepts.set(original, { replacement, extraProps });
|
||||
}
|
||||
|
||||
export function patchCreateElement(cleanups: (() => void)[]) {
|
||||
if (origCreateElement) return;
|
||||
|
||||
origCreateElement = React.createElement;
|
||||
|
||||
const patched = function (type: any, props: any, ...rest: any[]) {
|
||||
const entry = intercepts.get(type);
|
||||
if (entry) {
|
||||
const newProps = entry.extraProps
|
||||
? { ...props, ...entry.extraProps }
|
||||
: props;
|
||||
return origCreateElement!.call(React, entry.replacement, newProps, ...rest);
|
||||
}
|
||||
return origCreateElement!.call(React, type, props, ...rest);
|
||||
};
|
||||
|
||||
Object.assign(patched, origCreateElement);
|
||||
React.createElement = patched as typeof React.createElement;
|
||||
|
||||
cleanups.push(() => {
|
||||
if (origCreateElement) {
|
||||
React.createElement = origCreateElement;
|
||||
origCreateElement = null;
|
||||
}
|
||||
intercepts.clear();
|
||||
});
|
||||
}
|
||||
21
plugins/ServerDrawer/src/patches/getQuestAsset.ts
Normal file
21
plugins/ServerDrawer/src/patches/getQuestAsset.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { findByProps } from "@vendetta/metro";
|
||||
|
||||
const TAG = "[ServerDrawer]";
|
||||
|
||||
export function patchGetQuestAsset(cleanups: (() => void)[]): boolean {
|
||||
const mod = findByProps("getQuestAsset");
|
||||
if (!mod?.getQuestAsset) {
|
||||
console.log(TAG, "WARN: getQuestAsset not found");
|
||||
return false;
|
||||
}
|
||||
const orig = mod.getQuestAsset;
|
||||
mod.getQuestAsset = function (...args: any[]) {
|
||||
try {
|
||||
return orig.apply(this, args);
|
||||
} catch {
|
||||
return { url: null, isAnimated: false };
|
||||
}
|
||||
};
|
||||
cleanups.push(() => { mod.getQuestAsset = orig; });
|
||||
return true;
|
||||
}
|
||||
94
plugins/ServerDrawer/src/patches/hideGuildsBar.tsx
Normal file
94
plugins/ServerDrawer/src/patches/hideGuildsBar.tsx
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import React from "react";
|
||||
import { Pressable, Image, View } from "react-native";
|
||||
import { find, findByName, findByProps } from "@vendetta/metro";
|
||||
import { getAssetIDByName } from "@vendetta/ui/assets";
|
||||
import { registerIntercept } from "./createElementIntercept";
|
||||
|
||||
const TAG = "[ServerDrawer]";
|
||||
|
||||
const ChatIcon = getAssetIDByName("ChatIcon");
|
||||
const Haptic = findByProps("triggerHapticFeedback", "HapticFeedbackTypes");
|
||||
const ChannelActions = findByProps("selectPrivateChannel");
|
||||
const SelectedChannelStore = findByName("SelectedChannelStore");
|
||||
const FluxStores = findByProps("useStateFromStores");
|
||||
const useStateFromStores = FluxStores?.useStateFromStores;
|
||||
const GuildStore = findByProps("getGuildId");
|
||||
const colors = findByProps("colors", "unsafe_rawColors")?.colors;
|
||||
const Routes = findByProps("ME");
|
||||
const ME = Routes?.ME ?? "/channels/@me";
|
||||
|
||||
function openDms() {
|
||||
Haptic?.triggerHapticFeedback(Haptic.HapticFeedbackTypes.SOFT);
|
||||
if (ChannelActions?.selectPrivateChannel) {
|
||||
const lastChannelId = SelectedChannelStore?.getLastSelectedChannelId?.();
|
||||
ChannelActions.selectPrivateChannel(lastChannelId);
|
||||
}
|
||||
}
|
||||
|
||||
function useIsInDms(): boolean {
|
||||
try {
|
||||
return GuildStore ? useStateFromStores([GuildStore], () => {
|
||||
const guildId = GuildStore?.getGuildId?.();
|
||||
return guildId == null || guildId === ME;
|
||||
}) : false;
|
||||
} catch { return false; }
|
||||
}
|
||||
|
||||
function DmsGuildIcon() {
|
||||
const selected = useIsInDms();
|
||||
|
||||
const iconColor = selected
|
||||
? (colors?.WHITE ?? "#fff")
|
||||
: (colors?.INTERACTIVE_NORMAL ?? "#80848e");
|
||||
|
||||
return (
|
||||
<Pressable onPress={openDms} style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
|
||||
{selected ? (
|
||||
<View style={{
|
||||
position: "absolute", left: 0, top: "25%",
|
||||
width: 4, height: "50%", borderRadius: 2,
|
||||
backgroundColor: colors?.WHITE ?? "#fff",
|
||||
}} />
|
||||
) : null}
|
||||
<Image
|
||||
source={ChatIcon}
|
||||
style={{ width: 28, height: 28, tintColor: iconColor }}
|
||||
/>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
function findGuildsBar(): any {
|
||||
const byName = findByName("GuildsBar");
|
||||
if (byName) return { default: byName };
|
||||
|
||||
let mod = find((m) => {
|
||||
try { return m?.default?.type?.name === "GuildsBar"; } catch { return false; }
|
||||
});
|
||||
if (mod?.default) return mod;
|
||||
|
||||
mod = find((m) => {
|
||||
try { return m?.default?.displayName === "GuildsBar"; } catch { return false; }
|
||||
});
|
||||
if (mod?.default) return mod;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function patchHideGuildsBar(cleanups: (() => void)[]): boolean {
|
||||
const mod = findGuildsBar();
|
||||
if (!mod?.default) {
|
||||
console.log(TAG, "WARN: GuildsBar not found");
|
||||
return false;
|
||||
}
|
||||
const orig = mod.default;
|
||||
|
||||
registerIntercept(orig, DmsGuildIcon);
|
||||
|
||||
mod.default = function DmsGuildsBar() {
|
||||
return React.createElement(DmsGuildIcon);
|
||||
};
|
||||
mod.default.displayName = "GuildsBar";
|
||||
cleanups.push(() => { mod.default = orig; });
|
||||
return true;
|
||||
}
|
||||
29
plugins/ServerDrawer/src/patches/mobileQuestDock.ts
Normal file
29
plugins/ServerDrawer/src/patches/mobileQuestDock.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { findByProps } from "@vendetta/metro";
|
||||
|
||||
const TAG = "[ServerDrawer]";
|
||||
|
||||
export function patchMobileQuestDock(cleanups: (() => void)[]): boolean {
|
||||
const mod = findByProps("useMobileQuestDock");
|
||||
if (!mod.useMobileQuestDock) {
|
||||
console.log(TAG, "WARN: useMobileQuestDock not found");
|
||||
return false;
|
||||
}
|
||||
const orig = mod.useMobileQuestDock;
|
||||
mod.useMobileQuestDock = function (...args: any[]) {
|
||||
const real = orig.apply(this, args);
|
||||
if (real) return real;
|
||||
return {
|
||||
id: "server-drawer",
|
||||
config: {
|
||||
quest_content_type: 0,
|
||||
assets: { questBarHeroVideo: null, questBarHero: null },
|
||||
features: [],
|
||||
},
|
||||
userStatus: { enrolledAt: "2099-01-01", claimedAt: null },
|
||||
benefits: { rewards: [] },
|
||||
guildId: "0",
|
||||
};
|
||||
};
|
||||
cleanups.push(() => { mod.useMobileQuestDock = orig; });
|
||||
return true;
|
||||
}
|
||||
18
plugins/ServerDrawer/src/patches/questDockBase.ts
Normal file
18
plugins/ServerDrawer/src/patches/questDockBase.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { findByProps } from "@vendetta/metro";
|
||||
|
||||
const TAG = "[ServerDrawer]";
|
||||
|
||||
export function patchQuestDockBase(cleanups: (() => void)[]): boolean {
|
||||
const mod = findByProps("useIsMobileQuestDockRenderedBase");
|
||||
if (!mod?.useIsMobileQuestDockRenderedBase) {
|
||||
console.log(TAG, "WARN: useIsMobileQuestDockRenderedBase not found");
|
||||
return false;
|
||||
}
|
||||
const orig = mod.useIsMobileQuestDockRenderedBase;
|
||||
mod.useIsMobileQuestDockRenderedBase = function (...args: any[]) {
|
||||
orig.apply(this, args);
|
||||
return true;
|
||||
};
|
||||
cleanups.push(() => { mod.useIsMobileQuestDockRenderedBase = orig; });
|
||||
return true;
|
||||
}
|
||||
18
plugins/ServerDrawer/src/patches/questDockRender.ts
Normal file
18
plugins/ServerDrawer/src/patches/questDockRender.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { findByProps } from "@vendetta/metro";
|
||||
|
||||
const TAG = "[ServerDrawer]";
|
||||
|
||||
export function patchQuestDockRender(cleanups: (() => void)[]): boolean {
|
||||
const mod = findByProps("useIsMobileQuestDockRendered");
|
||||
if (!mod?.useIsMobileQuestDockRendered) {
|
||||
console.log(TAG, "WARN: useIsMobileQuestDockRendered not found");
|
||||
return false;
|
||||
}
|
||||
const orig = mod.useIsMobileQuestDockRendered;
|
||||
mod.useIsMobileQuestDockRendered = function (...args: any[]) {
|
||||
orig.apply(this, args);
|
||||
return true;
|
||||
};
|
||||
cleanups.push(() => { mod.useIsMobileQuestDockRendered = orig; });
|
||||
return true;
|
||||
}
|
||||
33
plugins/ServerDrawer/src/utils/theme.ts
Normal file
33
plugins/ServerDrawer/src/utils/theme.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { findByProps, findByStoreName } from "@vendetta/metro";
|
||||
|
||||
const Flux = findByProps("useStateFromStores");
|
||||
const colors = findByProps("colors", "unsafe_rawColors")?.colors;
|
||||
const ExpandedGuildFolderStore = findByStoreName("ExpandedGuildFolderStore");
|
||||
|
||||
export function useTheme() {
|
||||
return {
|
||||
text: colors?.TEXT_NORMAL ?? "#dbdee1",
|
||||
folder: "#5865f2",
|
||||
hover: colors?.STATE_LAYER_PRESS ?? "rgba(255,255,255,0.06)",
|
||||
};
|
||||
}
|
||||
|
||||
export function useFolderExpanded(folderId: string | number): boolean {
|
||||
return Flux?.useStateFromStores?.(
|
||||
[ExpandedGuildFolderStore],
|
||||
() => {
|
||||
const folders = ExpandedGuildFolderStore?.getExpandedFolders?.();
|
||||
return folders instanceof Set ? folders.has(folderId) : false;
|
||||
},
|
||||
[folderId],
|
||||
) ?? false;
|
||||
}
|
||||
|
||||
export interface GuildNode {
|
||||
type: string;
|
||||
id: string | number;
|
||||
name?: string;
|
||||
color?: number | null;
|
||||
expanded?: boolean;
|
||||
children: GuildNode[];
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue