feat(HeaderBarAPI): improvements & fixes (#538)

* feat(HeaderBarAPI): improvements & fixes

* t

* lint

* unused

* Update index.tsx

* Changelog: add for settings v2

---------

Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com>
This commit is contained in:
prism 2025-12-06 06:18:27 +01:00 • committed by GitHub
parent 862c43e60d
commit 7a737bc138
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 122 additions and 85 deletions

View file

@ -5,11 +5,21 @@
*/
import ErrorBoundary from "@components/ErrorBoundary";
import { Logger } from "@utils/Logger";
import { findComponentByCodeLazy } from "@webpack";
import { useEffect, useState } from "@webpack/common";
import type { JSX } from "react";
import type { ComponentType, JSX, MouseEventHandler, ReactNode, RefObject } from "react";
const logger = new Logger("HeaderBarAPI");
export type HeaderBarButtonFactory = () => JSX.Element | null;
export interface HeaderBarButtonData {
render: HeaderBarButtonFactory;
icon: ComponentType<any>;
priority?: number;
}
interface HeaderBarButton {
render: HeaderBarButtonFactory;
priority: number;
@ -19,6 +29,23 @@ export const buttons = new Map<string, HeaderBarButton>();
const listeners = new Set<() => void>();
export interface HeaderBarButtonProps {
icon: ComponentType<any>;
tooltip: ReactNode;
onClick?: MouseEventHandler<HTMLDivElement>;
onContextMenu?: MouseEventHandler<HTMLDivElement>;
className?: string;
iconClassName?: string;
position?: "top" | "bottom" | "left" | "right";
selected?: boolean;
disabled?: boolean;
showBadge?: boolean;
badgePosition?: "top" | "bottom";
ref?: RefObject<any>;
}
export const HeaderBarButton = findComponentByCodeLazy(".HEADER_BAR_BADGE_TOP:", '.iconBadge,"top"') as ComponentType<HeaderBarButtonProps>;
/**
* Add a button to the header bar.
* @param id Unique identifier for the button.
@ -51,7 +78,7 @@ function HeaderBarButtons() {
const sorted = Array.from(buttons).sort(([, a], [, b]) => a.priority - b.priority);
return sorted.map(([id, { render: Button }]) => (
<ErrorBoundary noop key={id}>
<ErrorBoundary noop key={id} onError={e => logger.error(`Failed to render ${id}`, e.error)}>
<Button />
</ErrorBoundary>
));

View file

@ -191,7 +191,7 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p:
onBeforeMessageEdit, onBeforeMessageSend, onMessageClick,
renderChatBarButton, chatBarButton, renderMemberListDecorator, renderMessageAccessory, renderMessageDecoration, renderMessagePopoverButton, messagePopoverButton,
// Custom
renderNicknameIcon, renderHeaderBarButton, audioProcessor
renderNicknameIcon, headerBarButton, audioProcessor
} = p;
if (p.start) {
@ -253,7 +253,7 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p:
// Custom
if (renderNicknameIcon) addNicknameIcon(name, renderNicknameIcon);
if (renderHeaderBarButton) addHeaderBarButton(name, renderHeaderBarButton);
if (headerBarButton) addHeaderBarButton(name, headerBarButton.render, headerBarButton.priority);
if (audioProcessor) addAudioProcessor(name, audioProcessor);
return true;
@ -265,7 +265,7 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu
onBeforeMessageEdit, onBeforeMessageSend, onMessageClick,
renderChatBarButton, chatBarButton, renderMemberListDecorator, renderMessageAccessory, renderMessageDecoration, renderMessagePopoverButton, messagePopoverButton,
// Custom
renderNicknameIcon, renderHeaderBarButton, audioProcessor
renderNicknameIcon, headerBarButton, audioProcessor
} = p;
if (p.stop) {
@ -321,7 +321,7 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu
// Custom
if (renderNicknameIcon) removeNicknameIcon(name);
if (renderHeaderBarButton) removeHeaderBarButton(name);
if (headerBarButton) removeHeaderBarButton(name);
if (audioProcessor) removeAudioProcessor(name);
return true;
@ -335,7 +335,7 @@ export const initPluginManager = onlyOnce(function init() {
"onBeforeMessageEdit", "onBeforeMessageSend", "onMessageClick",
"renderChatBarButton", "renderMemberListDecorator", "renderMessageAccessory", "renderMessageDecoration", "renderMessagePopoverButton",
// Custom
"renderNicknameIcon", "renderHeaderBarButton"
"renderNicknameIcon"
];
const neededApiPlugins = new Set<string>();
@ -374,7 +374,7 @@ export const initPluginManager = onlyOnce(function init() {
// Custom
if (p.renderNicknameIcon) neededApiPlugins.add("NicknameIconsAPI");
if (p.renderHeaderBarButton) neededApiPlugins.add("HeaderBarAPI");
if (p.headerBarButton) neededApiPlugins.add("HeaderBarAPI");
if (p.audioProcessor) neededApiPlugins.add("AudioPlayerAPI");
for (const key of pluginKeysToBind) {

View file

@ -705,7 +705,7 @@ function ChangelogContent() {
function ChangelogTab() {
return (
<SettingsTab title="Changelog">
<SettingsTab>
<ChangelogContent />
</SettingsTab>
);

View file

@ -20,6 +20,7 @@ import { useSettings } from "@api/Settings";
import { authorizeCloud, deauthorizeCloud } from "@api/SettingsSync/cloudSetup";
import { deleteCloudSettings, eraseAllCloudData, getCloudSettings, putCloudSettings } from "@api/SettingsSync/cloudSync";
import { Button } from "@components/Button";
import { Card } from "@components/Card";
import { CheckedTextInput } from "@components/CheckedTextInput";
import { Divider } from "@components/Divider";
import { FormSwitch } from "@components/FormSwitch";
@ -111,19 +112,19 @@ function CloudTab() {
return (
<SettingsTab>
<section className={Margins.top16}>
<Heading>Cloud Settings</Heading>
<Paragraph size="md" className={Margins.bottom20}>
Equicord comes with a cloud integration allowing settings to be synced across apps and devices.
<br />
We use our own <Link href="https://github.com/Equicord/Equicloud">Equicloud backend</Link> to provide our cloud instance with enhanced features.
<br />
Our <Link href="https://equicord.org/cloud/policy">privacy policy</Link> allows you to see what information we store, how we use it, and data retention.
<br />
Equicloud is BSD 3.0 licensed so you can host it yourself if you would like.
<br />
You can swap between Equicord and Vencord's different cloud instances below if needed.
</Paragraph>
<Card defaultPadding={true} className={Margins.bottom16}>
<Paragraph size="md">
Equicord comes with a cloud integration allowing settings to be synced across apps and devices.
<br />
We use our own <Link href="https://github.com/Equicord/Equicloud">Equicloud backend</Link> to provide our cloud instance with enhanced features.
<br />
Our <Link href="https://equicord.org/cloud/policy">privacy policy</Link> allows you to see what information we store, how we use it, and data retention.
<br />
Equicloud is BSD 3.0 licensed so you can host it yourself if you would like.
<br />
You can swap between Equicord and Vencord's different cloud instances below if needed.
</Paragraph>
</Card>
<FormSwitch
key="backend"
title="Enable Cloud Integrations"

View file

@ -18,17 +18,14 @@
import "./styles.css";
import { addHeaderBarButton, removeHeaderBarButton } from "@api/HeaderBar";
import { HeaderBarButton } from "@api/HeaderBar";
import { definePluginSettings, migratePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findComponentByCodeLazy } from "@webpack";
import { Popout, useRef, useState } from "@webpack/common";
import { renderPopout } from "./menu";
const HeaderBarIcon = findComponentByCodeLazy(".HEADER_BAR_BADGE_TOP:", '.iconBadge,"top"');
export const settings = definePluginSettings({
showPluginMenu: {
type: OptionType.BOOLEAN,
@ -37,9 +34,9 @@ export const settings = definePluginSettings({
}
});
function Icon() {
function Icon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 443 443" width={20} height={20} className="vc-toolbox-icon">
<svg viewBox="0 0 443 443" width={20} height={20} {...props}>
<path fill="currentColor" d="M221.5.6C99.2.6,0,99.7,0,222.1s99.2,221.5,221.5,221.5,221.5-99.2,221.5-221.5S343.8.6,221.5.6ZM221.5,363.3c-78.3,0-141.8-63.5-141.8-141.8s63.5-141.8,141.8-141.8,141.8,63.5,141.8,141.8-63.5,141.8-141.8,141.8Z" />
<path fill="currentColor" d="M438.3,175C421,90.1,354.7,23.1,269.9,5.1,145-21.3,33.1,57.6,6.2,169.5c-3.4,14.1,4.6,28.5,18.4,33.1l2.7.9c24.7,8.2,51.8-4.7,60.4-29.3,19.4-55.6,72.5-95.4,134.9-95,44.3.3,84.1,21.2,109.9,53.7l-63.3,29.5c-7.9-7-17.5-12.5-28.5-15.7-35.6-10.3-73.8,7.7-88.5,41.7-17.2,39.8,3.3,85.4,43.9,99.4,37.9,13,79.7-6.9,93.5-44.5,2.6-7.2,4.1-14.5,4.4-21.8l66-30.8c1.8,8.2,2.9,16.6,3.2,25.2,1.2,34-9.6,65.5-28.5,90.5-17.8,23.6-10.5,57.4,15.7,71.1l1.3.7c10.4,5.5,23.3,3.4,31.4-5.2,46.8-49.9,70.8-121.4,55.2-198Z" />
<path fill="currentColor" transform="translate(319.7 -16.5) rotate(65)" d="M172.8,152.7h0c17.7,0,32,14.3,32,32v148h-64v-148c0-17.7,14.3-32,32-32Z" />
@ -63,7 +60,7 @@ function VencordPopoutButton() {
renderPopout={() => renderPopout(() => setShow(false))}
>
{(_, { isShown }) => (
<HeaderBarIcon
<HeaderBarButton
ref={buttonRef}
className="vc-toolbox-btn"
onClick={() => setShow(v => !v)}
@ -85,11 +82,9 @@ export default definePlugin({
settings,
start() {
addHeaderBarButton("EquicordToolbox", VencordPopoutButton, 1000);
},
stop() {
removeHeaderBarButton("EquicordToolbox");
headerBarButton: {
icon: Icon,
render: VencordPopoutButton,
priority: 1337
}
});

View file

@ -1,16 +1,3 @@
.vc-toolbox-btn,
.vc-toolbox-icon {
.vc-toolbox-btn {
-webkit-app-region: no-drag;
}
.vc-toolbox-icon {
color: var(--interactive-normal);
}
.vc-toolbox-btn[class*="selected"] .vc-toolbox-icon {
color: var(--interactive-active);
}
.vc-toolbox-btn:hover .vc-toolbox-icon {
color: var(--interactive-hover);
}

View file

@ -19,13 +19,14 @@
import "./style.css";
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { HeaderBarButton } from "@api/HeaderBar";
import { DataStore } from "@api/index";
import { EquicordDevs } from "@utils/constants";
import { classes } from "@utils/misc";
import { openModal } from "@utils/modal";
import definePlugin from "@utils/types";
import { Message } from "@vencord/discord-types";
import { findByCodeLazy, findByProps, findComponentByCodeLazy } from "@webpack";
import { findByCodeLazy, findByProps } from "@webpack";
import { ChannelStore, Menu } from "@webpack/common";
import { Popover as NoteButtonPopover, Popover } from "./components/icons/NoteButton";
@ -35,8 +36,6 @@ import { DataStoreToCache, HolyNoteStore } from "./utils";
export const MessageType = findByCodeLazy("isEdited(){");
const HeaderBarIcon = findComponentByCodeLazy(".HEADER_BAR_BADGE_TOP:", '.iconBadge,"top"');
const messageContextMenuPatch: NavContextMenuPatchCallback = async (children, { message }: { message: Message; }) => {
children.push(
<Menu.MenuItem label="Add Message To" id="add-message-to-note">
@ -56,7 +55,7 @@ function ToolBarHeader() {
const iconClasses = findByProps("iconWrapper", "clickable");
return (
<HeaderBarIcon
<HeaderBarButton
tooltip="Holy Notes"
position="bottom"
className={classes("vc-note-button", iconClasses.iconWrapper, iconClasses.clickable)}
@ -82,7 +81,10 @@ export default definePlugin({
"message": messageContextMenuPatch
},
renderHeaderBarButton: ToolBarHeader,
headerBarButton: {
icon: Popover,
render: ToolBarHeader
},
messagePopoverButton: {
icon: NoteButtonPopover,

View file

@ -16,16 +16,16 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { HeaderBarButton } from "@api/HeaderBar";
import { findComponentByCodeLazy } from "@webpack";
import { openLogModal } from "./LogsModal";
const HeaderBarIcon = findComponentByCodeLazy(".HEADER_BAR_BADGE_TOP:", '.iconBadge,"top"');
const Icon = findComponentByCodeLazy("0-1.27-.97l-2.5.7a3");
export function OpenLogsButton() {
return (
<HeaderBarIcon
<HeaderBarButton
className="vc-log-toolbox-btn"
onClick={() => openLogModal()}
tooltip={"Open Logs"}
@ -33,3 +33,5 @@ export function OpenLogsButton() {
/>
);
}
export { Icon as LogsIcon };

View file

@ -14,7 +14,7 @@ import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { FluxDispatcher, MessageStore, UserStore } from "@webpack/common";
import { OpenLogsButton } from "./components/LogsButton";
import { LogsIcon, OpenLogsButton } from "./components/LogsButton";
import { openLogModal } from "./components/LogsModal";
import * as idb from "./db";
import { addMessage } from "./LoggedMessageManager";
@ -300,9 +300,13 @@ export default definePlugin({
openLogModal();
}
},
renderHeaderBarButton() {
if (!settings.store.ShowLogsButton) return null;
return OpenLogsButton();
headerBarButton: {
icon: LogsIcon,
render() {
if (!settings.store.ShowLogsButton) return null;
return OpenLogsButton();
}
},
processMessageFetch,

View file

@ -18,6 +18,7 @@
import "./style.css";
import { HeaderBarButton } from "@api/HeaderBar";
import { showNotification } from "@api/Notifications";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
@ -27,7 +28,6 @@ import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpac
import { ChannelStore, FluxDispatcher, GuildChannelStore, NavigationRouter, RestAPI, UserStore } from "@webpack/common";
const QuestIcon = findComponentByCodeLazy("10.47a.76.76");
const HeaderBarIcon = findComponentByCodeLazy(".HEADER_BAR_BADGE_TOP:", '.iconBadge,"top"');
const ApplicationStreamingStore = findStoreLazy("ApplicationStreamingStore");
const RunningGameStore = findStoreLazy("RunningGameStore");
const QuestsStore = findByPropsLazy("getQuest");
@ -36,7 +36,7 @@ let questIdCheck = 0;
function ToolBarHeader() {
return (
<HeaderBarIcon
<HeaderBarButton
tooltip="Complete Quest"
position="bottom"
className="vc-quest-completer"
@ -239,5 +239,8 @@ export default definePlugin({
}
});
},
renderHeaderBarButton: ToolBarHeader
headerBarButton: {
icon: QuestIcon,
render: ToolBarHeader
}
});

View file

@ -7,6 +7,7 @@
import "./styles.css";
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { HeaderBarButton } from "@api/HeaderBar";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { getCurrentChannel } from "@utils/discord";
@ -46,15 +47,10 @@ import {
import { settings, SidebarStore } from "./store";
// ??? no clue why this HeaderBarIcon doesnt work, its the same as the one below
const { HeaderBar, /* HeaderBarIcon*/ } = mapMangledModuleLazy(".themedMobile]:", {
HeaderBarIcon: filters.componentByCode('size:"custom",'),
const { HeaderBar } = mapMangledModuleLazy(".themedMobile]:", {
HeaderBar: filters.byCode(".themedMobile]:"),
});
// from toolbox
const HeaderBarIcon = findComponentByCodeLazy(".HEADER_BAR_BADGE_TOP:", '.iconBadge,"top"');
const { ForumView } = mapMangledModuleLazy("forum-grid-header-section-", {
ForumView: filters.byCode("sidebarState")
});
@ -314,9 +310,9 @@ const Header = ({ guild, channel }: { guild: Guild; channel: Channel; }) => {
<HeaderBar
toolbar={
<>
<HeaderBarIcon icon={ArrowsLeftRightIcon} tooltip="Switch channels" onClick={switchChannels} />
<HeaderBarIcon icon={WindowLaunchIcon} tooltip="Popout Chat" onClick={openPopout} />
<HeaderBarIcon icon={XSmallIcon} tooltip="Close Sidebar Chat" onClick={closeSidebar} />
<HeaderBarButton icon={ArrowsLeftRightIcon} tooltip="Switch channels" onClick={switchChannels} />
<HeaderBarButton icon={WindowLaunchIcon} tooltip="Popout Chat" onClick={openPopout} />
<HeaderBarButton icon={XSmallIcon} tooltip="Close Sidebar Chat" onClick={closeSidebar} />
</>
}
>

View file

@ -5,14 +5,12 @@
*/
import { AudioPlayerInterface, createAudioPlayer } from "@api/AudioPlayer";
import { HeaderBarButton } from "@api/HeaderBar";
import { definePluginSettings } from "@api/Settings";
import { EquicordDevs } from "@utils/constants";
import { ModalProps, ModalRoot, openModal } from "@utils/modal";
import definePlugin, { OptionType } from "@utils/types";
import { findComponentByCodeLazy } from "@webpack";
import { React } from "@webpack/common";
const HeaderBarIcon = findComponentByCodeLazy(".HEADER_BAR_BADGE_TOP:", '.iconBadge,"top"');
let boopSound: AudioPlayerInterface;
let song: AudioPlayerInterface;
@ -73,7 +71,7 @@ function buildSoggyModal(): any {
function SoggyButton() {
return (
<HeaderBarIcon
<HeaderBarButton
className="soggy-button"
tooltip={settings.store.tooltipText}
icon={() => (
@ -142,7 +140,20 @@ export default definePlugin({
authors: [EquicordDevs.sliwka],
settings,
dependencies: ["AudioPlayerAPI"],
renderHeaderBarButton: SoggyButton,
headerBarButton: {
icon: () => (
<img
alt=""
src={settings.store.imageLink}
width={24}
height={24}
draggable={false}
style={{ pointerEvents: "none" }}
/>
),
render: SoggyButton
},
start() {
assignBoop(settings.store.boopLink, settings.store.boopVolume * 100);

View file

@ -4,6 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { HeaderBarButton } from "@api/HeaderBar";
import { BaseText } from "@components/BaseText";
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
@ -15,15 +16,12 @@ import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { User } from "@vencord/discord-types";
import { findComponentByCodeLazy } from "@webpack";
import { Button, Clickable, Tooltip, useEffect, UserUtils, useState } from "@webpack/common";
import { LogIcon } from "./Icons";
import { openMoreUsersModal } from "./MoreUsersModal";
import { openUserModal } from "./UserModal";
const HeaderBarIcon = findComponentByCodeLazy(".HEADER_BAR_BADGE_TOP:", '.iconBadge,"top"');
export async function openSoundBoardLog(): Promise<void> {
const data = await getLoggedSounds();
@ -37,7 +35,7 @@ export async function openSoundBoardLog(): Promise<void> {
export function OpenSBLogsButton() {
return (
<HeaderBarIcon
<HeaderBarButton
className="chatBarLogIcon"
onClick={() => openSoundBoardLog()}
tooltip={"Open SoundBoard Log"}

View file

@ -9,6 +9,7 @@ import { Devs, EquicordDevs } from "@utils/constants";
import definePlugin from "@utils/types";
import { FluxDispatcher } from "@webpack/common";
import { LogIcon } from "./components/Icons";
import { OpenSBLogsButton } from "./components/SoundBoardLog";
import settings from "./settings";
import { updateLoggedSounds } from "./store";
@ -21,7 +22,11 @@ export default definePlugin({
description: "Logs all soundboards that are played in a voice chat and allows you to download them",
dependencies: ["AudioPlayerAPI"],
settings,
renderHeaderBarButton: OpenSBLogsButton,
headerBarButton: {
icon: LogIcon,
render: OpenSBLogsButton
},
start() {
enableStyle(styles);
FluxDispatcher.subscribe("VOICE_CHANNEL_EFFECT_SEND", async sound => {

View file

@ -5,7 +5,7 @@
*/
import { definePluginSettings } from "@api/Settings";
import { BackupRestoreIcon, CloudIcon, MainSettingsIcon, PaintbrushIcon, PatchHelperIcon, PlaceholderIcon, PluginsIcon, UpdaterIcon, VesktopSettingsIcon } from "@components/Icons";
import { BackupRestoreIcon, CloudIcon, LogIcon, MainSettingsIcon, PaintbrushIcon, PatchHelperIcon, PlaceholderIcon, PluginsIcon, UpdaterIcon, VesktopSettingsIcon } from "@components/Icons";
import {
BackupAndRestoreTab,
ChangelogTab,
@ -226,6 +226,12 @@ export default definePlugin({
Component: UpdaterTab,
Icon: UpdaterIcon
}),
buildEntry({
key: "equicord_changelog",
title: "Changelog",
Component: ChangelogTab,
Icon: LogIcon,
}),
buildEntry({
key: "equicord_cloud",
title: "Cloud",

View file

@ -20,7 +20,7 @@ import { AudioProcessor } from "@api/AudioPlayer";
import type { ProfileBadge } from "@api/Badges";
import type { ChatBarButtonData, ChatBarButtonFactory } from "@api/ChatButtons";
import type { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { HeaderBarButtonFactory } from "@api/HeaderBar";
import { HeaderBarButtonData } from "@api/HeaderBar";
import type { MemberListDecoratorFactory } from "@api/MemberListDecorators";
import type { MessageAccessoryFactory } from "@api/MessageAccessories";
import type { MessageDecorationFactory } from "@api/MessageDecorations";
@ -200,7 +200,7 @@ export interface PluginDef {
// Custom
renderNicknameIcon?: NicknameIconFactory;
renderHeaderBarButton?: HeaderBarButtonFactory;
headerBarButton?: HeaderBarButtonData;
audioProcessor?: AudioProcessor;
// TODO: Remove eventually