feat(HeaderBarAPI): ability to add buttons to ChannelToolbar (#551)

This commit is contained in:
prism 2025-12-10 14:35:29 +01:00 • committed by GitHub
parent 4d504f0d26
commit 9d94430dfc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 69 additions and 10 deletions

View file

@ -14,10 +14,13 @@ const logger = new Logger("HeaderBarAPI");
export type HeaderBarButtonFactory = () => JSX.Element | null;
export type HeaderBarButtonLocation = "headerbar" | "channeltoolbar";
export interface HeaderBarButtonData {
render: HeaderBarButtonFactory;
icon: ComponentType<any>;
priority?: number;
location?: HeaderBarButtonLocation;
}
interface HeaderBarButton {
@ -26,8 +29,10 @@ interface HeaderBarButton {
}
export const buttons = new Map<string, HeaderBarButton>();
export const channelToolbarButtons = new Map<string, HeaderBarButton>();
const listeners = new Set<() => void>();
const channelToolbarListeners = new Set<() => void>();
export interface HeaderBarButtonProps {
icon: ComponentType<any>;
@ -41,13 +46,15 @@ export interface HeaderBarButtonProps {
disabled?: boolean;
showBadge?: boolean;
badgePosition?: "top" | "bottom";
iconSize?: number;
ref?: RefObject<any>;
}
export const HeaderBarButton = findComponentByCodeLazy(".HEADER_BAR_BADGE_TOP:", '.iconBadge,"top"') as ComponentType<HeaderBarButtonProps>;
export const ChannelToolbarButton = HeaderBarButton;
/**
* Add a button to the header bar.
* @param id Unique identifier for the button.
* @param render Function that renders the button component.
* @param priority Higher priority = more to the right. Default is 0.
@ -57,15 +64,26 @@ export function addHeaderBarButton(id: string, render: HeaderBarButtonFactory, p
listeners.forEach(l => l());
}
/**
* Remove a button from the header bar.
* @param id The identifier of the button to remove.
*/
export function removeHeaderBarButton(id: string) {
buttons.delete(id);
listeners.forEach(l => l());
}
/**
* @param id Unique identifier for the button.
* @param render Function that renders the button component.
* @param priority Higher priority = more to the right. Default is 0.
*/
export function addChannelToolbarButton(id: string, render: HeaderBarButtonFactory, priority = 0) {
channelToolbarButtons.set(id, { render, priority });
channelToolbarListeners.forEach(l => l());
}
export function removeChannelToolbarButton(id: string) {
channelToolbarButtons.delete(id);
channelToolbarListeners.forEach(l => l());
}
function HeaderBarButtons() {
const [, forceUpdate] = useState(0);
@ -84,6 +102,28 @@ function HeaderBarButtons() {
));
}
function ChannelToolbarButtons() {
const [, forceUpdate] = useState(0);
useEffect(() => {
const listener = () => forceUpdate(n => n + 1);
channelToolbarListeners.add(listener);
return () => { channelToolbarListeners.delete(listener); };
}, []);
const sorted = Array.from(channelToolbarButtons).sort(([, a], [, b]) => a.priority - b.priority);
return sorted.map(([id, { render: Button }]) => (
<ErrorBoundary noop key={id} onError={e => logger.error(`Failed to render channel toolbar ${id}`, e.error)}>
<Button />
</ErrorBoundary>
));
}
export function _addButtons() {
return [<HeaderBarButtons key="vc-header-bar-buttons" />];
}
export function _addChannelToolbarButtons(toolbar: ReactNode[]) {
toolbar.push(<ChannelToolbarButtons key="vc-channel-toolbar-buttons" />);
}

View file

@ -41,7 +41,7 @@ export { Plugins as plugins };
import { traceFunction } from "../debug/Tracer";
import { addAudioProcessor, removeAudioProcessor } from "./AudioPlayer";
import { addHeaderBarButton, removeHeaderBarButton } from "./HeaderBar";
import { addChannelToolbarButton, addHeaderBarButton, removeChannelToolbarButton, removeHeaderBarButton } from "./HeaderBar";
const logger = new Logger("PluginManager", "#a6d189");
@ -253,7 +253,13 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p:
// Custom
if (renderNicknameIcon) addNicknameIcon(name, renderNicknameIcon);
if (headerBarButton) addHeaderBarButton(name, headerBarButton.render, headerBarButton.priority);
if (headerBarButton) {
if (headerBarButton.location === "channeltoolbar") {
addChannelToolbarButton(name, headerBarButton.render, headerBarButton.priority);
} else {
addHeaderBarButton(name, headerBarButton.render, headerBarButton.priority);
}
}
if (audioProcessor) addAudioProcessor(name, audioProcessor);
return true;
@ -321,7 +327,13 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu
// Custom
if (renderNicknameIcon) removeNicknameIcon(name);
if (headerBarButton) removeHeaderBarButton(name);
if (headerBarButton) {
if (headerBarButton.location === "channeltoolbar") {
removeChannelToolbarButton(name);
} else {
removeHeaderBarButton(name);
}
}
if (audioProcessor) removeAudioProcessor(name);
return true;

View file

@ -131,7 +131,7 @@ export const ContextMenu = $ContextMenu;
export const ChatButtons = $ChatButtons;
/**
* An API allowing you to add buttons to the header bar.
* An API allowing you to add buttons to the header bar or channel toolbar.
*/
export const HeaderBar = $HeaderBar;

View file

@ -9,7 +9,7 @@ import definePlugin from "@utils/types";
export default definePlugin({
name: "HeaderBarAPI",
description: "API to add buttons to the header bar.",
description: "API to add buttons to the header bar and channel toolbar.",
authors: [EquicordDevs.Prism],
patches: [
@ -19,6 +19,13 @@ export default definePlugin({
match: /(?<=\i\.Fragment,\{children:\[.{1,500}className:\i\}\))(?=\])/,
replace: ",...Vencord.Api.HeaderBar._addButtons()"
}
},
{
find: "Missing channel in Channel.renderHeaderToolbar",
replacement: {
match: /(?<=renderHeaderToolbar",\(\)=>\{.{1,200}let (\i)=\[\];)/,
replace: "Vencord.Api.HeaderBar._addChannelToolbarButtons($1);"
}
}
]
});