GlobalBadges: rework

This commit is contained in:
thororen1234 2025-11-27 02:36:33 -05:00
parent b6d8728b64
commit b48ce27250
No known key found for this signature in database
11 changed files with 136 additions and 355 deletions

View file

@ -17,9 +17,12 @@
*/
import ErrorBoundary from "@components/ErrorBoundary";
import globalBadges from "@equicordplugins/globalBadges";
import BadgeAPIPlugin from "@plugins/_api/badges";
import { ComponentType, HTMLProps } from "react";
import { isPluginEnabled } from "./PluginManager";
export const enum BadgePosition {
START,
END
@ -99,6 +102,18 @@ export function _getBadges(args: BadgeUserArgs) {
const donorBadges = BadgeAPIPlugin.getDonorBadges(args.userId);
const equicordDonorBadges = BadgeAPIPlugin.getEquicordDonorBadges(args.userId);
const GlobalBadges = isPluginEnabled(globalBadges.name) ? globalBadges.getGlobalBadges(args.userId) : false;
// do globalbadges first so it shows before the contrib badges but after donor badges
if (GlobalBadges) {
badges.unshift(
...GlobalBadges.map(badge => ({
...args,
...badge,
}))
);
}
if (donorBadges) {
badges.unshift(
...donorBadges.map(badge => ({

View file

@ -1,79 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { React, Tooltip, UserStore } from "@webpack/common";
import { JSX } from "react";
import { openBadgeModal } from "./badgeModal";
import { settings } from "./settings";
import { BadgeCache } from "./types";
import { cl, fetchBadges } from "./utils";
interface BadgeModalItem {
name: string;
rawName: string;
}
export let badgeData;
export const BadgeComponent = ({ name, img }: { name: string, img: string; }) => {
return (
<Tooltip text={name} >
{(tooltipProps: any) => (
<img
{...tooltipProps}
src={img}
className={cl("badge")}
style={{
transform: name.includes("Replugged") ? "scale(0.9)" : null
}}
/>
)}
</Tooltip>
);
};
export const GlobalBadges = ({ userId }: { userId: string; }) => {
const [badges, setBadges] = React.useState<BadgeCache["badges"]>({});
React.useEffect(() => setBadges(fetchBadges(userId) ?? {}), [userId]);
if (!badges) return null;
const globalBadges: JSX.Element[] = [];
const badgeModal: BadgeModalItem[] = [];
Object.keys(badges).forEach(mod => {
if (!badges[mod] || !Array.isArray(badges[mod]) || badges[mod].length === 0) return;
badges[mod].forEach(badge => {
if (!badge || !badge.tooltip || !badge.badge) return;
const prefix = settings.store.showPrefix ? `(${mod})` : "";
const suffix = settings.store.showSuffix ? `(${mod})` : "";
const displayName = `${prefix} ${badge.tooltip} ${suffix}`;
if (mod === "BadgeVault") {
badge.custom = true;
}
globalBadges.push(<BadgeComponent name={displayName} img={badge.badge} />);
badgeModal.push({
name: displayName,
rawName: badge.tooltip,
...badge
});
});
});
badgeData = badgeModal;
return (
<div
className={cl("badges")}
onClick={_ => openBadgeModal(UserStore.getUser(userId))}
>
{globalBadges}
</div>
);
};

View file

@ -1,81 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Button } from "@components/Button";
import ErrorBoundary from "@components/ErrorBoundary";
import { HeadingTertiary } from "@components/Heading";
import { Paragraph } from "@components/Paragraph";
import { openInviteModal } from "@utils/discord";
import { ModalContent, ModalFooter, ModalHeader, ModalRoot, openModal } from "@utils/modal";
import { User } from "@vencord/discord-types";
import { React, Tooltip } from "@webpack/common";
import { badgeData } from "./badgeComponent";
import { cl, INVITE_LINK } from "./utils";
export const BadgeModalComponent = ({ name, img }: { name: string, img: string; }) => {
return (
<>
<Tooltip text={name} >
{(tooltipProps: any) => (
<img
className={cl("modal-badge")}
{...tooltipProps}
src={img}
/>
)}
</Tooltip>
</>
);
};
export function openBadgeModal(user: User) {
const badgeDataElements = badgeData.map(badge => (
<BadgeModalComponent key={badge.rawName} name={badge.name} img={badge.badge} />
));
openModal(modalprops => (
<ModalRoot {...modalprops}>
<ErrorBoundary>
<ModalHeader>
<img
className={cl("modal-avatar")}
src={user.getAvatarURL(undefined, 512, true)}
alt=""
/>
<HeadingTertiary className={cl("modal-name")}>
{user.username}
</HeadingTertiary>
</ModalHeader>
<ModalContent className={cl("modal-description")}>
{badgeDataElements.length ? (
<>
<Paragraph className={cl("modal-text")}>
{badgeDataElements.length} global badge
{badgeDataElements.length > 1 ? "s" : ""}.
</Paragraph>
<div className={cl("modal-badges")}>
{badgeDataElements}
</div>
</>
) : (
<Paragraph>
No global badges.
</Paragraph>
)}
</ModalContent>
<ModalFooter>
<Button
variant="link"
onClick={() => openInviteModal(INVITE_LINK)}
>
Join GlobalBadges Server
</Button>
</ModalFooter>
</ErrorBoundary>
</ModalRoot>
));
}

View file

@ -6,23 +6,57 @@
import "./styles.css";
import { addProfileBadge, BadgePosition, ProfileBadge, removeProfileBadge } from "@api/Badges";
import { BadgePosition, ProfileBadge } from "@api/Badges";
import { classNameFactory } from "@api/Styles";
import { Button } from "@components/Button";
import { BadgeContextMenu } from "@plugins/_api/badges";
import { Devs, EquicordDevs } from "@utils/constants";
import { openInviteModal } from "@utils/discord";
import definePlugin from "@utils/types";
import { React } from "@webpack/common";
import { ContextMenuApi, React } from "@webpack/common";
import { GlobalBadges } from "./badgeComponent";
import { settings } from "./settings";
import { cl, fetchBadges, INVITE_LINK } from "./utils";
const Badge: ProfileBadge = {
component: b => <GlobalBadges {...b} />,
position: BadgePosition.START,
shouldShow: userInfo => !!Object.keys(fetchBadges(userInfo.userId) ?? {}).length,
key: "GlobalBadges"
};
let GlobalBadges = {};
let intervalId: any;
const INVITE_LINK = "kwHCJPxp8t";
const cl = classNameFactory("vc-global-badges-");
async function loadBadges() {
const globalBadges = await fetch("https://badges.equicord.org/users", { cache: "no-cache" })
.then(r => r.json());
const filteredUsers: Record<string, typeof globalBadges.users[string]> = {};
for (const key in globalBadges.users) {
filteredUsers[key] = globalBadges.users[key].map(b => {
if (b.url) {
return { tooltip: b.label, badge: b.url };
}
return b;
}).filter(b => {
const url = b.badge;
// fix this when creations updates it
return url &&
url.startsWith("https://") &&
!url.startsWith("https://cdn.discordapp.com") &&
!url.startsWith("https://images.equicord.org") &&
!(url.startsWith("https://badges.vencord.dev/badges") && !url.startsWith("https://badges.vencord.dev/badges/reviewdb")) &&
// !(!settings.store.showAero && url.startsWith(":aero_icon:")) &&
// !(!settings.store.showVelocity && url.startsWith("Velocity")) &&
// !(!settings.store.showVelocity && url.startsWith("Official Velocity")) &&
!(!settings.store.showCustom && url.startsWith("https://gb.obamabot.me")) &&
!(!settings.store.showNekocord && url.startsWith("https://nekocord.dev")) &&
!(!settings.store.showReviewDB && url.startsWith("https://badges.vencord.dev/badges/reviewdb")) &&
!(!settings.store.showAliucord && url.startsWith("https://aliucord.com")) &&
!(!settings.store.showRa1ncord && url.startsWith("https://codeberg.org/raincord/badges")) &&
!(!settings.store.showEnmity && url.startsWith("https://raw.githubusercontent.com/enmity-mod/badges"));
});
}
GlobalBadges = filteredUsers;
}
export default definePlugin({
name: "GlobalBadges",
@ -40,6 +74,32 @@ export default definePlugin({
</Button>
</>
),
start: () => addProfileBadge(Badge),
stop: () => removeProfileBadge(Badge),
async start() {
await loadBadges();
clearInterval(intervalId);
intervalId = setInterval(loadBadges, 1000 * 60 * 30);
},
async stop() {
clearInterval(intervalId);
},
get GlobalBadges() {
return GlobalBadges;
},
getGlobalBadges(userId: string) {
return GlobalBadges[userId]?.map(badge => ({
iconSrc: badge.badge,
description: badge.tooltip,
position: BadgePosition.START,
props: {
style: {
borderRadius: "50%",
transform: "scale(0.9)"
}
},
onContextMenu(event, badge) {
ContextMenuApi.openContextMenu(event, () => <BadgeContextMenu badge={badge} />);
},
} satisfies ProfileBadge));
}
});

View file

@ -78,12 +78,6 @@ export const settings = definePluginSettings({
default: true,
restartNeeded: false
},
showReplugged: {
type: OptionType.BOOLEAN,
description: "Show Replugged Badges",
default: true,
restartNeeded: false
},
apiUrl: {
type: OptionType.STRING,
description: "API to use",

View file

@ -1,61 +1,3 @@
.vc-global-badges-badges {
align-items: center;
display: flex;
flex-wrap: nowrap;
}
.vc-global-badges-badge {
width: 20px;
height: 20px;
gap: 1px;
}
.vc-global-badges-modal-name {
text-transform: none;
flex-grow: 0;
background: var(--background-base-lowest);
border-radius: 0 9999px 9999px 0;
padding: 6px 0.8em 6px 0.5em;
font-size: 20px;
height: 20px;
position: relative;
text-wrap: nowrap;
}
.vc-global-badges-modal-name::before {
content: "";
display: block;
position: absolute;
height: 100%;
width: 32px;
background: var(--background-base-lowest);
z-index: -1;
left: -32px;
top: 0;
border-top-left-radius: 9999px;
border-bottom-left-radius: 9999px;
}
.vc-global-badges-modal-avatar {
height: 48px;
width: 48px;
border-radius: 50%;
}
.vc-global-badges-modal-description {
font-size: 16px;
}
.vc-global-badges-modal-text {
margin-left: 8px;
}
.vc-global-badges-modal-badge {
width: 50px;
height: 50px;
margin: 2px;
}
.vc-global-badges-settings-button {
width: 100%;
height: 50px;

View file

@ -1,16 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export type CustomBadge = {
tooltip: string;
badge: string;
custom?: boolean;
};
export interface BadgeCache {
badges: { [mod: string]: CustomBadge[]; };
expires: number;
}

View file

@ -1,47 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { classNameFactory } from "@api/Styles";
import { settings } from "./settings";
import { BadgeCache } from "./types";
const cache = new Map<string, BadgeCache>();
const EXPIRES = 1000 * 60 * 15;
export const cl = classNameFactory("vc-global-badges-");
export const INVITE_LINK = "kwHCJPxp8t";
export const fetchBadges = (id: string): BadgeCache["badges"] | undefined => {
const cachedValue = cache.get(id);
if (!cache.has(id) || (cachedValue && cachedValue.expires < Date.now())) {
const services: string[] = [];
if (settings.store.showNekocord) services.push("nekocord");
if (settings.store.showReviewDB) services.push("reviewdb");
if (settings.store.showAero) services.push("aero");
if (settings.store.showAliucord) services.push("aliucord");
if (settings.store.showRa1ncord) services.push("ra1ncord");
if (settings.store.showVelocity) services.push("velocity");
if (settings.store.showEnmity) services.push("enmity");
if (settings.store.showReplugged) services.push("replugged");
if (settings.store.showCustom) services.push("badgevault");
if (services.length === 0) {
cache.set(id, { badges: {}, expires: Date.now() + EXPIRES });
return {};
}
fetch(`${settings.store.apiUrl}${id}?seperated=true&capitalize=true&services=${services.join(",")}`)
.then(res => res.json() as Promise<{ status: number; badges: BadgeCache["badges"]; }>)
.then(body => {
cache.set(id, { badges: body.badges, expires: Date.now() + EXPIRES });
return body.badges;
})
.catch(() => null);
} else if (cachedValue) {
return cachedValue.badges;
}
};

View file

@ -6,7 +6,7 @@
import { classNameFactory } from "@api/Styles";
import { BaseText } from "@components/BaseText";
import { Flex } from "@components/Flex";
import { HeadingPrimary } from "@components/Heading";
import { Paragraph } from "@components/Paragraph";
import { openUserProfile } from "@utils/discord";
import { Avatar, Clickable, React, TextInput, Tooltip, } from "@webpack/common";
@ -44,11 +44,9 @@ function UsersCollectionRows({ usersCollection }: { usersCollection: Data["users
.map(({ name, users }) => (
<aside key={name}>
<div className={cl("header-container")}>
<BaseText>{name.toUpperCase()}</BaseText>
<HeadingPrimary className={cl("header-name")}>{name}</HeadingPrimary>
<div className={cl("header-btns")}>
<Flex>
{users.map(u => <UserRow key={u.id} user={u} />)}
</Flex>
{users.map(u => <UserRow key={u.id} user={u} />)}
</div>
</div>
</aside>
@ -59,14 +57,14 @@ function UsersCollectionRows({ usersCollection }: { usersCollection: Data["users
function UserRow({ user, allowOwner = true }: { user: IStorageUser, allowOwner?: boolean; }) {
return (
<Flex key={user.id} className={cl("user-row")}>
<span className={cl("user")}>
<Flex>
<Clickable onClick={() => openUserProfile(user.id)}>
<span className={cl("user-avatar")}>
<Avatar src={user.iconURL} size="SIZE_24" />
</span>
</Clickable>
<div key={user.id} className={cl("user-row")}>
<div className={cl("user")}>
<Clickable onClick={() => openUserProfile(user.id)}>
<span className={cl("user-avatar")}>
<Avatar src={user.iconURL} size="SIZE_24" />
</span>
</Clickable>
<div className={cl("user-tooltip")}>
<Tooltip text={tooltipText(user)}>
{props =>
<Paragraph {...props} className={cl("user-username")}>
@ -74,15 +72,14 @@ function UserRow({ user, allowOwner = true }: { user: IStorageUser, allowOwner?:
</Paragraph>
}
</Tooltip>
</Flex>
</span>
<span className={cl("user-id")}>
<Paragraph>
{user.id}
</Paragraph>
</span>
</Flex>
<span className={cl("user-id")}>
<Paragraph>
{user.id}
</Paragraph>
</span>
</div>
</div>
</div>
);
}
@ -94,10 +91,10 @@ function SearchElement({ usersCollection }: { usersCollection: Data["usersCollec
<section className={cl("search")}>
<TextInput placeholder="Filter by tag, username" name="Filter" onChange={setCurrent} />
{current && (
<Flex className={cl("search-user")}>
<div className={cl("search-user")}>
{list.filter(user => user.tag.includes(current) || user.username.includes(current))
.map(user => <UserRow key={user.id} user={user} allowOwner={false} />)}
</Flex>
</div>
)}
</section>
);
@ -112,9 +109,9 @@ export function DataUI({ plugin, usersCollection }: { plugin: any; usersCollecti
<SectionDescription />
<SearchElement usersCollection={usersCollection} />
<Flex className={cl("rows")}>
<div className={cl("rows")}>
<UsersCollectionRows usersCollection={usersCollection} />
</Flex>
</div>
</main>
);
}

View file

@ -5,8 +5,15 @@
flex-direction: column;
}
.vc-i-remember-you-header-name {
margin-top: 16px;
}
.vc-i-remember-you-header-btns {
gap: 8px;
display: grid;
grid-template-columns: repeat(2, minmax(0, 350px));
gap: 10px;
margin-top: 8px;
}
.vc-i-remember-you-header {
@ -29,49 +36,38 @@
.vc-i-remember-you-user-avatar {
cursor: pointer;
margin-right: 30px;
margin-right: 0;
}
.vc-i-remember-you-user-row {
margin: 0;
width: 100%;
flex-wrap: wrap;
align-items: center;
display: flex;
flex-direction: column;
gap: 6px;
padding: 4px;
align-items: flex-start;
}
.vc-i-remember-you-user-id {
height: min-content;
align-self: flex-start;
opacity: 0.75;
height: min-content;
}
.vc-i-remember-you-user {
width: 500px;
gap: 250px;
display: flex;
flex-direction: row;
align-items: center;
overflow-wrap: break-word;
margin-top: 5px;
gap: 8px;
width: 100%;
margin-top: 0;
}
.vc-i-remember-you-user-username {
margin-top: 2px;
}
.vc-i-remember-you-buttons {
margin-top: 10px;
}
.vc-i-remember-you-buttons-save,
.vc-i-remember-you-buttons-cancel {
margin-left: 490px;
margin-top: 5px;
margin-bottom: 10px;
}
.vc-i-remember-you-buttons-cancel {
margin-left: 12px;
}
.vc-i-remember-you-buttons-reset {
cursor: pointer;
margin-left: 12px;
.vc-i-remember-you-user-tooltip {
display: flex;
flex-direction: column;
align-items: flex-start;
}

View file

@ -100,7 +100,7 @@ async function loadAllBadges(noCache = false) {
let intervalId: any;
function BadgeContextMenu({ badge }: { badge: ProfileBadge & BadgeUserArgs; }) {
export function BadgeContextMenu({ badge }: { badge: ProfileBadge & BadgeUserArgs; }) {
return (
<Menu.Menu
navId="vc-badge-context"