/* * Vencord, a Discord client mod * Copyright (c) 2025 Vendicated and contributors * SPDX-License-Identifier: GPL-3.0-or-later */ import ErrorBoundary from "@components/ErrorBoundary"; import { Logger } from "@utils/Logger"; import { findComponentByCodeLazy } from "@webpack"; import { useEffect, useState } from "@webpack/common"; import type { ComponentType, JSX, MouseEventHandler, ReactNode } from "react"; const logger = new Logger("HeaderBarAPI"); const HeaderBarIcon = findComponentByCodeLazy(".HEADER_BAR_BADGE_TOP:", '.iconBadge,"top"') as ComponentType; export interface HeaderBarButtonProps { /** The icon component to render inside the button */ icon: ComponentType; /** Tooltip text shown on hover. Pass null to disable tooltip */ tooltip: ReactNode; /** Called when the button is clicked */ onClick?: MouseEventHandler; /** Called when the button is right-clicked */ onContextMenu?: MouseEventHandler; /** Additional CSS class names */ className?: string; /** CSS class name for the icon element */ iconClassName?: string; /** Tooltip position relative to the button */ position?: "top" | "bottom" | "left" | "right"; /** Whether the button appears in a selected/active state */ selected?: boolean; /** Whether the button is disabled */ disabled?: boolean; /** Whether to show a notification badge */ showBadge?: boolean; /** Position of the notification badge */ badgePosition?: "top" | "bottom"; /** Size of the icon in pixels */ iconSize?: number; /** Ref to the button element */ ref?: React.RefObject; } export type HeaderBarButtonFactory = () => JSX.Element | null; export interface HeaderBarButtonData { /** Function that renders the button component */ render: HeaderBarButtonFactory; /** Icon component used for settings UI display */ icon: ComponentType; /** Higher priority buttons appear further right. Default: 0 */ priority?: number; /** Where to render the button. Default: "headerbar" */ location?: "headerbar" | "channeltoolbar"; } interface ButtonEntry { render: HeaderBarButtonFactory; priority: number; } /** * Button component for the top header bar (title bar area). * * @example * console.log("clicked")} * /> */ export const HeaderBarButton = HeaderBarIcon; /** * Button component for the channel toolbar (below the search bar). * Automatically handles selected state styling. * * @example * setOpen(v => !v)} * selected={isOpen} * /> */ export function ChannelToolbarButton(props: HeaderBarButtonProps) { return ; } const headerBarButtons = new Map(); const channelToolbarButtons = new Map(); const headerBarListeners = new Set<() => void>(); const channelToolbarListeners = new Set<() => void>(); /** * Adds a button to the header bar (title bar area). * * @param id - Unique identifier for the button (e.g., "my-plugin-button") * @param render - Function that returns the button JSX * @param priority - Higher values appear further right. Default: 0 * * @example * addHeaderBarButton("my-button", () => ( * * )); */ export function addHeaderBarButton(id: string, render: HeaderBarButtonFactory, priority = 0) { headerBarButtons.set(id, { render, priority }); headerBarListeners.forEach(listener => listener()); } /** * Removes a button from the header bar. * * @param id - The identifier used when adding the button */ export function removeHeaderBarButton(id: string) { headerBarButtons.delete(id); headerBarListeners.forEach(listener => listener()); } /** * Adds a button to the channel toolbar (below the search bar, next to pins/members). * * @param id - Unique identifier for the button (e.g., "my-plugin-toolbar") * @param render - Function that returns the button JSX * @param priority - Higher values appear further right. Default: 0 * * @example * addChannelToolbarButton("my-toolbar", () => ( * * )); */ export function addChannelToolbarButton(id: string, render: HeaderBarButtonFactory, priority = 0) { channelToolbarButtons.set(id, { render, priority }); channelToolbarListeners.forEach(listener => listener()); } /** * Removes a button from the channel toolbar. * * @param id - The identifier used when adding the button */ export function removeChannelToolbarButton(id: string) { channelToolbarButtons.delete(id); channelToolbarListeners.forEach(listener => listener()); } function HeaderBarButtons() { const [, forceUpdate] = useState(0); useEffect(() => { const listener = () => forceUpdate(n => n + 1); headerBarListeners.add(listener); return () => { headerBarListeners.delete(listener); }; }, []); return Array.from(headerBarButtons) .sort(([, a], [, b]) => a.priority - b.priority) .map(([id, { render: Button }]) => ( logger.error(`Failed to render header bar button: ${id}`, e.error)}>