Merge remote-tracking branch 'upstream/dev' into dev
This commit is contained in:
commit
8f6371a9e0
38 changed files with 1135 additions and 424 deletions
4
packages/discord-types/src/menu.d.ts
vendored
4
packages/discord-types/src/menu.d.ts
vendored
|
|
@ -1,4 +1,4 @@
|
|||
import type { ComponentType, CSSProperties, MouseEvent, PropsWithChildren, ReactNode, UIEvent } from "react";
|
||||
import type { ComponentType, CSSProperties, ForwardRefRenderFunction, MouseEvent, PropsWithChildren, ReactNode, UIEvent } from "react";
|
||||
|
||||
type RC<C> = ComponentType<PropsWithChildren<C & Record<string, any>>>;
|
||||
|
||||
|
|
@ -46,6 +46,8 @@ export interface Menu {
|
|||
MenuControlItem: RC<{
|
||||
id: string;
|
||||
interactive?: boolean;
|
||||
label?: string;
|
||||
control: ForwardRefRenderFunction<any, any>;
|
||||
}>;
|
||||
MenuSliderControl: RC<{
|
||||
minValue?: number,
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
import { createPackage } from "@electron/asar";
|
||||
import { readdir, writeFile } from "fs/promises";
|
||||
import { dirname, join } from "path";
|
||||
import { dirname, join, resolve } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
import { BUILD_TIMESTAMP, commonOpts, exists, globPlugins, IS_DEV, IS_REPORTER, IS_COMPANION_TEST, IS_STANDALONE, IS_UPDATER_DISABLED, resolvePluginName, VERSION, commonRendererPlugins, watch, buildOrWatchAll, stringifyValues, IS_ANTI_CRASH_TEST } from "./common.mjs";
|
||||
|
|
@ -81,6 +81,10 @@ const globNativesPlugin = {
|
|||
let code = "";
|
||||
let natives = "\n";
|
||||
let i = 0;
|
||||
/**
|
||||
* @type {string[]}
|
||||
*/
|
||||
const watchFiles = [];
|
||||
for (const dir of pluginDirs) {
|
||||
const dirPath = join("src", dir);
|
||||
if (!await exists(dirPath)) continue;
|
||||
|
|
@ -90,6 +94,8 @@ const globNativesPlugin = {
|
|||
const nativePath = join(dirPath, fileName, "native.ts");
|
||||
const indexNativePath = join(dirPath, fileName, "native/index.ts");
|
||||
|
||||
watchFiles.push(resolve(nativePath), resolve(indexNativePath));
|
||||
|
||||
if (!(await exists(nativePath)) && !(await exists(indexNativePath)))
|
||||
continue;
|
||||
|
||||
|
|
@ -104,7 +110,9 @@ const globNativesPlugin = {
|
|||
code += `export default {${natives}};`;
|
||||
return {
|
||||
contents: code,
|
||||
resolveDir: "./src"
|
||||
resolveDir: "./src",
|
||||
watchDirs: pluginDirs.map(d => resolve("src", d)),
|
||||
watchFiles,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import esbuild, { build, context } from "esbuild";
|
|||
import { constants as FsConstants, readFileSync } from "fs";
|
||||
import { access, readdir, readFile } from "fs/promises";
|
||||
import { minify as minifyHtml } from "html-minifier-terser";
|
||||
import { dirname, join, relative } from "path";
|
||||
import { dirname, join, relative, resolve } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { promisify } from "util";
|
||||
|
||||
|
|
@ -195,7 +195,8 @@ export const globPlugins = kind => ({
|
|||
code += `export default {${pluginsCode}};export const PluginMeta={${metaCode}};export const ExcludedPlugins={${excludedCode}};`;
|
||||
return {
|
||||
contents: code,
|
||||
resolveDir: "./src"
|
||||
resolveDir: "./src",
|
||||
watchDirs: pluginDirs.map(d => resolve("src", d)),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export * as WebpackPatcher from "./webpack/patchWebpack";
|
|||
export { PlainSettings, Settings };
|
||||
|
||||
import { addVencordUiStyles } from "@components/css";
|
||||
import { openUpdaterModal } from "@components/settings/tabs/updater";
|
||||
import { openSettingsTabModal, UpdaterTab } from "@components/settings";
|
||||
import { debounce } from "@shared/debounce";
|
||||
import { IS_WINDOWS } from "@utils/constants";
|
||||
import { createAndAppendStyle } from "@utils/css";
|
||||
|
|
@ -123,6 +123,8 @@ async function syncSettings() {
|
|||
let notifiedForUpdatesThisSession = false;
|
||||
|
||||
async function runUpdateCheck() {
|
||||
if (IS_UPDATER_DISABLED) return;
|
||||
|
||||
const notify = (data: NotificationData) => {
|
||||
if (notifiedForUpdatesThisSession) return;
|
||||
notifiedForUpdatesThisSession = true;
|
||||
|
|
@ -153,7 +155,7 @@ async function runUpdateCheck() {
|
|||
notify({
|
||||
title: "A Equicord update is available!",
|
||||
body: "Click here to view the update",
|
||||
onClick: openUpdaterModal!
|
||||
onClick: () => openSettingsTabModal(UpdaterTab!)
|
||||
});
|
||||
} catch (err) {
|
||||
UpdateLogger.error("Failed to check for updates", err);
|
||||
|
|
|
|||
|
|
@ -9,11 +9,15 @@ import "./ChatButton.css";
|
|||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { classes } from "@utils/misc";
|
||||
import { IconComponent } from "@utils/types";
|
||||
import { Channel } from "@vencord/discord-types";
|
||||
import { waitFor } from "@webpack";
|
||||
import { ButtonWrapperClasses, Clickable, Tooltip } from "@webpack/common";
|
||||
import { ButtonWrapperClasses, Clickable, Menu, Tooltip } from "@webpack/common";
|
||||
import { HTMLProps, JSX, MouseEventHandler, ReactNode } from "react";
|
||||
|
||||
import { addContextMenuPatch, findGroupChildrenByChildId } from "./ContextMenu";
|
||||
import { useSettings } from "./Settings";
|
||||
|
||||
let ChannelTextAreaClasses: Record<"button" | "buttonContainer", string>;
|
||||
waitFor(["buttonContainer", "channelTextArea"], m => ChannelTextAreaClasses = m);
|
||||
|
||||
|
|
@ -76,26 +80,51 @@ export interface ChatBarProps {
|
|||
}
|
||||
|
||||
export type ChatBarButtonFactory = (props: ChatBarProps & { isMainChat: boolean; }) => JSX.Element | null;
|
||||
export type ChatBarButtonData = {
|
||||
render: ChatBarButtonFactory;
|
||||
/**
|
||||
* This icon is used only for Settings UI. Your render function must still render an icon,
|
||||
* and it can be different from this one.
|
||||
*/
|
||||
icon: IconComponent;
|
||||
};
|
||||
|
||||
const buttonFactories = new Map<string, ChatBarButtonFactory>();
|
||||
/**
|
||||
* Don't use this directly, use {@link addChatBarButton} and {@link removeChatBarButton} instead.
|
||||
*/
|
||||
export const ChatBarButtonMap = new Map<string, ChatBarButtonData>();
|
||||
const logger = new Logger("ChatButtons");
|
||||
|
||||
function VencordChatBarButtons(props: ChatBarProps) {
|
||||
const { chatBarButtons } = useSettings(["uiElements.chatBarButtons.*"]).uiElements;
|
||||
|
||||
return (
|
||||
<>
|
||||
{Array.from(ChatBarButtonMap)
|
||||
.filter(([key]) => chatBarButtons[key]?.enabled !== false)
|
||||
.map(([key, { render: Button }]) => (
|
||||
<ErrorBoundary noop key={key} onError={e => logger.error(`Failed to render ${key}`, e.error)}>
|
||||
<Button {...props} isMainChat={props.type.analyticsName === "normal"} />
|
||||
</ErrorBoundary>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function _injectButtons(buttons: ReactNode[], props: ChatBarProps) {
|
||||
if (props.disabled) return;
|
||||
|
||||
for (const [key, Button] of buttonFactories) {
|
||||
buttons.unshift(
|
||||
<ErrorBoundary noop key={key} onError={e => logger.error(`Failed to render ${key}`, e.error)}>
|
||||
<Button {...props} isMainChat={props.type.analyticsName === "normal"} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
buttons.unshift(<VencordChatBarButtons key="vencord-chat-buttons" {...props} />);
|
||||
|
||||
return buttons;
|
||||
}
|
||||
|
||||
export const addChatBarButton = (id: string, button: ChatBarButtonFactory) => buttonFactories.set(id, button);
|
||||
export const removeChatBarButton = (id: string) => buttonFactories.delete(id);
|
||||
/**
|
||||
* The icon argument is used only for Settings UI. Your render function must still render an icon,
|
||||
* and it can be different from this one.
|
||||
*/
|
||||
export const addChatBarButton = (id: string, render: ChatBarButtonFactory, icon: IconComponent) => ChatBarButtonMap.set(id, { render, icon });
|
||||
export const removeChatBarButton = (id: string) => ChatBarButtonMap.delete(id);
|
||||
|
||||
export interface ChatBarButtonProps {
|
||||
children: ReactNode;
|
||||
|
|
@ -105,6 +134,7 @@ export interface ChatBarButtonProps {
|
|||
onAuxClick?: MouseEventHandler;
|
||||
buttonProps?: Omit<HTMLProps<HTMLDivElement>, "size" | "onClick" | "onContextMenu" | "onAuxClick">;
|
||||
}
|
||||
|
||||
export const ChatBarButton = ErrorBoundary.wrap((props: ChatBarButtonProps) => {
|
||||
return (
|
||||
<Tooltip text={props.tooltip}>
|
||||
|
|
@ -129,3 +159,35 @@ export const ChatBarButton = ErrorBoundary.wrap((props: ChatBarButtonProps) => {
|
|||
</Tooltip>
|
||||
);
|
||||
}, { noop: true });
|
||||
|
||||
addContextMenuPatch("textarea-context", (children, args) => {
|
||||
const { chatBarButtons } = useSettings(["uiElements.chatBarButtons.*"]).uiElements;
|
||||
|
||||
const buttons = Array.from(ChatBarButtonMap.entries());
|
||||
if (!buttons.length) return;
|
||||
|
||||
const group = findGroupChildrenByChildId("submit-button", children);
|
||||
if (!group) return;
|
||||
|
||||
const idx = group.findIndex(c => c?.props?.id === "submit-button");
|
||||
if (idx === -1) return;
|
||||
|
||||
group.splice(idx, 0,
|
||||
<Menu.MenuItem id="vc-chat-buttons" key="vencord-chat-buttons" label="Vencord Buttons">
|
||||
{buttons.map(([id]) => (
|
||||
<Menu.MenuCheckboxItem
|
||||
label={id}
|
||||
key={id}
|
||||
id={`vc-chat-button-${id}`}
|
||||
checked={chatBarButtons[id]?.enabled !== false}
|
||||
action={() => {
|
||||
const wasEnabled = chatBarButtons[id]?.enabled !== false;
|
||||
|
||||
chatBarButtons[id] ??= {} as any;
|
||||
chatBarButtons[id].enabled = !wasEnabled;
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Menu.MenuItem>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -18,9 +18,12 @@
|
|||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { IconComponent } from "@utils/types";
|
||||
import { Channel, Message } from "@vencord/discord-types";
|
||||
import type { ComponentType, MouseEventHandler } from "react";
|
||||
|
||||
import { useSettings } from "./Settings";
|
||||
|
||||
const logger = new Logger("MessagePopover");
|
||||
|
||||
export interface MessagePopoverButtonItem {
|
||||
|
|
@ -34,41 +37,63 @@ export interface MessagePopoverButtonItem {
|
|||
}
|
||||
|
||||
export type MessagePopoverButtonFactory = (message: Message) => MessagePopoverButtonItem | null;
|
||||
export type MessagePopoverButtonData = {
|
||||
render: MessagePopoverButtonFactory;
|
||||
/**
|
||||
* This icon is used only for Settings UI. Your render function must still return an icon,
|
||||
* and it can be different from this one.
|
||||
*/
|
||||
icon: IconComponent;
|
||||
};
|
||||
|
||||
export const buttons = new Map<string, MessagePopoverButtonFactory>();
|
||||
export const MessagePopoverButtonMap = new Map<string, MessagePopoverButtonData>();
|
||||
|
||||
/**
|
||||
* The icon argument is used only for Settings UI. Your render function must still return an icon,
|
||||
* and it can be different from this one.
|
||||
*/
|
||||
export function addMessagePopoverButton(
|
||||
identifier: string,
|
||||
item: MessagePopoverButtonFactory,
|
||||
render: MessagePopoverButtonFactory,
|
||||
icon: IconComponent
|
||||
) {
|
||||
buttons.set(identifier, item);
|
||||
MessagePopoverButtonMap.set(identifier, { render, icon });
|
||||
}
|
||||
|
||||
export function removeMessagePopoverButton(identifier: string) {
|
||||
buttons.delete(identifier);
|
||||
MessagePopoverButtonMap.delete(identifier);
|
||||
}
|
||||
|
||||
function VencordPopoverButtons(props: { Component: React.ComponentType<MessagePopoverButtonItem>, message: Message; }) {
|
||||
const { Component, message } = props;
|
||||
|
||||
const { messagePopoverButtons } = useSettings(["uiElements.messagePopoverButtons.*"]).uiElements;
|
||||
|
||||
const elements = Array.from(MessagePopoverButtonMap.entries())
|
||||
.filter(([key]) => messagePopoverButtons[key]?.enabled !== false)
|
||||
.map(([key, { render }]) => {
|
||||
try {
|
||||
// FIXME: this should use proper React to ensure hooks work
|
||||
const item = render(message);
|
||||
if (!item) return null;
|
||||
|
||||
return (
|
||||
<ErrorBoundary noop>
|
||||
<Component key={key} {...item} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error(`[${key}]`, err);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
return <>{elements}</>;
|
||||
}
|
||||
|
||||
export function _buildPopoverElements(
|
||||
Component: React.ComponentType<MessagePopoverButtonItem>,
|
||||
message: Message
|
||||
) {
|
||||
const items: React.ReactNode[] = [];
|
||||
|
||||
for (const [identifier, getItem] of buttons.entries()) {
|
||||
try {
|
||||
const item = getItem(message);
|
||||
if (item) {
|
||||
item.key ??= identifier;
|
||||
items.push(
|
||||
<ErrorBoundary noop>
|
||||
<Component {...item} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`[${identifier}]`, err);
|
||||
}
|
||||
}
|
||||
|
||||
return <>{items}</>;
|
||||
return <VencordPopoverButtons Component={Component} message={message} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p:
|
|||
const {
|
||||
name, commands, contextMenus, managedStyle, userProfileBadges,
|
||||
onBeforeMessageEdit, onBeforeMessageSend, onMessageClick,
|
||||
renderChatBarButton, renderMemberListDecorator, renderNicknameIcon, renderMessageAccessory, renderMessageDecoration, renderMessagePopoverButton
|
||||
renderChatBarButton, chatBarButton, renderMemberListDecorator, renderMessageAccessory, renderMessageDecoration, renderMessagePopoverButton, messagePopoverButton, renderNicknameIcon
|
||||
} = p;
|
||||
|
||||
if (p.start) {
|
||||
|
|
@ -237,12 +237,16 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p:
|
|||
if (onBeforeMessageSend) addMessagePreSendListener(onBeforeMessageSend);
|
||||
if (onMessageClick) addMessageClickListener(onMessageClick);
|
||||
|
||||
if (renderChatBarButton) addChatBarButton(name, renderChatBarButton);
|
||||
if (chatBarButton) addChatBarButton(name, chatBarButton.render, chatBarButton.icon);
|
||||
// @ts-expect-error: legacy code doesn't have icon
|
||||
else if (renderChatBarButton) addChatBarButton(name, renderChatBarButton);
|
||||
if (renderMemberListDecorator) addMemberListDecorator(name, renderMemberListDecorator);
|
||||
if (renderNicknameIcon) addNicknameIcon(name, renderNicknameIcon);
|
||||
if (renderMessageDecoration) addMessageDecoration(name, renderMessageDecoration);
|
||||
if (renderMessageAccessory) addMessageAccessory(name, renderMessageAccessory);
|
||||
if (renderMessagePopoverButton) addMessagePopoverButton(name, renderMessagePopoverButton);
|
||||
if (messagePopoverButton) addMessagePopoverButton(name, messagePopoverButton.render, messagePopoverButton.icon);
|
||||
// @ts-expect-error: legacy code doesn't have icon
|
||||
else if (renderMessagePopoverButton) addMessagePopoverButton(name, renderMessagePopoverButton);
|
||||
|
||||
return true;
|
||||
}, p => `startPlugin ${p.name}`);
|
||||
|
|
@ -251,7 +255,7 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu
|
|||
const {
|
||||
name, commands, contextMenus, managedStyle, userProfileBadges,
|
||||
onBeforeMessageEdit, onBeforeMessageSend, onMessageClick,
|
||||
renderChatBarButton, renderMemberListDecorator, renderNicknameIcon, renderMessageAccessory, renderMessageDecoration, renderMessagePopoverButton
|
||||
renderChatBarButton, chatBarButton, renderMemberListDecorator, renderMessageAccessory, renderMessageDecoration, renderMessagePopoverButton, messagePopoverButton, renderNicknameIcon
|
||||
} = p;
|
||||
|
||||
if (p.stop) {
|
||||
|
|
@ -299,12 +303,12 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu
|
|||
if (onBeforeMessageSend) removeMessagePreSendListener(onBeforeMessageSend);
|
||||
if (onMessageClick) removeMessageClickListener(onMessageClick);
|
||||
|
||||
if (renderChatBarButton) removeChatBarButton(name);
|
||||
if (chatBarButton || renderChatBarButton) removeChatBarButton(name);
|
||||
if (renderMemberListDecorator) removeMemberListDecorator(name);
|
||||
if (renderNicknameIcon) removeNicknameIcon(name);
|
||||
if (renderMessageDecoration) removeMessageDecoration(name);
|
||||
if (renderMessageAccessory) removeMessageAccessory(name);
|
||||
if (renderMessagePopoverButton) removeMessagePopoverButton(name);
|
||||
if (messagePopoverButton || renderMessagePopoverButton) removeMessagePopoverButton(name);
|
||||
|
||||
return true;
|
||||
}, p => `stopPlugin ${p.name}`);
|
||||
|
|
@ -345,11 +349,11 @@ export const initPluginManager = onlyOnce(function init() {
|
|||
|
||||
if (p.commands?.length) neededApiPlugins.add("CommandsAPI");
|
||||
if (p.onBeforeMessageEdit || p.onBeforeMessageSend || p.onMessageClick) neededApiPlugins.add("MessageEventsAPI");
|
||||
if (p.renderChatBarButton) neededApiPlugins.add("ChatInputButtonAPI");
|
||||
if (p.chatBarButton || p.renderChatBarButton) neededApiPlugins.add("ChatInputButtonAPI");
|
||||
if (p.renderMemberListDecorator) neededApiPlugins.add("MemberListDecoratorsAPI");
|
||||
if (p.renderMessageAccessory) neededApiPlugins.add("MessageAccessoriesAPI");
|
||||
if (p.renderMessageDecoration) neededApiPlugins.add("MessageDecorationsAPI");
|
||||
if (p.renderMessagePopoverButton) neededApiPlugins.add("MessagePopoverAPI");
|
||||
if (p.messagePopoverButton || p.renderMessagePopoverButton) neededApiPlugins.add("MessagePopoverAPI");
|
||||
if (p.renderNicknameIcon) neededApiPlugins.add("NicknameIconsAPI");
|
||||
if (p.userProfileBadge) neededApiPlugins.add("BadgeAPI");
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,18 @@ import { React, useEffect } from "@webpack/common";
|
|||
import plugins from "~plugins";
|
||||
|
||||
const logger = new Logger("Settings");
|
||||
|
||||
export interface SettingsPluginUiElement {
|
||||
enabled: boolean;
|
||||
// TODO
|
||||
/** not implemented for now */
|
||||
order?: number;
|
||||
}
|
||||
export type SettingsPluginUiElements = {
|
||||
/** id will be whatever id the element was registered with. Usually, but not always, the plugin name */
|
||||
[id: string]: SettingsPluginUiElement;
|
||||
};
|
||||
|
||||
export interface Settings {
|
||||
autoUpdate: boolean;
|
||||
autoUpdateNotification: boolean;
|
||||
|
|
@ -61,6 +73,11 @@ export interface Settings {
|
|||
};
|
||||
};
|
||||
|
||||
uiElements: {
|
||||
messagePopoverButtons: SettingsPluginUiElements;
|
||||
chatBarButtons: SettingsPluginUiElements;
|
||||
},
|
||||
|
||||
notifications: {
|
||||
timeout: number;
|
||||
position: "top-right" | "bottom-right";
|
||||
|
|
@ -103,6 +120,11 @@ const DefaultSettings: Settings = {
|
|||
winNativeTitleBar: false,
|
||||
plugins: {},
|
||||
|
||||
uiElements: {
|
||||
chatBarButtons: {},
|
||||
messagePopoverButtons: {}
|
||||
},
|
||||
|
||||
notifications: {
|
||||
timeout: 5000,
|
||||
position: "bottom-right",
|
||||
|
|
@ -201,8 +223,21 @@ export function useSettings(paths?: UseSettings<Settings>[]) {
|
|||
|
||||
useEffect(() => {
|
||||
if (paths) {
|
||||
paths.forEach(p => SettingsStore.addChangeListener(p, forceUpdate));
|
||||
return () => paths.forEach(p => SettingsStore.removeChangeListener(p, forceUpdate));
|
||||
paths.forEach(p => {
|
||||
if (p.endsWith(".*")) {
|
||||
SettingsStore.addPrefixChangeListener(p.slice(0, -2), forceUpdate);
|
||||
} else {
|
||||
SettingsStore.addChangeListener(p, forceUpdate);
|
||||
}
|
||||
});
|
||||
|
||||
return () => paths.forEach(p => {
|
||||
if (p.endsWith(".*")) {
|
||||
SettingsStore.removePrefixChangeListener(p.slice(0, -2), forceUpdate);
|
||||
} else {
|
||||
SettingsStore.removeChangeListener(p, forceUpdate);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
SettingsStore.addGlobalChangeListener(forceUpdate);
|
||||
return () => SettingsStore.removeGlobalChangeListener(forceUpdate);
|
||||
|
|
@ -280,9 +315,11 @@ export function definePluginSettings<
|
|||
if (!definedSettings.pluginName) throw new Error("Cannot access settings before plugin is initialized");
|
||||
return PlainSettings.plugins[definedSettings.pluginName] as any;
|
||||
},
|
||||
use: settings => useSettings(
|
||||
settings?.map(name => `plugins.${definedSettings.pluginName}.${name}`) as UseSettings<Settings>[]
|
||||
).plugins[definedSettings.pluginName] as any,
|
||||
use: settings => useSettings((
|
||||
settings
|
||||
? settings.map(name => `plugins.${definedSettings.pluginName}.${name}`)
|
||||
: [`plugins.${definedSettings.pluginName}.*`]
|
||||
) as UseSettings<Settings>[]).plugins[definedSettings.pluginName] as any,
|
||||
def,
|
||||
checks: checks ?? {} as any,
|
||||
pluginName: "",
|
||||
|
|
@ -302,7 +339,7 @@ type ResolveUseSettings<T extends object> = {
|
|||
Key extends string
|
||||
? T[Key] extends Record<string, unknown>
|
||||
// @ts-expect-error "Type instantiation is excessively deep and possibly infinite"
|
||||
? UseSettings<T[Key]> extends string ? `${Key}.${UseSettings<T[Key]>}` : never
|
||||
? `${Key}.*` | (ResolveUseSettings<T[Key]> extends Record<string, string> ? `${Key}.${ResolveUseSettings<T[Key]>[keyof T[Key]]}` : never)
|
||||
: Key
|
||||
: never;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { LazyComponent, LazyComponentWrapper } from "@utils/lazyReact";
|
|||
import { Logger } from "@utils/Logger";
|
||||
import { Margins } from "@utils/margins";
|
||||
import type { React } from "@webpack/common";
|
||||
import { ComponentType } from "react";
|
||||
|
||||
import { ErrorCard } from "./ErrorCard";
|
||||
|
||||
|
|
@ -115,13 +116,21 @@ const ErrorBoundary = LazyComponent(() => {
|
|||
};
|
||||
}) as
|
||||
LazyComponentWrapper<React.ComponentType<React.PropsWithChildren<Props>> & {
|
||||
wrap<T extends object = any>(Component: React.ComponentType<T>, errorBoundaryProps?: Omit<Props<T>, "wrappedProps">): React.FunctionComponent<T>;
|
||||
wrap<T extends object = any>(Component: React.ComponentType<T>, errorBoundaryProps?: Omit<Props<T>, "wrappedProps"> & { displayName?: string; }): React.FunctionComponent<T>;
|
||||
}>;
|
||||
|
||||
ErrorBoundary.wrap = (Component, errorBoundaryProps) => props => (
|
||||
<ErrorBoundary {...errorBoundaryProps} wrappedProps={props}>
|
||||
<Component {...props} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
ErrorBoundary.wrap = (Component, errorBoundaryProps) => {
|
||||
const wrapper: ComponentType<any> = props => (
|
||||
<ErrorBoundary {...errorBoundaryProps} wrappedProps={props}>
|
||||
<Component {...props} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
if (errorBoundaryProps?.displayName) {
|
||||
wrapper.displayName = errorBoundaryProps.displayName;
|
||||
}
|
||||
|
||||
return wrapper;
|
||||
};
|
||||
|
||||
export default ErrorBoundary;
|
||||
|
|
|
|||
|
|
@ -524,3 +524,17 @@ export function WebsiteIcon(props: IconProps) {
|
|||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A question mark inside a square, used as a placeholder icon when no other icon is available
|
||||
*/
|
||||
export function PlaceholderIcon(props: IconProps) {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path fill={props.fill || "currentColor"} fillRule="evenodd" d="M5 2a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3V5a3 3 0 0 0-3-3H5Zm6.81 7c-.54 0-1 .26-1.23.61A1 1 0 0 1 8.92 8.5 3.49 3.49 0 0 1 11.82 7c1.81 0 3.43 1.38 3.43 3.25 0 1.45-.98 2.61-2.27 3.06a1 1 0 0 1-1.96.37l-.19-1a1 1 0 0 1 .98-1.18c.87 0 1.44-.63 1.44-1.25S12.68 9 11.81 9ZM13 16a1 1 0 1 1-2 0 1 1 0 0 1 2 0Zm7-10.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0ZM18.5 20a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM7 18.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0ZM5.5 7a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" clipRule="evenodd" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { BaseText } from "@components/BaseText";
|
|||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { handleComponentFailed } from "@components/handleComponentFailed";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { ModalCloseButton, ModalContent, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { onlyOnce } from "@utils/onlyOnce";
|
||||
import type { ComponentType, PropsWithChildren } from "react";
|
||||
|
||||
|
|
@ -35,8 +36,26 @@ export function SettingsTab({ title, children }: PropsWithChildren<{ title: stri
|
|||
export const handleSettingsTabError = onlyOnce(handleComponentFailed);
|
||||
|
||||
export function wrapTab(component: ComponentType<any>, tab: string) {
|
||||
return ErrorBoundary.wrap(component, {
|
||||
const wrapped = ErrorBoundary.wrap(component, {
|
||||
displayName: `${tab}SettingsTab`,
|
||||
message: `Failed to render the ${tab} tab. If this issue persists, try using the installer to reinstall!`,
|
||||
onError: handleSettingsTabError,
|
||||
});
|
||||
|
||||
return wrapped;
|
||||
}
|
||||
|
||||
export function openSettingsTabModal(Tab: ComponentType<any>) {
|
||||
try {
|
||||
openModal(wrapTab((modalProps: ModalProps) => (
|
||||
<ModalRoot {...modalProps} size={ModalSize.MEDIUM}>
|
||||
<ModalContent className="vc-settings-modal">
|
||||
<ModalCloseButton onClick={modalProps.onClose} className="vc-settings-modal-close" />
|
||||
<Tab />
|
||||
</ModalContent>
|
||||
</ModalRoot>
|
||||
), Tab.displayName || "Settings Tab"));
|
||||
} catch {
|
||||
handleSettingsTabError();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,5 +15,5 @@ export { openContributorModal } from "./plugins/ContributorModal";
|
|||
export { openPluginModal } from "./plugins/PluginModal";
|
||||
export { default as BackupAndRestoreTab } from "./sync/BackupAndRestoreTab";
|
||||
export { default as CloudTab } from "./sync/CloudTab";
|
||||
export { openUpdaterModal, default as UpdaterTab } from "./updater";
|
||||
export { default as UpdaterTab } from "./updater";
|
||||
export { default as VencordTab } from "./vencord";
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ export function makeDummyUser(user: { username: string; id?: string; avatar?: st
|
|||
}
|
||||
|
||||
export default function PluginModal({ plugin, onRestartNeeded, onClose, transitionState }: PluginModalProps) {
|
||||
const pluginSettings = useSettings().plugins[plugin.name];
|
||||
const pluginSettings = useSettings([`plugins.${plugin.name}.*`]).plugins[plugin.name];
|
||||
const hasSettings = Boolean(pluginSettings && plugin.options && !isObjectEmpty(plugin.options));
|
||||
|
||||
const [authors, setAuthors] = useState<Partial<User>[]>([]);
|
||||
|
|
|
|||
42
src/components/settings/tabs/plugins/UIElements.css
Normal file
42
src/components/settings/tabs/plugins/UIElements.css
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
.vc-plugin-ui-elements-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--interactive-normal);
|
||||
margin-top: 0.5em;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
color: var(--interactive-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.vc-plugin-ui-elements-button-arrow {
|
||||
margin-left: auto;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.vc-plugin-ui-elements-modal-content {
|
||||
padding: 1em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5em;
|
||||
}
|
||||
|
||||
.vc-plugin-ui-elements-switches {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
.vc-plugin-ui-elements-switches-row {
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
|
||||
:last-child {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
107
src/components/settings/tabs/plugins/UIElements.tsx
Normal file
107
src/components/settings/tabs/plugins/UIElements.tsx
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./UIElements.css";
|
||||
|
||||
import { ChatBarButtonMap } from "@api/ChatButtons";
|
||||
import { MessagePopoverButtonMap } from "@api/MessagePopover";
|
||||
import { SettingsPluginUiElements, useSettings } from "@api/Settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { BaseText } from "@components/BaseText";
|
||||
import { Card } from "@components/Card";
|
||||
import { PlaceholderIcon } from "@components/Icons";
|
||||
import { Paragraph } from "@components/Paragraph";
|
||||
import { Switch } from "@components/Switch";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes } from "@utils/misc";
|
||||
import { ModalContent, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { IconComponent } from "@utils/types";
|
||||
import { Clickable } from "@webpack/common";
|
||||
|
||||
|
||||
const cl = classNameFactory("vc-plugin-ui-elements-");
|
||||
|
||||
export function UIElementsButton() {
|
||||
return (
|
||||
<Clickable onClick={() => openModal(modalProps => <UIElementsModal {...modalProps} />)}>
|
||||
<Card className={cl("button")} defaultPadding>
|
||||
<div className={cl("button-description")}>
|
||||
<Paragraph size="md" weight="semibold">
|
||||
Manage plugin UI elements
|
||||
</Paragraph>
|
||||
<Paragraph size="xs">
|
||||
Allows you to hide buttons you don't like
|
||||
</Paragraph>
|
||||
</div>
|
||||
<svg
|
||||
className={cl("button-arrow")}
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path fill="currentColor" d="M9.3 5.3a1 1 0 0 0 0 1.4l5.29 5.3-5.3 5.3a1 1 0 1 0 1.42 1.4l6-6a1 1 0 0 0 0-1.4l-6-6a1 1 0 0 0-1.42 0Z" />
|
||||
</svg>
|
||||
</Card>
|
||||
</Clickable >
|
||||
);
|
||||
}
|
||||
|
||||
function Section(props: {
|
||||
title: string;
|
||||
description: string;
|
||||
settings: SettingsPluginUiElements;
|
||||
buttonMap: Map<string, { icon: IconComponent; }>;
|
||||
}) {
|
||||
const { buttonMap, description, title, settings } = props;
|
||||
|
||||
return (
|
||||
<section>
|
||||
<BaseText tag="h3" size="xl" weight="bold">{title}</BaseText>
|
||||
<Paragraph size="sm" className={classes(Margins.top8, Margins.bottom20)}>{description}</Paragraph>
|
||||
|
||||
<div className={cl("switches")}>
|
||||
{Array.from(buttonMap, ([name, { icon }]) => {
|
||||
const Icon = icon ?? PlaceholderIcon;
|
||||
return (
|
||||
<Paragraph size="md" weight="semibold" key={name} className={cl("switches-row")}>
|
||||
<Icon height={20} width={20} />
|
||||
{name}
|
||||
<Switch
|
||||
checked={settings[name]?.enabled ?? true}
|
||||
onChange={v => {
|
||||
settings[name] ??= {} as any;
|
||||
settings[name].enabled = v;
|
||||
}}
|
||||
/>
|
||||
</Paragraph>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function UIElementsModal(props: ModalProps) {
|
||||
const { uiElements } = useSettings(["uiElements.*"]);
|
||||
|
||||
return (
|
||||
<ModalRoot {...props} size={ModalSize.MEDIUM}>
|
||||
<ModalContent className={cl("modal-content")}>
|
||||
<Section
|
||||
title="Chatbar Buttons"
|
||||
description="These are the buttons on the right side of the chat input bar"
|
||||
buttonMap={ChatBarButtonMap}
|
||||
settings={uiElements.chatBarButtons}
|
||||
/>
|
||||
<Section
|
||||
title="Message Popover Buttons"
|
||||
description="These are the floating buttons on the right when you hover over a message"
|
||||
buttonMap={MessagePopoverButtonMap}
|
||||
settings={uiElements.messagePopoverButtons}
|
||||
/>
|
||||
</ModalContent>
|
||||
</ModalRoot>
|
||||
);
|
||||
}
|
||||
|
|
@ -43,6 +43,7 @@ import Plugins, { ExcludedPlugins, PluginMeta } from "~plugins";
|
|||
import { PluginCard } from "./PluginCard";
|
||||
import { openWarningModal } from "./PluginModal";
|
||||
import { StockPluginsCard, UserPluginsCard } from "./PluginStatCards";
|
||||
import { UIElementsButton } from "./UIElements";
|
||||
|
||||
export const cl = classNameFactory("vc-plugins-");
|
||||
export const logger = new Logger("PluginSettings", "#a6d189");
|
||||
|
|
@ -371,13 +372,7 @@ export default function PluginSettings() {
|
|||
|
||||
<ReloadRequiredCard required={changes.hasChanges} enabledPlugins={enabledPlugins} openWarningModal={openWarningModal} resetCheckAndDo={resetCheckAndDo} />
|
||||
|
||||
<div className={cl("stats-container")} style={{
|
||||
marginTop: "16px",
|
||||
gap: "16px",
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
width: "100%"
|
||||
}}>
|
||||
<div className={cl("stats-container")}>
|
||||
<StockPluginsCard
|
||||
totalStockPlugins={totalStockPlugins}
|
||||
enabledStockPlugins={enabledStockPlugins}
|
||||
|
|
@ -388,6 +383,10 @@ export default function PluginSettings() {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div className={cl("ui-elements")}>
|
||||
<UIElementsButton />
|
||||
</div>
|
||||
|
||||
<HeadingTertiary className={classes(Margins.top20, Margins.bottom8)}>
|
||||
Filters
|
||||
</HeadingTertiary>
|
||||
|
|
|
|||
|
|
@ -79,10 +79,10 @@
|
|||
background-color: var(--button-danger-background);
|
||||
}
|
||||
|
||||
.vc-updater-modal {
|
||||
.vc-settings-modal {
|
||||
padding: 1.5em !important;
|
||||
}
|
||||
|
||||
.vc-updater-modal-close-button {
|
||||
.vc-settings-modal-close {
|
||||
float: right;
|
||||
}
|
||||
|
|
@ -24,10 +24,9 @@ import { FormSwitch } from "@components/FormSwitch";
|
|||
import { Heading } from "@components/Heading";
|
||||
import { Link } from "@components/Link";
|
||||
import { Paragraph } from "@components/Paragraph";
|
||||
import { handleSettingsTabError, SettingsTab, wrapTab } from "@components/settings";
|
||||
import { SettingsTab, wrapTab } from "@components/settings";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes } from "@utils/misc";
|
||||
import { ModalCloseButton, ModalContent, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { relaunch } from "@utils/native";
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import { changes, checkForUpdates, getRepo, isNewer, update, updateError, UpdateLogger } from "@utils/updater";
|
||||
|
|
@ -224,21 +223,6 @@ function Updater() {
|
|||
);
|
||||
}
|
||||
|
||||
export default IS_UPDATER_DISABLED ? null : wrapTab(Updater, "Updater");
|
||||
|
||||
export const openUpdaterModal = IS_UPDATER_DISABLED ? null : function () {
|
||||
const UpdaterTab = wrapTab(Updater, "Updater");
|
||||
|
||||
try {
|
||||
openModal(wrapTab((modalProps: ModalProps) => (
|
||||
<ModalRoot {...modalProps} size={ModalSize.MEDIUM}>
|
||||
<ModalContent className="vc-updater-modal">
|
||||
<ModalCloseButton onClick={modalProps.onClose} className="vc-updater-modal-close-button" />
|
||||
<UpdaterTab />
|
||||
</ModalContent>
|
||||
</ModalRoot>
|
||||
), "UpdaterModal"));
|
||||
} catch {
|
||||
handleSettingsTabError();
|
||||
}
|
||||
};
|
||||
export default IS_UPDATER_DISABLED
|
||||
? null
|
||||
: wrapTab(Updater, "Updater");
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export function openNotificationSettingsModal() {
|
|||
}
|
||||
|
||||
function NotificationSettings() {
|
||||
const settings = useSettings().notifications;
|
||||
const settings = useSettings(["notifications.*"]).notifications;
|
||||
|
||||
return (
|
||||
<div style={{ padding: "1em 0" }}>
|
||||
|
|
|
|||
|
|
@ -18,76 +18,27 @@
|
|||
|
||||
import "./styles.css";
|
||||
|
||||
import { openNotificationLogModal } from "@api/Notifications/notificationLog";
|
||||
import { isPluginEnabled, plugins } from "@api/PluginManager";
|
||||
import { migratePluginSettings, Settings, useSettings } from "@api/Settings";
|
||||
import { definePluginSettings, migratePluginSettings } from "@api/Settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findComponentByCodeLazy } from "@webpack";
|
||||
import { Menu, Popout, useRef, useState } from "@webpack/common";
|
||||
import type { PropsWithChildren, ReactNode } from "react";
|
||||
import { Popout, useRef, useState } from "@webpack/common";
|
||||
import type { PropsWithChildren } from "react";
|
||||
|
||||
import { renderPopout } from "./menu";
|
||||
|
||||
const HeaderBarIcon = findComponentByCodeLazy(".HEADER_BAR_BADGE_TOP:", '.iconBadge,"top"');
|
||||
|
||||
function VencordPopout(onClose: () => void) {
|
||||
const { useQuickCss } = useSettings(["useQuickCss"]);
|
||||
|
||||
const pluginEntries = [] as ReactNode[];
|
||||
|
||||
for (const plugin of Object.values(plugins)) {
|
||||
if (plugin.toolboxActions && isPluginEnabled(plugin.name)) {
|
||||
pluginEntries.push(
|
||||
<Menu.MenuGroup
|
||||
label={plugin.name}
|
||||
key={`vc-toolbox-${plugin.name}`}
|
||||
>
|
||||
{Object.entries(plugin.toolboxActions).map(([text, action]) => {
|
||||
const key = `vc-toolbox-${plugin.name}-${text}`;
|
||||
|
||||
return (
|
||||
<Menu.MenuItem
|
||||
id={key}
|
||||
key={key}
|
||||
label={text}
|
||||
action={action}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Menu.MenuGroup>
|
||||
);
|
||||
}
|
||||
export const settings = definePluginSettings({
|
||||
showPluginMenu: {
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
description: "Show the plugins menu in the toolbox",
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Menu.Menu
|
||||
navId="vc-toolbox"
|
||||
onClose={onClose}
|
||||
>
|
||||
<Menu.MenuItem
|
||||
id="vc-toolbox-notifications"
|
||||
label="Open Notification Log"
|
||||
action={openNotificationLogModal}
|
||||
/>
|
||||
<Menu.MenuCheckboxItem
|
||||
id="vc-toolbox-quickcss-toggle"
|
||||
checked={useQuickCss}
|
||||
label={"Enable QuickCSS"}
|
||||
action={() => {
|
||||
Settings.useQuickCss = !useQuickCss;
|
||||
}}
|
||||
/>
|
||||
<Menu.MenuItem
|
||||
id="vc-toolbox-quickcss"
|
||||
label="Open QuickCSS"
|
||||
action={() => VencordNative.quickCss.openEditor()}
|
||||
/>
|
||||
{...pluginEntries}
|
||||
</Menu.Menu>
|
||||
);
|
||||
}
|
||||
|
||||
function VencordPopoutIcon() {
|
||||
function Icon() {
|
||||
return (
|
||||
<svg viewBox="0 0 443 443" width={20} height={20} className="vc-toolbox-icon">
|
||||
<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" />
|
||||
|
|
@ -110,15 +61,15 @@ function VencordPopoutButton({ buttonClass }: { buttonClass: string; }) {
|
|||
shouldShow={show}
|
||||
onRequestClose={() => setShow(false)}
|
||||
targetElementRef={buttonRef}
|
||||
renderPopout={() => VencordPopout(() => setShow(false))}
|
||||
renderPopout={() => renderPopout(() => setShow(false))}
|
||||
>
|
||||
{(_, { isShown }) => (
|
||||
<HeaderBarIcon
|
||||
ref={buttonRef}
|
||||
className={`vc-toolbox-btn ${buttonClass}`}
|
||||
onClick={() => setShow(v => !v)}
|
||||
tooltip={isShown ? null : "Equicord Toolbox"}
|
||||
icon={() => VencordPopoutIcon()}
|
||||
tooltip={isShown ? null : "Vencord Toolbox"}
|
||||
icon={() => <Icon />}
|
||||
selected={isShown}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -132,6 +83,8 @@ export default definePlugin({
|
|||
description: "Adds a button next to the inbox button in the channel header that houses Equicord quick actions",
|
||||
authors: [Devs.Ven, Devs.AutumnVN],
|
||||
|
||||
settings,
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: '?"BACK_FORWARD_NAVIGATION":',
|
||||
|
|
|
|||
317
src/equicordplugins/equicordToolbox/menu.tsx
Normal file
317
src/equicordplugins/equicordToolbox/menu.tsx
Normal file
|
|
@ -0,0 +1,317 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { openNotificationLogModal } from "@api/Notifications/notificationLog";
|
||||
import { isPluginEnabled, plugins } from "@api/PluginManager";
|
||||
import { Settings, useSettings } from "@api/Settings";
|
||||
import { openPluginModal, openSettingsTabModal, PluginsTab, ThemesTab } from "@components/settings";
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import { wordsFromCamel, wordsToTitle } from "@utils/text";
|
||||
import { OptionType, Plugin } from "@utils/types";
|
||||
import { Menu, showToast, useMemo, useState } from "@webpack/common";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { settings } from ".";
|
||||
|
||||
function buildPluginMenu() {
|
||||
const { showPluginMenu } = settings.use(["showPluginMenu"]);
|
||||
|
||||
// has to be here due to hooks
|
||||
const pluginEntries = buildPluginMenuEntries();
|
||||
|
||||
if (!showPluginMenu) return null;
|
||||
|
||||
return (
|
||||
<Menu.MenuItem
|
||||
id="plugins"
|
||||
label="Plugins"
|
||||
action={() => openSettingsTabModal(PluginsTab)}
|
||||
>
|
||||
{pluginEntries}
|
||||
</Menu.MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
export function buildPluginMenuEntries(includeEmpty = false) {
|
||||
const pluginSettings = useSettings().plugins;
|
||||
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const lowerSearch = search.toLowerCase();
|
||||
|
||||
const sortedPlugins = useMemo(() =>
|
||||
Object.values(plugins).sort((a, b) => a.name.localeCompare(b.name)),
|
||||
[]
|
||||
);
|
||||
|
||||
const candidates = useMemo(() =>
|
||||
sortedPlugins
|
||||
.filter(p => {
|
||||
if (!isPluginEnabled(p.name)) return false;
|
||||
if (p.name.endsWith("API")) return false;
|
||||
|
||||
const name = p.name.toLowerCase();
|
||||
return name.includes(lowerSearch);
|
||||
}),
|
||||
[lowerSearch]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu.MenuControlItem
|
||||
id="plugins-search"
|
||||
control={(props, ref) => (
|
||||
<Menu.MenuSearchControl
|
||||
{...props}
|
||||
query={search}
|
||||
onChange={setSearch}
|
||||
ref={ref}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Menu.MenuSeparator />
|
||||
|
||||
{candidates
|
||||
.map(p => {
|
||||
const options = [] as ReactNode[];
|
||||
|
||||
let hasAnyOption = false;
|
||||
|
||||
if (p.options) for (const [key, option] of Object.entries(p.options)) {
|
||||
if ("hidden" in option && option.hidden) continue;
|
||||
|
||||
hasAnyOption = true;
|
||||
|
||||
const s = pluginSettings[p.name];
|
||||
|
||||
const baseProps = {
|
||||
id: `${p.name}-${key}`,
|
||||
key: key,
|
||||
label: wordsToTitle(wordsFromCamel(key)),
|
||||
disabled: "disabled" in option ? option.disabled?.call(p.settings) : false,
|
||||
};
|
||||
|
||||
switch (option.type) {
|
||||
case OptionType.BOOLEAN:
|
||||
options.push(
|
||||
<Menu.MenuCheckboxItem
|
||||
{...baseProps}
|
||||
checked={s[key]}
|
||||
action={() => {
|
||||
s[key] = !s[key];
|
||||
if (option.restartNeeded) showToast("Restart to apply the change");
|
||||
}}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case OptionType.SELECT:
|
||||
options.push(
|
||||
<Menu.MenuItem {...baseProps}>
|
||||
{option.options.map(opt => (
|
||||
<Menu.MenuRadioItem
|
||||
group={`${p.name}-${key}`}
|
||||
id={`${p.name}-${key}-${opt.value}`}
|
||||
key={opt.label}
|
||||
label={opt.label}
|
||||
checked={s[key] === opt.value}
|
||||
action={() => {
|
||||
s[key] = opt.value;
|
||||
if (option.restartNeeded) showToast("Restart to apply the change");
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Menu.MenuItem>
|
||||
);
|
||||
break;
|
||||
case OptionType.SLIDER:
|
||||
// The menu slider doesn't support these options. Skip to avoid confusion
|
||||
if (option.stickToMarkers || option.componentProps) continue;
|
||||
|
||||
options.push(
|
||||
<Menu.MenuControlItem
|
||||
{...baseProps}
|
||||
control={(props, ref) => (
|
||||
<Menu.MenuSliderControl
|
||||
ref={ref}
|
||||
{...props}
|
||||
minValue={option.markers[0]}
|
||||
maxValue={option.markers.at(-1)!}
|
||||
value={s[key]}
|
||||
onChange={v => s[key] = v}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const hasVisibleOptions = options.length > 0;
|
||||
const shouldSkip = !hasVisibleOptions && !(includeEmpty && hasAnyOption);
|
||||
if (shouldSkip) return null;
|
||||
|
||||
return (
|
||||
<Menu.MenuItem
|
||||
id={`${p.name}-menu`}
|
||||
key={p.name}
|
||||
label={p.name}
|
||||
action={() => openPluginModal(p)}
|
||||
>
|
||||
{hasVisibleOptions && (
|
||||
<>
|
||||
<Menu.MenuGroup label={p.name}>
|
||||
{options}
|
||||
</Menu.MenuGroup>
|
||||
|
||||
<Menu.MenuSeparator />
|
||||
|
||||
<Menu.MenuItem
|
||||
id={`${p.name}-open`}
|
||||
label={"Open Settings"}
|
||||
action={() => openPluginModal(p)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Menu.MenuItem>
|
||||
);
|
||||
})
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function buildThemeMenu() {
|
||||
return (
|
||||
<Menu.MenuItem
|
||||
id="themes"
|
||||
label="Themes"
|
||||
action={() => openSettingsTabModal(ThemesTab)}
|
||||
>
|
||||
{buildThemeMenuEntries()}
|
||||
</Menu.MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
export function buildThemeMenuEntries() {
|
||||
const { useQuickCss, enabledThemes } = useSettings(["useQuickCss", "enabledThemes"]);
|
||||
const [themes] = useAwaiter(VencordNative.themes.getThemesList);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu.MenuCheckboxItem
|
||||
id="toggle-quickcss"
|
||||
checked={useQuickCss}
|
||||
label={"Enable QuickCSS"}
|
||||
action={() => {
|
||||
Settings.useQuickCss = !useQuickCss;
|
||||
}}
|
||||
/>
|
||||
<Menu.MenuItem
|
||||
id="edit-quickcss"
|
||||
label="Edit QuickCSS"
|
||||
action={() => VencordNative.quickCss.openEditor()}
|
||||
/>
|
||||
<Menu.MenuItem
|
||||
id="manage-themes"
|
||||
label="Manage Themes"
|
||||
action={() => openSettingsTabModal(ThemesTab)}
|
||||
/>
|
||||
{!!themes?.length && (
|
||||
<Menu.MenuGroup>
|
||||
{themes.map(theme => (
|
||||
<Menu.MenuCheckboxItem
|
||||
id={`theme-${theme.fileName}`}
|
||||
key={theme.fileName}
|
||||
label={theme.name}
|
||||
checked={enabledThemes.includes(theme.fileName)}
|
||||
action={() => {
|
||||
if (enabledThemes.includes(theme.fileName)) {
|
||||
Settings.enabledThemes = enabledThemes.filter(t => t !== theme.fileName);
|
||||
} else {
|
||||
Settings.enabledThemes = [...enabledThemes, theme.fileName];
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Menu.MenuGroup>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function buildCustomPluginEntries() {
|
||||
const pluginEntries = [] as { plugin: Plugin, node: ReactNode; }[];
|
||||
|
||||
for (const plugin of Object.values(plugins)) {
|
||||
if (plugin.toolboxActions && isPluginEnabled(plugin.name)) {
|
||||
const entries = typeof plugin.toolboxActions === "function"
|
||||
? plugin.toolboxActions()
|
||||
: Object.entries(plugin.toolboxActions).map(([text, action]) => {
|
||||
const key = `${plugin.name}-${text}`;
|
||||
|
||||
return (
|
||||
<Menu.MenuItem
|
||||
id={key}
|
||||
key={key}
|
||||
label={text}
|
||||
action={action}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
if (!entries || Array.isArray(entries) && entries.length === 0) continue;
|
||||
|
||||
pluginEntries.push({
|
||||
plugin,
|
||||
node:
|
||||
<Menu.MenuGroup label={plugin.name} key={`${plugin.name}-group`}>
|
||||
{entries}
|
||||
</Menu.MenuGroup>
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// If there aren't too many entries, just put them all in the main menu.
|
||||
// Otherwise, add submenus for each plugin
|
||||
// FIXME: the Slider component has broken styles that overlap with higher context menus
|
||||
// https://discord.com/channels/1015060230222131221/1015063227299811479/1440489344631705693
|
||||
if (pluginEntries.length <= 5)
|
||||
return pluginEntries.map(e => e.node);
|
||||
|
||||
const submenuEntries = pluginEntries.map(({ node, plugin }) => (
|
||||
<Menu.MenuItem
|
||||
id={`${plugin.name}-menu`}
|
||||
key={`${plugin.name}-menu`}
|
||||
label={plugin.name}
|
||||
action={() => openPluginModal(plugin)}
|
||||
>
|
||||
{node}
|
||||
</Menu.MenuItem>
|
||||
));
|
||||
|
||||
return <Menu.MenuGroup>{submenuEntries}</Menu.MenuGroup>;
|
||||
}
|
||||
|
||||
export function renderPopout(onClose: () => void) {
|
||||
return (
|
||||
<Menu.Menu
|
||||
navId="vc-toolbox"
|
||||
onClose={onClose}
|
||||
>
|
||||
<Menu.MenuItem
|
||||
id="notifications"
|
||||
label="Open Notification Log"
|
||||
action={openNotificationLogModal}
|
||||
/>
|
||||
|
||||
{buildThemeMenu()}
|
||||
{buildPluginMenu()}
|
||||
|
||||
{buildCustomPluginEntries()}
|
||||
</Menu.Menu >
|
||||
);
|
||||
}
|
||||
|
|
@ -26,7 +26,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
|
|||
import { Flex } from "@components/Flex";
|
||||
import { Link } from "@components/Link";
|
||||
import { Paragraph } from "@components/Paragraph";
|
||||
import { openUpdaterModal } from "@components/settings";
|
||||
import { openSettingsTabModal, UpdaterTab } from "@components/settings";
|
||||
import { platformName } from "@equicordplugins/equicordHelper/utils";
|
||||
import { gitHashShort } from "@shared/vencordUserAgent";
|
||||
import { CONTRIB_ROLE_ID, Devs, DONOR_ROLE_ID, EQUIBOP_CONTRIB_ROLE_ID, EQUICORD_TEAM, GUILD_ID, SUPPORT_CHANNEL_ID, SUPPORT_CHANNEL_IDS, VC_CONTRIB_ROLE_ID, VC_DONOR_ROLE_ID, VC_GUILD_ID, VC_REGULAR_ROLE_ID, VC_SUPPORT_CHANNEL_IDS, VENCORD_CONTRIB_ROLE_ID } from "@utils/constants";
|
||||
|
|
@ -266,7 +266,7 @@ export default definePlugin({
|
|||
Please first update before asking for support!
|
||||
</Paragraph>
|
||||
</div>,
|
||||
onCancel: () => openUpdaterModal!(),
|
||||
onCancel: () => openSettingsTabModal(UpdaterTab!),
|
||||
cancelText: "View Updates",
|
||||
confirmText: "Update & Restart Now",
|
||||
onConfirm: forceUpdate,
|
||||
|
|
|
|||
|
|
@ -1,70 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { isPluginEnabled } from "@api/PluginManager";
|
||||
import { openPluginModal } from "@components/settings/tabs";
|
||||
import { getIntlMessage } from "@utils/discord";
|
||||
import { isObjectEmpty } from "@utils/misc";
|
||||
import { Alerts, Menu, useMemo, useState } from "@webpack/common";
|
||||
|
||||
import Plugins from "~plugins";
|
||||
|
||||
function onRestartNeeded() {
|
||||
Alerts.show({
|
||||
title: "Restart required",
|
||||
body: <p>You have changed settings that require a restart.</p>,
|
||||
confirmText: "Restart now",
|
||||
cancelText: "Later!",
|
||||
onConfirm: () => location.reload()
|
||||
});
|
||||
}
|
||||
|
||||
export default function PluginsSubmenu() {
|
||||
const sortedPlugins = useMemo(() => Object.values(Plugins)
|
||||
.sort((a, b) => a.name.localeCompare(b.name)), []);
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
const search = query.toLowerCase();
|
||||
const include = (p: typeof Plugins[keyof typeof Plugins]) => (
|
||||
isPluginEnabled(p.name)
|
||||
&& p.options && !isObjectEmpty(p.options)
|
||||
&& (
|
||||
p.name.toLowerCase().includes(search)
|
||||
|| p.description.toLowerCase().includes(search)
|
||||
|| p.tags?.some(t => t.toLowerCase().includes(search))
|
||||
)
|
||||
);
|
||||
|
||||
const plugins = sortedPlugins.filter(include);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu.MenuControlItem
|
||||
id="vc-plugins-search"
|
||||
control={(props, ref) => (
|
||||
<Menu.MenuSearchControl
|
||||
{...props}
|
||||
query={query}
|
||||
onChange={setQuery}
|
||||
ref={ref}
|
||||
placeholder={getIntlMessage("SEARCH")}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
{!!plugins.length && <Menu.MenuSeparator />}
|
||||
|
||||
{plugins.map(p => (
|
||||
<Menu.MenuItem
|
||||
key={p.name}
|
||||
id={p.name}
|
||||
label={p.name}
|
||||
action={() => openPluginModal(p, onRestartNeeded)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { buildPluginMenuEntries, buildThemeMenuEntries } from "@equicordplugins/equicordToolbox/menu";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { getIntlMessage } from "@utils/discord";
|
||||
import { Logger } from "@utils/Logger";
|
||||
|
|
@ -14,8 +15,6 @@ import { waitFor } from "@webpack";
|
|||
import { ComponentDispatch, FocusLock, Menu, useEffect, useRef } from "@webpack/common";
|
||||
import type { HTMLAttributes, ReactElement } from "react";
|
||||
|
||||
import PluginsSubmenu from "./PluginsSubmenu";
|
||||
|
||||
type SettingsEntry = { section: string, label: string; };
|
||||
|
||||
const cl = classNameFactory("");
|
||||
|
|
@ -131,13 +130,14 @@ export default definePlugin({
|
|||
},
|
||||
{
|
||||
match: /case \i\.\i\.DEVELOPER_OPTIONS:return \i;/,
|
||||
replace: "$&case 'EquicordPlugins':return $self.PluginsSubmenu();"
|
||||
replace: "$&case 'EquicordPlugins':return $self.buildPluginMenuEntries(true);$&case 'EquicordThemes':return $self.buildThemeMenuEntries();"
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
|
||||
PluginsSubmenu,
|
||||
buildPluginMenuEntries,
|
||||
buildThemeMenuEntries,
|
||||
|
||||
// This is the very outer layer of the entire ui, so we can't wrap this in an ErrorBoundary
|
||||
// without possibly also catching unrelated errors of children.
|
||||
|
|
|
|||
|
|
@ -8,18 +8,20 @@ import { definePluginSettings } from "@api/Settings";
|
|||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { makeRange, OptionType } from "@utils/types";
|
||||
|
||||
const settings = definePluginSettings({
|
||||
volume: {
|
||||
type: OptionType.SLIDER,
|
||||
description: "The volume % to set for spotify embeds. Anything above 10% is veeeery loud",
|
||||
markers: makeRange(0, 100, 10),
|
||||
stickToMarkers: false,
|
||||
default: 10
|
||||
}
|
||||
});
|
||||
|
||||
// The entire code of this plugin can be found in ipcPlugins
|
||||
export default definePlugin({
|
||||
name: "FixSpotifyEmbeds",
|
||||
description: "Fixes spotify embeds being incredibly loud by letting you customise the volume",
|
||||
authors: [Devs.Ven],
|
||||
settings: definePluginSettings({
|
||||
volume: {
|
||||
type: OptionType.SLIDER,
|
||||
description: "The volume % to set for spotify embeds. Anything above 10% is veeeery loud",
|
||||
markers: makeRange(0, 100, 10),
|
||||
stickToMarkers: false,
|
||||
default: 10
|
||||
}
|
||||
})
|
||||
settings,
|
||||
});
|
||||
|
|
@ -4,24 +4,60 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { app } from "electron";
|
||||
import { RendererSettings } from "main/settings";
|
||||
import { RendererSettings } from "@main/settings";
|
||||
import { app, WebFrameMain, webFrameMain } from "electron";
|
||||
|
||||
// TODO: routingID is deprecated and should be replaced with frameToken, but it's too new
|
||||
const ids = [] as Record<"routingId" | "processId", number>[];
|
||||
|
||||
function cleanUpAndGetSpotifyFrames() {
|
||||
const spotifyFrames = [] as WebFrameMain[];
|
||||
for (let i = ids.length - 1; i >= 0; i--) {
|
||||
const { processId, routingId } = ids[i];
|
||||
|
||||
const frame = webFrameMain.fromId(processId, routingId);
|
||||
if (!frame) {
|
||||
ids.splice(i, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
spotifyFrames.push(frame);
|
||||
}
|
||||
|
||||
return spotifyFrames;
|
||||
}
|
||||
|
||||
app.on("browser-window-created", (_, win) => {
|
||||
win.webContents.on("frame-created", (_, { frame }) => {
|
||||
frame?.once("dom-ready", () => {
|
||||
if (frame.url.startsWith("https://open.spotify.com/embed/")) {
|
||||
cleanUpAndGetSpotifyFrames(); // clean up stale frames
|
||||
|
||||
const { routingId, processId } = frame;
|
||||
ids.push({ routingId, processId });
|
||||
|
||||
const settings = RendererSettings.store.plugins?.FixSpotifyEmbeds;
|
||||
if (!settings?.enabled) return;
|
||||
|
||||
frame.executeJavaScript(`
|
||||
globalThis._vcVolume = ${settings.volume / 100};
|
||||
const original = Audio.prototype.play;
|
||||
Audio.prototype.play = function() {
|
||||
this.volume = ${(settings.volume / 100) || 0.1};
|
||||
this.volume = _vcVolume;
|
||||
return original.apply(this, arguments);
|
||||
}
|
||||
`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
RendererSettings.addChangeListener("plugins.FixSpotifyEmbeds.volume", newVolume => {
|
||||
try {
|
||||
cleanUpAndGetSpotifyFrames().forEach(frame =>
|
||||
frame.executeJavaScript(`globalThis._vcVolume = ${newVolume / 100}`)
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("FixSpotifyEmbeds: Failed to update volume", e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,12 +16,15 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { isPluginEnabled } from "@api/PluginManager";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { getUserSettingLazy } from "@api/UserSettings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import VencordToolboxPlugin from "@plugins/vencordToolbox";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findComponentByCodeLazy } from "@webpack";
|
||||
import { Menu } from "@webpack/common";
|
||||
|
||||
import managedStyle from "./style.css?managed";
|
||||
|
||||
|
|
@ -29,8 +32,29 @@ const Button = findComponentByCodeLazy(".NONE,disabled:", ".PANEL_BUTTON");
|
|||
|
||||
const ShowCurrentGame = getUserSettingLazy<boolean>("status", "showCurrentGame")!;
|
||||
|
||||
function makeIcon(showCurrentGame?: boolean) {
|
||||
const settings = definePluginSettings({
|
||||
oldIcon: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Use the old icon style before Discord icon redesign",
|
||||
default: false
|
||||
},
|
||||
location: {
|
||||
type: OptionType.SELECT,
|
||||
description: "Where to show the game activity toggle button",
|
||||
options: [
|
||||
{ label: "Next to Mute/Deafen", value: "PANEL", default: true },
|
||||
{ label: "Vencord Toolbox", value: "TOOLBOX" }
|
||||
],
|
||||
get hidden() {
|
||||
return !isPluginEnabled(VencordToolboxPlugin.name);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function Icon() {
|
||||
const { oldIcon } = settings.use(["oldIcon"]);
|
||||
const showCurrentGame = ShowCurrentGame.useSetting();
|
||||
|
||||
|
||||
const redLinePath = !oldIcon
|
||||
? "M22.7 2.7a1 1 0 0 0-1.4-1.4l-20 20a1 1 0 1 0 1.4 1.4Z"
|
||||
|
|
@ -40,34 +64,35 @@ function makeIcon(showCurrentGame?: boolean) {
|
|||
? "M23.27 4.73 19.27 .73 -.27 20.27 3.73 24.27Z"
|
||||
: "M23.27 4.54 19.46.73 .73 19.46 4.54 23.27 23.27 4.54Z";
|
||||
|
||||
return function () {
|
||||
return (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill={!showCurrentGame && !oldIcon ? "var(--status-danger)" : "currentColor"}
|
||||
mask={!showCurrentGame ? "url(#gameActivityMask)" : void 0}
|
||||
d="M3.06 20.4q-1.53 0-2.37-1.065T.06 16.74l1.26-9q.27-1.8 1.605-2.97T6.06 3.6h11.88q1.8 0 3.135 1.17t1.605 2.97l1.26 9q.21 1.53-.63 2.595T20.94 20.4q-.63 0-1.17-.225T18.78 19.5l-2.7-2.7H7.92l-2.7 2.7q-.45.45-.99.675t-1.17.225Zm14.94-7.2q.51 0 .855-.345T19.2 12q0-.51-.345-.855T18 10.8q-.51 0-.855.345T16.8 12q0 .51.345 .855T18 13.2Zm-2.4-3.6q.51 0 .855-.345T16.8 8.4q0-.51-.345-.855T15.6 7.2q-.51 0-.855.345T14.4 8.4q0 .51.345 .855T15.6 9.6ZM6.9 13.2h1.8v-2.1h2.1v-1.8h-2.1v-2.1h-1.8v2.1h-2.1v1.8h2.1v2.1Z"
|
||||
/>
|
||||
{!showCurrentGame && <>
|
||||
<path fill="var(--status-danger)" d={redLinePath} />
|
||||
<mask id="gameActivityMask">
|
||||
<rect fill="white" x="0" y="0" width="24" height="24" />
|
||||
<path fill="black" d={maskBlackPath} />
|
||||
</mask>
|
||||
</>}
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill={!showCurrentGame && !oldIcon ? "var(--status-danger)" : "currentColor"}
|
||||
mask={!showCurrentGame ? "url(#gameActivityMask)" : void 0}
|
||||
d="M3.06 20.4q-1.53 0-2.37-1.065T.06 16.74l1.26-9q.27-1.8 1.605-2.97T6.06 3.6h11.88q1.8 0 3.135 1.17t1.605 2.97l1.26 9q.21 1.53-.63 2.595T20.94 20.4q-.63 0-1.17-.225T18.78 19.5l-2.7-2.7H7.92l-2.7 2.7q-.45.45-.99.675t-1.17.225Zm14.94-7.2q.51 0 .855-.345T19.2 12q0-.51-.345-.855T18 10.8q-.51 0-.855.345T16.8 12q0 .51.345 .855T18 13.2Zm-2.4-3.6q.51 0 .855-.345T16.8 8.4q0-.51-.345-.855T15.6 7.2q-.51 0-.855.345T14.4 8.4q0 .51.345 .855T15.6 9.6ZM6.9 13.2h1.8v-2.1h2.1v-1.8h-2.1v-2.1h-1.8v2.1h-2.1v1.8h2.1v2.1Z"
|
||||
/>
|
||||
{!showCurrentGame && <>
|
||||
<path fill="var(--status-danger)" d={redLinePath} />
|
||||
<mask id="gameActivityMask">
|
||||
<rect fill="white" x="0" y="0" width="24" height="24" />
|
||||
<path fill="black" d={maskBlackPath} />
|
||||
</mask>
|
||||
</>}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function GameActivityToggleButton(props: { nameplate?: any; }) {
|
||||
const { location } = settings.use(["location"]);
|
||||
const showCurrentGame = ShowCurrentGame.useSetting();
|
||||
|
||||
if (location !== "PANEL" && isPluginEnabled(VencordToolboxPlugin.name)) return null;
|
||||
|
||||
return (
|
||||
<Button
|
||||
className="vc-game-activity"
|
||||
tooltipText={showCurrentGame ? "Disable Game Activity" : "Enable Game Activity"}
|
||||
icon={makeIcon(showCurrentGame)}
|
||||
icon={Icon}
|
||||
role="switch"
|
||||
aria-checked={!showCurrentGame}
|
||||
redGlow={!showCurrentGame}
|
||||
|
|
@ -77,14 +102,6 @@ function GameActivityToggleButton(props: { nameplate?: any; }) {
|
|||
);
|
||||
}
|
||||
|
||||
const settings = definePluginSettings({
|
||||
oldIcon: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Use the old icon style before Discord icon redesign",
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "GameActivityToggle",
|
||||
description: "Adds a button next to the mic and deafen button to toggle game activity.",
|
||||
|
|
@ -104,6 +121,21 @@ export default definePlugin({
|
|||
}
|
||||
],
|
||||
|
||||
GameActivityToggleButton: ErrorBoundary.wrap(GameActivityToggleButton, { noop: true }),
|
||||
toolboxActions() {
|
||||
const { location } = settings.use(["location"]);
|
||||
const showCurrentGame = ShowCurrentGame.useSetting();
|
||||
|
||||
if (location !== "TOOLBOX") return null;
|
||||
|
||||
return (
|
||||
<Menu.MenuCheckboxItem
|
||||
id="game-activity-toggle-toolbox"
|
||||
label="Enable Game Activity"
|
||||
checked={showCurrentGame}
|
||||
action={() => ShowCurrentGame.updateSetting(old => !old)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
GameActivityToggleButton: ErrorBoundary.wrap(GameActivityToggleButton, { noop: true }),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -44,6 +44,15 @@ migratePluginSettings("HideMedia", "HideAttachments");
|
|||
|
||||
const hasMedia = (msg: Message) => msg.attachments.length > 0 || msg.embeds.length > 0 || msg.stickerItems.length > 0;
|
||||
|
||||
async function toggleHide(channelId: string, messageId: string) {
|
||||
const ids = await getHiddenMessages();
|
||||
if (!ids.delete(messageId))
|
||||
ids.add(messageId);
|
||||
|
||||
await saveHiddenMessages(ids);
|
||||
updateMessage(channelId, messageId);
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "HideMedia",
|
||||
description: "Hide attachments and embeds for individual messages via hover button",
|
||||
|
|
@ -58,18 +67,21 @@ export default definePlugin({
|
|||
}
|
||||
}],
|
||||
|
||||
renderMessagePopoverButton(msg) {
|
||||
if (!hasMedia(msg) && !msg.messageSnapshots.some(s => hasMedia(s.message))) return null;
|
||||
messagePopoverButton: {
|
||||
icon: ImageInvisible,
|
||||
render(msg) {
|
||||
if (!hasMedia(msg) && !msg.messageSnapshots.some(s => hasMedia(s.message))) return null;
|
||||
|
||||
const isHidden = hiddenMessages.has(msg.id);
|
||||
const isHidden = hiddenMessages.has(msg.id);
|
||||
|
||||
return {
|
||||
label: isHidden ? "Show Media" : "Hide Media",
|
||||
icon: isHidden ? ImageVisible : ImageInvisible,
|
||||
message: msg,
|
||||
channel: ChannelStore.getChannel(msg.channel_id),
|
||||
onClick: () => this.toggleHide(msg.channel_id, msg.id)
|
||||
};
|
||||
return {
|
||||
label: isHidden ? "Show Media" : "Hide Media",
|
||||
icon: isHidden ? ImageVisible : ImageInvisible,
|
||||
message: msg,
|
||||
channel: ChannelStore.getChannel(msg.channel_id),
|
||||
onClick: () => toggleHide(msg.channel_id, msg.id)
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
renderMessageAccessory({ message }) {
|
||||
|
|
@ -93,13 +105,4 @@ export default definePlugin({
|
|||
shouldHide(messageId: string) {
|
||||
return hiddenMessages.has(messageId);
|
||||
},
|
||||
|
||||
async toggleHide(channelId: string, messageId: string) {
|
||||
const ids = await getHiddenMessages();
|
||||
if (!ids.delete(messageId))
|
||||
ids.add(messageId);
|
||||
|
||||
await saveHiddenMessages(ids);
|
||||
updateMessage(channelId, messageId);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
import { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons";
|
||||
import { generateId, sendBotMessage } from "@api/Commands";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { StartAt } from "@utils/types";
|
||||
import definePlugin, { IconComponent, StartAt } from "@utils/types";
|
||||
import { CloudUpload, MessageAttachment } from "@vencord/discord-types";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { DraftStore, DraftType, SelectedChannelStore, UserStore, useStateFromStores } from "@webpack/common";
|
||||
|
|
@ -73,6 +73,22 @@ const getAttachments = async (channelId: string) =>
|
|||
);
|
||||
|
||||
|
||||
const PreviewIcon: IconComponent = ({ height = 20, width = 20, className }) => {
|
||||
return (
|
||||
<svg
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
width={width}
|
||||
height={height}
|
||||
className={className}
|
||||
viewBox="0 0 24 24"
|
||||
style={{ scale: "1.096", translate: "0 -1px" }}
|
||||
>
|
||||
<path d="M22.89 11.7c.07.2.07.4 0 .6C22.27 13.9 19.1 21 12 21c-7.11 0-10.27-7.11-10.89-8.7a.83.83 0 0 1 0-.6C1.73 10.1 4.9 3 12 3c7.11 0 10.27 7.11 10.89 8.7Zm-4.5-3.62A15.11 15.11 0 0 1 20.85 12c-.38.88-1.18 2.47-2.46 3.92C16.87 17.62 14.8 19 12 19c-2.8 0-4.87-1.38-6.39-3.08A15.11 15.11 0 0 1 3.15 12c.38-.88 1.18-2.47 2.46-3.92C7.13 6.38 9.2 5 12 5c2.8 0 4.87 1.38 6.39 3.08ZM15.56 11.77c.2-.1.44.02.44.23a4 4 0 1 1-4-4c.21 0 .33.25.23.44a2.5 2.5 0 0 0 3.32 3.32Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const PreviewButton: ChatBarButtonFactory = ({ isMainChat, isEmpty, type: { attachments } }) => {
|
||||
const channelId = SelectedChannelStore.getChannelId();
|
||||
const draft = useStateFromStores([DraftStore], () => getDraft(channelId));
|
||||
|
|
@ -102,16 +118,7 @@ const PreviewButton: ChatBarButtonFactory = ({ isMainChat, isEmpty, type: { atta
|
|||
}
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
style={{ scale: "1.096", translate: "0 -1px" }}
|
||||
>
|
||||
<path d="M22.89 11.7c.07.2.07.4 0 .6C22.27 13.9 19.1 21 12 21c-7.11 0-10.27-7.11-10.89-8.7a.83.83 0 0 1 0-.6C1.73 10.1 4.9 3 12 3c7.11 0 10.27 7.11 10.89 8.7Zm-4.5-3.62A15.11 15.11 0 0 1 20.85 12c-.38.88-1.18 2.47-2.46 3.92C16.87 17.62 14.8 19 12 19c-2.8 0-4.87-1.38-6.39-3.08A15.11 15.11 0 0 1 3.15 12c.38-.88 1.18-2.47 2.46-3.92C7.13 6.38 9.2 5 12 5c2.8 0 4.87 1.38 6.39 3.08ZM15.56 11.77c.2-.1.44.02.44.23a4 4 0 1 1-4-4c.21 0 .33.25.23.44a2.5 2.5 0 0 0 3.32 3.32Z" />
|
||||
</svg>
|
||||
<PreviewIcon />
|
||||
</ChatBarButton>
|
||||
);
|
||||
|
||||
|
|
@ -125,5 +132,8 @@ export default definePlugin({
|
|||
// This makes the popping in less awkward
|
||||
startAt: StartAt.Init,
|
||||
|
||||
renderChatBarButton: PreviewButton,
|
||||
chatBarButton: {
|
||||
icon: PreviewIcon,
|
||||
render: PreviewButton
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,29 +21,12 @@ import { insertTextIntoChatInputBox } from "@utils/discord";
|
|||
import definePlugin from "@utils/types";
|
||||
import { ChannelStore, PermissionsBits, PermissionStore } from "@webpack/common";
|
||||
|
||||
export default definePlugin({
|
||||
name: "QuickMention",
|
||||
authors: [Devs.kemo],
|
||||
description: "Adds a quick mention button to the message actions bar",
|
||||
|
||||
renderMessagePopoverButton(msg) {
|
||||
const channel = ChannelStore.getChannel(msg.channel_id);
|
||||
if (channel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, channel)) return null;
|
||||
|
||||
return {
|
||||
label: "Quick Mention",
|
||||
icon: this.Icon,
|
||||
message: msg,
|
||||
channel,
|
||||
onClick: () => insertTextIntoChatInputBox(`<@${msg.author.id}> `)
|
||||
};
|
||||
},
|
||||
|
||||
Icon: () => (
|
||||
function Icon({ height = 24, width = 24, className = "icon" }: { height?: number | string; width?: number | string; className?: string; }) {
|
||||
return (
|
||||
<svg
|
||||
className="icon"
|
||||
height="24"
|
||||
width="24"
|
||||
className={className}
|
||||
height={height}
|
||||
width={width}
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
|
|
@ -51,5 +34,27 @@ export default definePlugin({
|
|||
d="M12 2C6.486 2 2 6.486 2 12C2 17.515 6.486 22 12 22C14.039 22 15.993 21.398 17.652 20.259L16.521 18.611C15.195 19.519 13.633 20 12 20C7.589 20 4 16.411 4 12C4 7.589 7.589 4 12 4C16.411 4 20 7.589 20 12V12.782C20 14.17 19.402 15 18.4 15L18.398 15.018C18.338 15.005 18.273 15 18.209 15H18C17.437 15 16.6 14.182 16.6 13.631V12C16.6 9.464 14.537 7.4 12 7.4C9.463 7.4 7.4 9.463 7.4 12C7.4 14.537 9.463 16.6 12 16.6C13.234 16.6 14.35 16.106 15.177 15.313C15.826 16.269 16.93 17 18 17L18.002 16.981C18.064 16.994 18.129 17 18.195 17H18.4C20.552 17 22 15.306 22 12.782V12C22 6.486 17.514 2 12 2ZM12 14.599C10.566 14.599 9.4 13.433 9.4 11.999C9.4 10.565 10.566 9.399 12 9.399C13.434 9.399 14.6 10.565 14.6 11.999C14.6 13.433 13.434 14.599 12 14.599Z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "QuickMention",
|
||||
authors: [Devs.kemo],
|
||||
description: "Adds a quick mention button to the message actions bar",
|
||||
|
||||
messagePopoverButton: {
|
||||
icon: Icon,
|
||||
render(msg) {
|
||||
const channel = ChannelStore.getChannel(msg.channel_id);
|
||||
if (channel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, channel)) return null;
|
||||
|
||||
return {
|
||||
label: "Quick Mention",
|
||||
icon: Icon,
|
||||
message: msg,
|
||||
channel,
|
||||
onClick: () => insertTextIntoChatInputBox(`<@${msg.author.id}> `)
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import { Devs } from "@utils/constants";
|
|||
import { getTheme, insertTextIntoChatInputBox, Theme } from "@utils/discord";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/modal";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import definePlugin, { IconComponent, OptionType } from "@utils/types";
|
||||
import { Button, Parser, Select, useMemo, useState } from "@webpack/common";
|
||||
|
||||
const settings = definePluginSettings({
|
||||
|
|
@ -127,7 +127,26 @@ function PickerModal({ rootProps, close }: { rootProps: ModalProps, close(): voi
|
|||
);
|
||||
}
|
||||
|
||||
const ChatBarIcon: ChatBarButtonFactory = ({ isMainChat }) => {
|
||||
const SendTimestampIcon: IconComponent = ({ height = 20, width = 20, className }) => {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
width={width}
|
||||
height={height}
|
||||
className={className}
|
||||
viewBox="0 0 24 24"
|
||||
style={{ scale: "1.2" }}
|
||||
>
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<path fill="currentColor" d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM7 10h5v5H7v-5z" />
|
||||
<rect width="24" height="24" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const SendTimestampButton: ChatBarButtonFactory = ({ isMainChat }) => {
|
||||
if (!isMainChat) return null;
|
||||
|
||||
return (
|
||||
|
|
@ -143,18 +162,7 @@ const ChatBarIcon: ChatBarButtonFactory = ({ isMainChat }) => {
|
|||
}}
|
||||
buttonProps={{ "aria-haspopup": "dialog" }}
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<path fill="currentColor" d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM7 10h5v5H7v-5z" />
|
||||
<rect width="24" height="24" />
|
||||
</g>
|
||||
</svg>
|
||||
<SendTimestampIcon />
|
||||
</ChatBarButton>
|
||||
);
|
||||
};
|
||||
|
|
@ -165,7 +173,10 @@ export default definePlugin({
|
|||
authors: [Devs.Ven, Devs.Tyler, Devs.Grzesiek11],
|
||||
settings,
|
||||
|
||||
renderChatBarButton: ChatBarIcon,
|
||||
chatBarButton: {
|
||||
icon: SendTimestampIcon,
|
||||
render: SendTimestampButton
|
||||
},
|
||||
|
||||
onBeforeMessageSend(_, msg) {
|
||||
if (settings.store.replaceMessageContents) {
|
||||
|
|
|
|||
|
|
@ -100,8 +100,8 @@ export const settings = definePluginSettings({
|
|||
description: "Background opacity",
|
||||
markers: [0, 20, 40, 60, 80, 100],
|
||||
default: 100,
|
||||
stickToMarkers: false,
|
||||
componentProps: {
|
||||
stickToMarkers: false,
|
||||
onValueRender: null, // Defaults to percentage
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons";
|
|||
import { addMessagePreSendListener, MessageSendListener, removeMessagePreSendListener } from "@api/MessageEvents";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Devs, EquicordDevs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import definePlugin, { IconComponent, OptionType } from "@utils/types";
|
||||
import { React, useEffect, useState } from "@webpack/common";
|
||||
|
||||
let lastState = false;
|
||||
|
|
@ -47,6 +47,33 @@ const settings = definePluginSettings({
|
|||
}
|
||||
});
|
||||
|
||||
function SilentMessageDisabledIcon() {
|
||||
return (
|
||||
<SilentMessageIcon>
|
||||
<mask id="vc-silent-msg-mask">
|
||||
<path fill="#fff" d="M0 0h24v24H0Z" />
|
||||
<path stroke="#000" strokeWidth="5.99068" d="M0 24 24 0" />
|
||||
</mask>
|
||||
<path fill="var(--status-danger)" d="m21.178 1.70703 1.414 1.414L4.12103 21.593l-1.414-1.415L21.178 1.70703Z" />
|
||||
</SilentMessageIcon>
|
||||
);
|
||||
}
|
||||
|
||||
const SilentMessageIcon: IconComponent = ({ height = 20, width = 20, className, children }) => {
|
||||
return (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 24 24"
|
||||
className={className}
|
||||
style={{ scale: "1.2" }}
|
||||
>
|
||||
<path fill="currentColor" mask="url(#vc-silent-msg-mask)" d="M18 10.7101C15.1085 9.84957 13 7.17102 13 4c0-.30736.0198-.6101.0582-.907C12.7147 3.03189 12.3611 3 12 3 8.686 3 6 5.686 6 9v5c0 1.657-1.344 3-3 3v1h18v-1c-1.656 0-3-1.343-3-3v-3.2899ZM8.55493 19c.693 1.19 1.96897 2 3.44497 2s2.752-.81 3.445-2H8.55493ZM18.2624 5.50209 21 2.5V1h-4.9651v1.49791h2.4411L16 5.61088V7h5V5.50209h-2.7376Z" />
|
||||
{children}
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const SilentMessageToggle: ChatBarButtonFactory = ({ isMainChat }) => {
|
||||
const [enabled, setEnabled] = useState(settings.store.persistState === "restarts" || lastState);
|
||||
|
||||
|
|
@ -74,21 +101,7 @@ const SilentMessageToggle: ChatBarButtonFactory = ({ isMainChat }) => {
|
|||
tooltip={enabled ? "Disable Silent Message" : "Enable Silent Message"}
|
||||
onClick={() => setEnabledValue(!enabled)}
|
||||
>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
style={{ scale: "1.2" }}
|
||||
>
|
||||
<path fill="currentColor" mask="url(#vc-silent-msg-mask)" d="M18 10.7101C15.1085 9.84957 13 7.17102 13 4c0-.30736.0198-.6101.0582-.907C12.7147 3.03189 12.3611 3 12 3 8.686 3 6 5.686 6 9v5c0 1.657-1.344 3-3 3v1h18v-1c-1.656 0-3-1.343-3-3v-3.2899ZM8.55493 19c.693 1.19 1.96897 2 3.44497 2s2.752-.81 3.445-2H8.55493ZM18.2624 5.50209 21 2.5V1h-4.9651v1.49791h2.4411L16 5.61088V7h5V5.50209h-2.7376Z" />
|
||||
{!enabled && <>
|
||||
<mask id="vc-silent-msg-mask">
|
||||
<path fill="#fff" d="M0 0h24v24H0Z" />
|
||||
<path stroke="#000" strokeWidth="5.99068" d="M0 24 24 0" />
|
||||
</mask>
|
||||
<path fill="var(--status-danger)" d="m21.178 1.70703 1.414 1.414L4.12103 21.593l-1.414-1.415L21.178 1.70703Z" />
|
||||
</>}
|
||||
</svg>
|
||||
{enabled ? <SilentMessageIcon /> : <SilentMessageDisabledIcon />}
|
||||
</ChatBarButton>
|
||||
);
|
||||
};
|
||||
|
|
@ -99,5 +112,8 @@ export default definePlugin({
|
|||
description: "Adds a button to the chat bar to toggle sending a silent message.",
|
||||
settings,
|
||||
|
||||
renderChatBarButton: SilentMessageToggle,
|
||||
chatBarButton: {
|
||||
icon: SilentMessageIcon,
|
||||
render: SilentMessageToggle
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -20,13 +20,14 @@ import { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons";
|
|||
import { Paragraph } from "@components/Paragraph";
|
||||
import { classes } from "@utils/misc";
|
||||
import { openModal } from "@utils/modal";
|
||||
import { IconComponent } from "@utils/types";
|
||||
import { Alerts, Tooltip, useEffect, useState } from "@webpack/common";
|
||||
|
||||
import { settings } from "./settings";
|
||||
import { TranslateModal } from "./TranslateModal";
|
||||
import { cl } from "./utils";
|
||||
|
||||
export function TranslateIcon({ height = 20, width = 20, className }: { height?: number; width?: number; className?: string; }) {
|
||||
export const TranslateIcon: IconComponent = ({ height = 20, width = 20, className }) => {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 96 960 960"
|
||||
|
|
@ -37,7 +38,7 @@ export function TranslateIcon({ height = 20, width = 20, className }: { height?:
|
|||
<path fill="currentColor" d="m475 976 181-480h82l186 480h-87l-41-126H604l-47 126h-82Zm151-196h142l-70-194h-2l-70 194Zm-466 76-55-55 204-204q-38-44-67.5-88.5T190 416h87q17 33 37.5 62.5T361 539q45-47 75-97.5T487 336H40v-80h280v-80h80v80h280v80H567q-22 69-58.5 135.5T419 598l98 99-30 81-127-122-200 200Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export let setShouldShowTranslateEnabledTooltip: undefined | ((show: boolean) => void);
|
||||
|
||||
|
|
|
|||
|
|
@ -74,22 +74,28 @@ export default definePlugin({
|
|||
|
||||
renderMessageAccessory: props => <TranslationAccessory message={props.message} />,
|
||||
|
||||
renderChatBarButton: TranslateChatBarIcon,
|
||||
chatBarButton: {
|
||||
icon: TranslateIcon,
|
||||
render: TranslateChatBarIcon
|
||||
},
|
||||
|
||||
renderMessagePopoverButton(message: Message) {
|
||||
const content = getMessageContent(message);
|
||||
if (!content) return null;
|
||||
messagePopoverButton: {
|
||||
icon: TranslateIcon,
|
||||
render(message: Message) {
|
||||
const content = getMessageContent(message);
|
||||
if (!content) return null;
|
||||
|
||||
return {
|
||||
label: "Translate",
|
||||
icon: TranslateIcon,
|
||||
message,
|
||||
channel: ChannelStore.getChannel(message.channel_id),
|
||||
onClick: async () => {
|
||||
const trans = await translate("received", content);
|
||||
handleTranslate(message.id, trans);
|
||||
}
|
||||
};
|
||||
return {
|
||||
label: "Translate",
|
||||
icon: TranslateIcon,
|
||||
message,
|
||||
channel: ChannelStore.getChannel(message.channel_id),
|
||||
onClick: async () => {
|
||||
const trans = await translate("received", content);
|
||||
handleTranslate(message.id, trans);
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
async onBeforeMessageSend(_, message) {
|
||||
|
|
|
|||
|
|
@ -28,16 +28,25 @@ import { Devs } from "@utils/constants";
|
|||
import { copyWithToast, getCurrentGuild, getIntlMessage } from "@utils/discord";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import definePlugin, { IconComponent, OptionType } from "@utils/types";
|
||||
import { Message } from "@vencord/discord-types";
|
||||
import { Button, ChannelStore, GuildRoleStore, Menu } from "@webpack/common";
|
||||
|
||||
|
||||
const CopyIcon = () => {
|
||||
return <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" width="20" height="20">
|
||||
<path d="M12.9297 3.25007C12.7343 3.05261 12.4154 3.05226 12.2196 3.24928L11.5746 3.89824C11.3811 4.09297 11.3808 4.40733 11.5739 4.60245L16.5685 9.64824C16.7614 9.84309 16.7614 10.1569 16.5685 10.3517L11.5739 15.3975C11.3808 15.5927 11.3811 15.907 11.5746 16.1017L12.2196 16.7507C12.4154 16.9477 12.7343 16.9474 12.9297 16.7499L19.2604 10.3517C19.4532 10.1568 19.4532 9.84314 19.2604 9.64832L12.9297 3.25007Z" />
|
||||
<path d="M8.42616 4.60245C8.6193 4.40733 8.61898 4.09297 8.42545 3.89824L7.78047 3.24928C7.58466 3.05226 7.26578 3.05261 7.07041 3.25007L0.739669 9.64832C0.5469 9.84314 0.546901 10.1568 0.739669 10.3517L7.07041 16.7499C7.26578 16.9474 7.58465 16.9477 7.78047 16.7507L8.42545 16.1017C8.61898 15.907 8.6193 15.5927 8.42616 15.3975L3.43155 10.3517C3.23869 10.1569 3.23869 9.84309 3.43155 9.64824L8.42616 4.60245Z" />
|
||||
</svg>;
|
||||
const CopyIcon: IconComponent = ({ height = 20, width = 20, className }) => {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
width={width}
|
||||
height={height}
|
||||
className={className}
|
||||
>
|
||||
<path d="M12.9297 3.25007C12.7343 3.05261 12.4154 3.05226 12.2196 3.24928L11.5746 3.89824C11.3811 4.09297 11.3808 4.40733 11.5739 4.60245L16.5685 9.64824C16.7614 9.84309 16.7614 10.1569 16.5685 10.3517L11.5739 15.3975C11.3808 15.5927 11.3811 15.907 11.5746 16.1017L12.2196 16.7507C12.4154 16.9477 12.7343 16.9474 12.9297 16.7499L19.2604 10.3517C19.4532 10.1568 19.4532 9.84314 19.2604 9.64832L12.9297 3.25007Z" />
|
||||
<path d="M8.42616 4.60245C8.6193 4.40733 8.61898 4.09297 8.42545 3.89824L7.78047 3.24928C7.58466 3.05226 7.26578 3.05261 7.07041 3.25007L0.739669 9.64832C0.5469 9.84314 0.546901 10.1568 0.739669 10.3517L7.07041 16.7499C7.26578 16.9474 7.58465 16.9477 7.78047 16.7507L8.42545 16.1017C8.61898 15.907 8.6193 15.5927 8.42616 15.3975L3.43155 10.3517C3.23869 10.1569 3.23869 9.84309 3.43155 9.64824L8.42616 4.60245Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
function sortObject<T extends object>(obj: T): T {
|
||||
|
|
@ -135,9 +144,11 @@ function MakeContextCallback(name: "Guild" | "Role" | "User" | "Channel"): NavCo
|
|||
children = p.children;
|
||||
}
|
||||
|
||||
// typescript parser goes crazy if this is inline
|
||||
const id = `vc-view-${name.toLowerCase()}-raw`;
|
||||
children.splice(-1, 0,
|
||||
<Menu.MenuItem
|
||||
id={`vc-view-${name.toLowerCase()}-raw`}
|
||||
id={id}
|
||||
label="View Raw"
|
||||
action={() => openViewRawModal(JSON.stringify(value, null, 4), name)}
|
||||
icon={CopyIcon}
|
||||
|
|
@ -179,38 +190,41 @@ export default definePlugin({
|
|||
"dev-context": devContextCallback
|
||||
},
|
||||
|
||||
renderMessagePopoverButton(msg) {
|
||||
const handleClick = () => {
|
||||
if (settings.store.clickMethod === "Right") {
|
||||
copyWithToast(msg.content);
|
||||
} else {
|
||||
openViewRawModalMessage(msg);
|
||||
}
|
||||
};
|
||||
messagePopoverButton: {
|
||||
icon: CopyIcon,
|
||||
render(msg) {
|
||||
const handleClick = () => {
|
||||
if (settings.store.clickMethod === "Right") {
|
||||
copyWithToast(msg.content);
|
||||
} else {
|
||||
openViewRawModalMessage(msg);
|
||||
}
|
||||
};
|
||||
|
||||
const handleContextMenu = e => {
|
||||
if (settings.store.clickMethod === "Left") {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
copyWithToast(msg.content);
|
||||
} else {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
openViewRawModalMessage(msg);
|
||||
}
|
||||
};
|
||||
const handleContextMenu = e => {
|
||||
if (settings.store.clickMethod === "Left") {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
copyWithToast(msg.content);
|
||||
} else {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
openViewRawModalMessage(msg);
|
||||
}
|
||||
};
|
||||
|
||||
const label = settings.store.clickMethod === "Right"
|
||||
? "Copy Raw (Left Click) / View Raw (Right Click)"
|
||||
: "View Raw (Left Click) / Copy Raw (Right Click)";
|
||||
const label = settings.store.clickMethod === "Right"
|
||||
? "Copy Raw (Left Click) / View Raw (Right Click)"
|
||||
: "View Raw (Left Click) / Copy Raw (Right Click)";
|
||||
|
||||
return {
|
||||
label,
|
||||
icon: CopyIcon,
|
||||
message: msg,
|
||||
channel: ChannelStore.getChannel(msg.channel_id),
|
||||
onClick: handleClick,
|
||||
onContextMenu: handleContextMenu
|
||||
};
|
||||
return {
|
||||
label,
|
||||
icon: CopyIcon,
|
||||
message: msg,
|
||||
channel: ChannelStore.getChannel(msg.channel_id),
|
||||
onClick: handleClick,
|
||||
onContextMenu: handleContextMenu
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,7 +10,12 @@ export const SYM_IS_PROXY = Symbol("SettingsStore.isProxy");
|
|||
export const SYM_GET_RAW_TARGET = Symbol("SettingsStore.getRawTarget");
|
||||
|
||||
// Resolves a possibly nested prop in the form of "some.nested.prop" to type of T.some.nested.prop
|
||||
type ResolvePropDeep<T, P> = P extends `${infer Pre}.${infer Suf}`
|
||||
type ResolvePropDeep<T, P> =
|
||||
P extends `${infer Pre}.*` ?
|
||||
Pre extends keyof T
|
||||
? T[Pre][keyof T[Pre]]
|
||||
: any
|
||||
: P extends `${infer Pre}.${infer Suf}`
|
||||
? Pre extends keyof T
|
||||
? ResolvePropDeep<T[Pre], Suf>
|
||||
: any
|
||||
|
|
@ -42,6 +47,7 @@ interface ProxyContext<T extends object = any> {
|
|||
*/
|
||||
export class SettingsStore<T extends object> {
|
||||
private pathListeners = new Map<string, Set<(newData: any) => void>>();
|
||||
private prefixListeners = new Map<string, Set<(newData: any, path: string) => void>>();
|
||||
private globalListeners = new Set<(newData: T, path: string) => void>();
|
||||
private readonly proxyContexts = new WeakMap<any, ProxyContext<T>>();
|
||||
|
||||
|
|
@ -152,6 +158,13 @@ export class SettingsStore<T extends object> {
|
|||
return new Proxy(object, this.proxyHandler);
|
||||
}
|
||||
|
||||
private notifyPrefixListeners(pathString: string, pathElements: string[], value: any) {
|
||||
for (let i = 1; i <= pathElements.length; i++) {
|
||||
const prefix = pathElements.slice(0, i).join(".");
|
||||
this.prefixListeners.get(prefix)?.forEach(cb => cb(value, pathString));
|
||||
}
|
||||
}
|
||||
|
||||
private notifyListeners(pathStr: string, value: any, root: T) {
|
||||
const paths = pathStr.split(".");
|
||||
|
||||
|
|
@ -172,6 +185,7 @@ export class SettingsStore<T extends object> {
|
|||
}
|
||||
|
||||
this.pathListeners.get(pathStr)?.forEach(cb => cb(value));
|
||||
this.notifyPrefixListeners(pathStr, paths, value);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -203,6 +217,7 @@ export class SettingsStore<T extends object> {
|
|||
}
|
||||
|
||||
this.pathListeners.get(pathToNotify)?.forEach(cb => cb(v));
|
||||
this.notifyPrefixListeners(pathToNotify, path, v);
|
||||
}
|
||||
|
||||
this.markAsChanged();
|
||||
|
|
@ -229,8 +244,6 @@ export class SettingsStore<T extends object> {
|
|||
* ```js
|
||||
* Setting.store.foo.baz = "hi"
|
||||
* ```
|
||||
* @param path
|
||||
* @param cb
|
||||
*/
|
||||
public addChangeListener<P extends LiteralUnion<keyof T, string>>(
|
||||
path: P,
|
||||
|
|
@ -241,6 +254,20 @@ export class SettingsStore<T extends object> {
|
|||
this.pathListeners.set(path as string, listeners);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a prefix change listener that will fire whenever a setting matching the specified prefix is changed.
|
||||
* For example if prefix is `"foo"`, the listener will fire on
|
||||
* ```js
|
||||
* Setting.store.foo.bar = "hi"
|
||||
* Setting.store.foo.baz = "hi"
|
||||
* ```
|
||||
*/
|
||||
public addPrefixChangeListener<P extends string>(prefix: P, cb: (data: ResolvePropDeep<T, P>, path: string) => void) {
|
||||
const listeners = this.prefixListeners.get(prefix) ?? new Set();
|
||||
listeners.add(cb);
|
||||
this.prefixListeners.set(prefix, listeners);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a global listener
|
||||
* @see {@link addGlobalChangeListener}
|
||||
|
|
@ -261,6 +288,18 @@ export class SettingsStore<T extends object> {
|
|||
if (!listeners.size) this.pathListeners.delete(path as string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a prefix listener
|
||||
* @see {@link addPrefixChangeListener}
|
||||
*/
|
||||
public removePrefixChangeListener(prefix: string, cb: (data: any, path: string) => void) {
|
||||
const listeners = this.prefixListeners.get(prefix);
|
||||
if (!listeners) return;
|
||||
|
||||
listeners.delete(cb);
|
||||
if (!listeners.size) this.prefixListeners.delete(prefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call all global change listeners
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -17,13 +17,13 @@
|
|||
*/
|
||||
|
||||
import type { ProfileBadge } from "@api/Badges";
|
||||
import type { ChatBarButtonFactory } from "@api/ChatButtons";
|
||||
import type { ChatBarButtonData, ChatBarButtonFactory } from "@api/ChatButtons";
|
||||
import type { NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||
import type { MemberListDecoratorFactory } from "@api/MemberListDecorators";
|
||||
import type { MessageAccessoryFactory } from "@api/MessageAccessories";
|
||||
import type { MessageDecorationFactory } from "@api/MessageDecorations";
|
||||
import type { MessageClickListener, MessageEditListener, MessageSendListener } from "@api/MessageEvents";
|
||||
import type { MessagePopoverButtonFactory } from "@api/MessagePopover";
|
||||
import type { MessagePopoverButtonData, MessagePopoverButtonFactory } from "@api/MessagePopover";
|
||||
import type { NicknameIconFactory } from "@api/NicknameIcons";
|
||||
import type { Command, FluxEvents } from "@vencord/discord-types";
|
||||
import type { ReactNode } from "react";
|
||||
|
|
@ -96,6 +96,8 @@ export interface Plugin extends PluginDef {
|
|||
isDependency?: boolean;
|
||||
}
|
||||
|
||||
export type IconComponent = (props: IconProps & Record<string, any>) => ReactNode;
|
||||
export type IconProps = { height?: number | string; width?: number | string; className?: string; };
|
||||
export interface PluginDef {
|
||||
name: string;
|
||||
description: string;
|
||||
|
|
@ -161,9 +163,16 @@ export interface PluginDef {
|
|||
contextMenus?: Record<string, NavContextMenuPatchCallback>;
|
||||
/**
|
||||
* Allows you to add custom actions to the Vencord Toolbox.
|
||||
* The key will be used as text for the button
|
||||
*
|
||||
* Can either be an object mapping labels to action functions or a Function returning Menu components.
|
||||
* Please note that you can only use Menu components.
|
||||
*
|
||||
* @example
|
||||
* toolboxActions: {
|
||||
* "Click Me": () => alert("Hi")
|
||||
* }
|
||||
*/
|
||||
toolboxActions?: Record<string, () => void>;
|
||||
toolboxActions?: Record<string, () => void> | (() => ReactNode);
|
||||
|
||||
tags?: string[];
|
||||
|
||||
|
|
@ -175,18 +184,28 @@ export interface PluginDef {
|
|||
userProfileBadge?: ProfileBadge;
|
||||
userProfileBadges?: ProfileBadge[];
|
||||
|
||||
messagePopoverButton?: MessagePopoverButtonData;
|
||||
chatBarButton?: ChatBarButtonData;
|
||||
|
||||
onMessageClick?: MessageClickListener;
|
||||
onBeforeMessageSend?: MessageSendListener;
|
||||
onBeforeMessageEdit?: MessageEditListener;
|
||||
|
||||
renderMessagePopoverButton?: MessagePopoverButtonFactory;
|
||||
renderMessageAccessory?: MessageAccessoryFactory;
|
||||
renderMessageDecoration?: MessageDecorationFactory;
|
||||
|
||||
renderMemberListDecorator?: MemberListDecoratorFactory;
|
||||
renderNicknameIcon?: NicknameIconFactory;
|
||||
|
||||
// TODO: Remove eventually
|
||||
/**
|
||||
* @deprecated Use {@link chatBarButton} instead
|
||||
*/
|
||||
renderChatBarButton?: ChatBarButtonFactory;
|
||||
/**
|
||||
* @deprecated Use {@link messagePopoverButton} instead
|
||||
*/
|
||||
renderMessagePopoverButton?: MessagePopoverButtonFactory;
|
||||
|
||||
/**
|
||||
* A Vencord plugin that is modified for extra features in Equicord
|
||||
|
|
@ -421,3 +440,5 @@ export type PluginNative<PluginExports extends Record<string, (event: Electron.I
|
|||
? (...args: Args) => Return extends Promise<any> ? Return : Promise<Return>
|
||||
: never;
|
||||
};
|
||||
|
||||
export type AllOrNothing<T> = T | { [K in keyof T]?: never; };
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue