Merge remote-tracking branch 'upstream/dev' into dev
This commit is contained in:
commit
334c39decc
47 changed files with 646 additions and 258 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "equicord",
|
"name": "equicord",
|
||||||
"private": "true",
|
"private": "true",
|
||||||
"version": "1.14.16.0",
|
"version": "1.15.0.0",
|
||||||
"description": "The other cutest Discord client mod",
|
"description": "The other cutest Discord client mod",
|
||||||
"homepage": "https://github.com/Equicord/Equicord#readme",
|
"homepage": "https://github.com/Equicord/Equicord#readme",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
|
|
@ -97,7 +97,7 @@
|
||||||
"typescript-transform-paths": "^3.5.6",
|
"typescript-transform-paths": "^3.5.6",
|
||||||
"zip-local": "^0.3.5"
|
"zip-local": "^0.3.5"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@11.13.0",
|
"packageManager": "pnpm@11.17.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22"
|
"node": ">=22"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ import { appendFile, mkdir, readdir, readFile, rm, writeFile } from "fs/promises
|
||||||
import path, { join } from "path";
|
import path, { join } from "path";
|
||||||
import Zip from "zip-local";
|
import Zip from "zip-local";
|
||||||
|
|
||||||
import { BUILD_TIMESTAMP, commonOpts, globPlugins, IS_DEV, IS_REPORTER, IS_COMPANION_TEST, IS_STANDALONE, VERSION, commonRendererPlugins, buildOrWatchAll, stringifyValues, IS_ANTI_CRASH_TEST } from "./common.mjs";
|
import { BUILD_TIMESTAMP, commonOpts, globPlugins, IS_DEV, IS_ANTI_CRASH_TEST, IS_REPORTER, IS_COMPANION_TEST, IS_STANDALONE, VERSION, commonRendererPlugins, buildOrWatchAll, stringifyValues } from "./common.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {import("esbuild").BuildOptions}
|
* @type {import("esbuild").BuildOptions}
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ export interface ProfileBadge {
|
||||||
/** Action to perform when you click the badge */
|
/** Action to perform when you click the badge */
|
||||||
onClick?(event: React.MouseEvent, props: ProfileBadge & BadgeUserArgs): void;
|
onClick?(event: React.MouseEvent, props: ProfileBadge & BadgeUserArgs): void;
|
||||||
/** Action to perform when you right click the badge */
|
/** Action to perform when you right click the badge */
|
||||||
onContextMenu?(event: React.MouseEvent, props: BadgeUserArgs & BadgeUserArgs): void;
|
onContextMenu?(event: React.MouseEvent, props: ProfileBadge & BadgeUserArgs): void;
|
||||||
/** Should the user display this badge? */
|
/** Should the user display this badge? */
|
||||||
shouldShow?(userInfo: BadgeUserArgs): boolean;
|
shouldShow?(userInfo: BadgeUserArgs): boolean;
|
||||||
/** Optional props (e.g. style) for the badge, ignored for component badges */
|
/** Optional props (e.g. style) for the badge, ignored for component badges */
|
||||||
|
|
|
||||||
|
|
@ -46,16 +46,14 @@ export let OptionalMessageOption: CommandOption = OptPlaceholder;
|
||||||
*/
|
*/
|
||||||
export let RequiredMessageOption: CommandOption = ReqPlaceholder;
|
export let RequiredMessageOption: CommandOption = ReqPlaceholder;
|
||||||
|
|
||||||
// Discord's command list has random gaps for some reason, which can cause issues while rendering the commands
|
let idCounter = 99;
|
||||||
// Add this offset to every added command to keep them unique
|
|
||||||
let commandIdOffset: number;
|
|
||||||
|
|
||||||
export const _init = function (cmds: VencordCommand[]) {
|
export const _init = function (cmds: VencordCommand[]) {
|
||||||
try {
|
try {
|
||||||
BUILT_IN = cmds;
|
BUILT_IN = cmds;
|
||||||
OptionalMessageOption = cmds.find(c => (c.untranslatedName || c.displayName) === "shrug")!.options![0];
|
OptionalMessageOption = cmds.find(c => (c.untranslatedName || c.displayName) === "shrug")!.options![0];
|
||||||
RequiredMessageOption = cmds.find(c => (c.untranslatedName || c.displayName) === "me")!.options![0];
|
RequiredMessageOption = cmds.find(c => (c.untranslatedName || c.displayName) === "me")!.options![0];
|
||||||
commandIdOffset = Math.abs(BUILT_IN.map(x => Number(x.id)).sort((x, y) => x - y)[0]) - BUILT_IN.length;
|
idCounter = Math.abs(BUILT_IN.map(x => Number(x.id)).sort((x, y) => x - y)[0]) + 1;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
new Logger("CommandsAPI").error("Failed to load CommandsApi", e, " - cmds is", cmds);
|
new Logger("CommandsAPI").error("Failed to load CommandsApi", e, " - cmds is", cmds);
|
||||||
}
|
}
|
||||||
|
|
@ -106,21 +104,23 @@ export function prepareOption<O extends CommandOption | VencordCommand>(opt: O):
|
||||||
return opt;
|
return opt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isSubCommandParent = (cmd: VencordCommand) => cmd.options?.[0]?.type === ApplicationCommandOptionType.SUB_COMMAND;
|
||||||
|
const getSubCommandName = (cmd: VencordCommand, option: CommandOption) => `${cmd.name} ${option.name}`;
|
||||||
|
|
||||||
// Yes, Discord registers individual commands for each subcommand
|
// Yes, Discord registers individual commands for each subcommand
|
||||||
// TODO: This probably doesn't support nested subcommands. If that is ever needed,
|
|
||||||
// investigate
|
|
||||||
function registerSubCommands(cmd: VencordCommand, plugin: string) {
|
function registerSubCommands(cmd: VencordCommand, plugin: string) {
|
||||||
cmd.options?.forEach(o => {
|
cmd.options?.forEach(o => {
|
||||||
if (o.type !== ApplicationCommandOptionType.SUB_COMMAND)
|
if (o.type !== ApplicationCommandOptionType.SUB_COMMAND)
|
||||||
throw new Error("When specifying sub-command options, all options must be sub-commands.");
|
throw new Error("When specifying sub-command options, all options must be sub-commands.");
|
||||||
|
|
||||||
const subCmd = {
|
const subCmd = {
|
||||||
...cmd,
|
...cmd,
|
||||||
...o,
|
...o,
|
||||||
options: o.options !== undefined ? o.options : undefined,
|
options: o.options !== undefined ? o.options : undefined,
|
||||||
type: ApplicationCommandType.CHAT_INPUT,
|
type: ApplicationCommandType.CHAT_INPUT,
|
||||||
name: `${cmd.name} ${o.name}`,
|
|
||||||
id: `${o.name}-${cmd.id}`,
|
id: `${o.name}-${cmd.id}`,
|
||||||
displayName: `${cmd.name} ${o.name}`,
|
name: getSubCommandName(cmd, o),
|
||||||
|
displayName: getSubCommandName(cmd, o),
|
||||||
subCommandPath: [{
|
subCommandPath: [{
|
||||||
name: o.name,
|
name: o.name,
|
||||||
type: o.type,
|
type: o.type,
|
||||||
|
|
@ -128,10 +128,18 @@ function registerSubCommands(cmd: VencordCommand, plugin: string) {
|
||||||
}],
|
}],
|
||||||
rootCommand: cmd
|
rootCommand: cmd
|
||||||
};
|
};
|
||||||
registerCommand(subCmd as any, plugin);
|
registerCommand(subCmd, plugin);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function unregisterSubCommands(cmd: VencordCommand): boolean {
|
||||||
|
const results = BUILT_IN
|
||||||
|
.filter(c => c.rootCommand === cmd)
|
||||||
|
.map(c => unregisterCommand(c.name));
|
||||||
|
|
||||||
|
return results.length > 0 && results.every(x => x);
|
||||||
|
}
|
||||||
|
|
||||||
export function registerCommand<C extends VencordCommand>(command: C, plugin: string) {
|
export function registerCommand<C extends VencordCommand>(command: C, plugin: string) {
|
||||||
if (!BUILT_IN) {
|
if (!BUILT_IN) {
|
||||||
console.warn(
|
console.warn(
|
||||||
|
|
@ -148,24 +156,30 @@ export function registerCommand<C extends VencordCommand>(command: C, plugin: st
|
||||||
command.isVencordCommand = true;
|
command.isVencordCommand = true;
|
||||||
command.untranslatedName ??= command.name;
|
command.untranslatedName ??= command.name;
|
||||||
command.untranslatedDescription ??= command.description;
|
command.untranslatedDescription ??= command.description;
|
||||||
command.id ??= `-${BUILT_IN.length + commandIdOffset + 1}`;
|
command.id ??= `-${idCounter++}`;
|
||||||
command.applicationId ??= "-1"; // BUILT_IN;
|
command.applicationId ??= "-1"; // BUILT_IN;
|
||||||
command.type ??= ApplicationCommandType.CHAT_INPUT;
|
command.type ??= ApplicationCommandType.CHAT_INPUT;
|
||||||
command.inputType ??= ApplicationCommandInputType.BUILT_IN_TEXT;
|
command.inputType ??= ApplicationCommandInputType.BUILT_IN_TEXT;
|
||||||
command.plugin ||= plugin;
|
command.plugin ||= plugin;
|
||||||
|
|
||||||
prepareOption(command);
|
prepareOption(command);
|
||||||
|
commands[command.name] = command;
|
||||||
|
|
||||||
if (command.options?.[0]?.type === ApplicationCommandOptionType.SUB_COMMAND) {
|
if (isSubCommandParent(command)) {
|
||||||
registerSubCommands(command, plugin);
|
registerSubCommands(command, plugin);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
commands[command.name] = command;
|
|
||||||
BUILT_IN.push(command);
|
BUILT_IN.push(command);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function unregisterCommand(name: string) {
|
export function unregisterCommand(name: string, isSubCommands = false) {
|
||||||
|
const cmd = commands[name];
|
||||||
|
if (cmd && isSubCommandParent(cmd)) {
|
||||||
|
delete commands[name];
|
||||||
|
return unregisterSubCommands(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
const idx = BUILT_IN.findIndex(c => c.name === name);
|
const idx = BUILT_IN.findIndex(c => c.name === name);
|
||||||
if (idx === -1)
|
if (idx === -1)
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -9,4 +9,5 @@ export { ApplicationCommandInputType, ApplicationCommandOptionType, ApplicationC
|
||||||
|
|
||||||
export interface VencordCommand extends Command {
|
export interface VencordCommand extends Command {
|
||||||
isVencordCommand?: boolean;
|
isVencordCommand?: boolean;
|
||||||
|
rootCommand?: VencordCommand;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -75,14 +75,21 @@ export async function persistNotification(notification: NotificationData) {
|
||||||
signals.forEach(x => x());
|
signals.forEach(x => x());
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteNotification(timestamp: number) {
|
export async function deleteNotification(id: string) {
|
||||||
const log = await getLog();
|
let found = false;
|
||||||
const index = log.findIndex(x => x.timestamp === timestamp);
|
|
||||||
if (index === -1) return;
|
|
||||||
|
|
||||||
log.splice(index, 1);
|
await DataStore.update(KEY, (old: PersistentNotificationData[] | undefined) => {
|
||||||
await DataStore.set(KEY, log);
|
const log = old ?? [];
|
||||||
signals.forEach(x => x());
|
const index = log.findIndex(x => x.id === id);
|
||||||
|
if (index === -1) return log;
|
||||||
|
|
||||||
|
log.splice(index, 1);
|
||||||
|
found = true;
|
||||||
|
return log;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (found)
|
||||||
|
signals.forEach(x => x());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useLogs() {
|
export function useLogs() {
|
||||||
|
|
@ -114,7 +121,7 @@ function NotificationEntry({ data }: { data: PersistentNotificationData; }) {
|
||||||
if (removing) return;
|
if (removing) return;
|
||||||
setRemoving(true);
|
setRemoving(true);
|
||||||
|
|
||||||
setTimeout(() => deleteNotification(data.timestamp), 200);
|
setTimeout(() => deleteNotification(data.id), 200);
|
||||||
}}
|
}}
|
||||||
richBody={
|
richBody={
|
||||||
<div className={cl("body-wrapper")}>
|
<div className={cl("body-wrapper")}>
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@ export interface Settings {
|
||||||
plugins: {
|
plugins: {
|
||||||
[plugin: string]: {
|
[plugin: string]: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
isFavorite?: boolean;
|
||||||
[setting: string]: any;
|
[setting: string]: any;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
|
|
||||||
import { Settings, SettingsStore, type ThemeActivationMode } from "@api/Settings";
|
import { Settings, SettingsStore, type ThemeActivationMode } from "@api/Settings";
|
||||||
import { createAndAppendStyle } from "@utils/css";
|
import { createAndAppendStyle } from "@utils/css";
|
||||||
|
import { isNonNullish } from "@utils/guards";
|
||||||
import { ThemeStore } from "@vencord/discord-types";
|
import { ThemeStore } from "@vencord/discord-types";
|
||||||
import { PopoutWindowStore } from "@webpack/common";
|
import { PopoutWindowStore } from "@webpack/common";
|
||||||
|
|
||||||
|
|
@ -52,6 +53,9 @@ async function toggle(isEnabled: boolean) {
|
||||||
style.disabled = !isEnabled;
|
style.disabled = !isEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// for cleanup
|
||||||
|
let previousThemeBlobObjectURLs = [] as string[];
|
||||||
|
|
||||||
async function initThemes() {
|
async function initThemes() {
|
||||||
themesStyle ??= createAndAppendStyle("vencord-themes", userStyleRootNode);
|
themesStyle ??= createAndAppendStyle("vencord-themes", userStyleRootNode);
|
||||||
|
|
||||||
|
|
@ -78,15 +82,22 @@ async function initThemes() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IS_WEB) {
|
if (IS_WEB) {
|
||||||
for (const theme of enabledThemes) {
|
previousThemeBlobObjectURLs.forEach(url => URL.revokeObjectURL(url));
|
||||||
const mode = getThemeActivationMode(theme);
|
|
||||||
if (!shouldApplyTheme(mode, activeTheme)) continue;
|
|
||||||
|
|
||||||
|
const themesToApply = enabledThemes.filter(theme =>
|
||||||
|
shouldApplyTheme(getThemeActivationMode(theme), activeTheme)
|
||||||
|
);
|
||||||
|
|
||||||
|
const objectUrls = await Promise.all(themesToApply.map(async theme => {
|
||||||
const themeData = await VencordNative.themes.getThemeData(theme);
|
const themeData = await VencordNative.themes.getThemeData(theme);
|
||||||
if (!themeData) continue;
|
if (!themeData) return null;
|
||||||
|
|
||||||
const blob = new Blob([themeData], { type: "text/css" });
|
const blob = new Blob([themeData], { type: "text/css" });
|
||||||
links.add(URL.createObjectURL(blob));
|
return URL.createObjectURL(blob);
|
||||||
}
|
}));
|
||||||
|
|
||||||
|
previousThemeBlobObjectURLs = objectUrls.filter(isNonNullish);
|
||||||
|
previousThemeBlobObjectURLs.forEach(url => links.add(url));
|
||||||
} else {
|
} else {
|
||||||
const version = Date.now();
|
const version = Date.now();
|
||||||
for (const theme of enabledThemes) {
|
for (const theme of enabledThemes) {
|
||||||
|
|
|
||||||
|
|
@ -1224,3 +1224,31 @@ export function DoubleCheckmarkIcon(props: IconProps) {
|
||||||
</Icon>
|
</Icon>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function StarFilled(props: IconProps) {
|
||||||
|
return (
|
||||||
|
<Icon
|
||||||
|
{...props}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill={props.fill || "currentColor"}
|
||||||
|
d="M10.81 2.86c.38-1.15 2-1.15 2.38 0l1.89 5.83h6.12c1.2 0 1.71 1.54.73 2.25l-4.95 3.6 1.9 5.82a1.25 1.25 0 0 1-1.93 1.4L12 18.16l-4.95 3.6c-.98.7-2.3-.25-1.92-1.4l1.89-5.82-4.95-3.6a1.25 1.25 0 0 1 .73-2.25h6.12l1.9-5.83Z"
|
||||||
|
/>
|
||||||
|
</Icon>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StarOutlined(props: IconProps) {
|
||||||
|
return (
|
||||||
|
<Icon
|
||||||
|
{...props}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill={props.fill || "currentColor"}
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M2.07 10.94a1.25 1.25 0 0 1 .73-2.25h6.12l1.9-5.83c.37-1.15 2-1.15 2.37 0l1.89 5.83h6.12c1.2 0 1.71 1.54.73 2.25l-4.95 3.6 1.9 5.82a1.25 1.25 0 0 1-1.93 1.4L12 18.16l-4.95 3.6c-.98.7-2.3-.25-1.92-1.4l1.89-5.82-4.95-3.6Zm11.55-.25h5.26l-4.25 3.09 1.62 5-4.25-3.1-4.25 3.1 1.62-5-4.25-3.1h5.26l1.62-5 1.62 5Z" />
|
||||||
|
</Icon>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ export function Link({
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: PropsWithChildren<LinkProps>) {
|
}: PropsWithChildren<LinkProps>) {
|
||||||
|
|
||||||
const isInternal = href && /^(?:discord:\/)?\/[a-zA-Z0-9_-]/.test(href);
|
const isInternal = href && /^(?:discord:\/)?\/[a-zA-Z0-9_-]/.test(href);
|
||||||
const needsSafeAttrs = href && !isInternal;
|
const needsSafeAttrs = href && !isInternal;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,14 @@ export interface QuickActionProps {
|
||||||
text: ReactNode;
|
text: ReactNode;
|
||||||
action?: () => void;
|
action?: () => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
style?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function QuickAction(props: QuickActionProps) {
|
export function QuickAction(props: QuickActionProps) {
|
||||||
const { Icon, action, text, disabled } = props;
|
const { Icon, action, text, disabled, style } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button className={cl("pill")} onClick={action} disabled={disabled}>
|
<button className={cl("pill")} onClick={action} disabled={disabled} style={style}>
|
||||||
<Icon className={cl("img")} />
|
<Icon className={cl("img")} />
|
||||||
{text}
|
{text}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,8 @@ import { Modal, openModal, showToast, useEffect, useMemo, UserProfileStore, useS
|
||||||
|
|
||||||
import Plugins, { PluginMeta } from "~plugins";
|
import Plugins, { PluginMeta } from "~plugins";
|
||||||
|
|
||||||
import { GithubButton, WebsiteButton } from "./LinkIconButton";
|
|
||||||
import { PluginCard } from "./PluginCard";
|
import { PluginCard } from "./PluginCard";
|
||||||
|
import { GithubButton, WebsiteButton } from "./PluginModalButtons";
|
||||||
|
|
||||||
const cl = classNameFactory("vc-author-modal-");
|
const cl = classNameFactory("vc-author-modal-");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
/*
|
|
||||||
* Vencord, a Discord client mod
|
|
||||||
* Copyright (c) 2024 Vendicated and contributors
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Button } from "@components/Button";
|
|
||||||
import { GithubIcon, WebsiteIcon } from "@components/Icons";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
text: string;
|
|
||||||
href: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function WebsiteButton({ text, href }: Props) {
|
|
||||||
return (
|
|
||||||
<Button variant="secondary" size="small" style={{ gap: 4 }} onClick={() => VencordNative.native.openExternal(href)}>
|
|
||||||
<WebsiteIcon width={16} height={16} />
|
|
||||||
{text}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function GithubButton({ text, href }: Props) {
|
|
||||||
return (
|
|
||||||
<Button variant="secondary" size="small" style={{ gap: 4 }} onClick={() => VencordNative.native.openExternal(href)}>
|
|
||||||
<GithubIcon width={16} height={16} />
|
|
||||||
{text}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -42,7 +42,7 @@ import { PluginMeta } from "~plugins";
|
||||||
|
|
||||||
import { OptionComponentMap } from "./components";
|
import { OptionComponentMap } from "./components";
|
||||||
import { openContributorModal } from "./ContributorModal";
|
import { openContributorModal } from "./ContributorModal";
|
||||||
import { GithubButton, WebsiteButton } from "./LinkIconButton";
|
import { FavoriteButton, GithubButton, WebsiteButton } from "./PluginModalButtons";
|
||||||
|
|
||||||
const cl = classNameFactory("vc-plugin-modal-");
|
const cl = classNameFactory("vc-plugin-modal-");
|
||||||
|
|
||||||
|
|
@ -258,6 +258,10 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
||||||
) : <div />}
|
) : <div />}
|
||||||
{!pluginMeta.userPlugin && (
|
{!pluginMeta.userPlugin && (
|
||||||
<div className={cl("links")}>
|
<div className={cl("links")}>
|
||||||
|
<FavoriteButton
|
||||||
|
isFavorite={pluginSettings.isFavorite ?? false}
|
||||||
|
onClick={() => pluginSettings.isFavorite = !pluginSettings.isFavorite}
|
||||||
|
/>
|
||||||
<WebsiteButton
|
<WebsiteButton
|
||||||
text="Website"
|
text="Website"
|
||||||
href={isEquicordPlugin ? `https://equicord.org/plugins/${plugin.name}` : `https://vencord.dev/plugins/${plugin.name}`}
|
href={isEquicordPlugin ? `https://equicord.org/plugins/${plugin.name}` : `https://vencord.dev/plugins/${plugin.name}`}
|
||||||
|
|
|
||||||
37
src/components/settings/tabs/plugins/PluginModalButtons.css
Normal file
37
src/components/settings/tabs/plugins/PluginModalButtons.css
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
.vc-settings-modal-light {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-settings-modal-dark {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.vc-settings-modal-favorite-button {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-settings-modal-favorite-button-active,
|
||||||
|
.vc-settings-modal-favorite-button:hover {
|
||||||
|
color: var(--yellow-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-settings-modal-link-icon {
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 4px solid var(--background-base-lowest);
|
||||||
|
box-sizing: border-box
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-settings-modal-favorite-icon {
|
||||||
|
background: var(--background-base-lowest);
|
||||||
|
padding: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-settings-modal-links {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.2em;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
53
src/components/settings/tabs/plugins/PluginModalButtons.tsx
Normal file
53
src/components/settings/tabs/plugins/PluginModalButtons.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "./PluginModalButtons.css";
|
||||||
|
|
||||||
|
import { GithubIcon, StarFilled, StarOutlined, WebsiteIcon } from "@components/Icons";
|
||||||
|
import { classNameFactory } from "@utils/css";
|
||||||
|
import { getTheme, Theme } from "@utils/discord";
|
||||||
|
import { Clickable, MaskedLink, Tooltip } from "@webpack/common";
|
||||||
|
|
||||||
|
const cl = classNameFactory("vc-settings-modal-");
|
||||||
|
|
||||||
|
const getThemeClass = () => getTheme() !== Theme.Light ? "dark" : "light";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
text: string;
|
||||||
|
href: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function LinkIcon({ text, href, Icon }: Props & { Icon: React.ComponentType; }) {
|
||||||
|
return (
|
||||||
|
<Tooltip text={text}>
|
||||||
|
{props =>
|
||||||
|
<MaskedLink {...props} href={href} className={cl("link-button", getThemeClass())}>
|
||||||
|
<Icon />
|
||||||
|
</MaskedLink>
|
||||||
|
}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const GithubLinkIcon = () => <GithubIcon aria-hidden className={cl("link-icon")} />;
|
||||||
|
const WebsiteLinkIcon = () => <WebsiteIcon aria-hidden className={cl("link-icon")} />;
|
||||||
|
|
||||||
|
export const GithubButton = (props: Props) => <LinkIcon {...props} Icon={GithubLinkIcon} />;
|
||||||
|
export const WebsiteButton = (props: Props) => <LinkIcon {...props} Icon={WebsiteLinkIcon} />;
|
||||||
|
|
||||||
|
export function FavoriteButton({ isFavorite, onClick }: { isFavorite: boolean; onClick: () => void; }) {
|
||||||
|
const Icon = isFavorite ? StarFilled : StarOutlined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip text={isFavorite ? "Unfavorite plugin" : "Favorite plugin - pins it to the top of the plugin list"}>
|
||||||
|
{props =>
|
||||||
|
<Clickable {...props} onClick={onClick} className={cl("favorite-button", isFavorite && "favorite-button-active", getThemeClass())}>
|
||||||
|
<Icon aria-hidden className={cl("link-icon", "favorite-icon")} />
|
||||||
|
</Clickable>
|
||||||
|
}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -99,6 +99,7 @@ function ReloadRequiredCard({ required, enabledPlugins, openWarningModal, resetC
|
||||||
|
|
||||||
const enum SearchStatus {
|
const enum SearchStatus {
|
||||||
ALL,
|
ALL,
|
||||||
|
FAVORITES,
|
||||||
ENABLED,
|
ENABLED,
|
||||||
DISABLED,
|
DISABLED,
|
||||||
EQUICORD,
|
EQUICORD,
|
||||||
|
|
@ -170,10 +171,10 @@ export default function PluginSettings() {
|
||||||
<p>The following plugins require a restart:</p>
|
<p>The following plugins require a restart:</p>
|
||||||
<div>
|
<div>
|
||||||
{displayed.map((s, i) => (
|
{displayed.map((s, i) => (
|
||||||
<span key={i}>
|
<React.Fragment key={i}>
|
||||||
{i > 0 && ", "}
|
{i > 0 && ", "}
|
||||||
{Parser.parse("`" + s + "`")}
|
{Parser.parse("`" + s + "`")}
|
||||||
</span>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
{remainingCount > 0 && <span> and {remainingCount} more</span>}
|
{remainingCount > 0 && <span> and {remainingCount} more</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -197,8 +198,11 @@ export default function PluginSettings() {
|
||||||
return o;
|
return o;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const sortedPlugins = useMemo(() => Object.values(Plugins)
|
const sortedPlugins = useMemo(() =>
|
||||||
.sort((a, b) => a.name.localeCompare(b.name)), []);
|
Object.values(Plugins).sort((a, b) => a.name.localeCompare(b.name)),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
.toSorted((a, b) => Number(settings.plugins[b.name]?.isFavorite ?? false) - Number(settings.plugins[a.name]?.isFavorite ?? false));
|
||||||
|
|
||||||
const hasUserPlugins = useMemo(() => !IS_STANDALONE && Object.values(PluginMeta).some(m => m.userPlugin), []);
|
const hasUserPlugins = useMemo(() => !IS_STANDALONE && Object.values(PluginMeta).some(m => m.userPlugin), []);
|
||||||
|
|
||||||
|
|
@ -211,6 +215,9 @@ export default function PluginSettings() {
|
||||||
const { status, tags } = searchValue;
|
const { status, tags } = searchValue;
|
||||||
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
|
case SearchStatus.FAVORITES:
|
||||||
|
if (!settings.plugins[plugin.name]?.isFavorite) return false;
|
||||||
|
break;
|
||||||
case SearchStatus.DISABLED:
|
case SearchStatus.DISABLED:
|
||||||
if (isPluginEnabled(plugin.name)) return false;
|
if (isPluginEnabled(plugin.name)) return false;
|
||||||
break;
|
break;
|
||||||
|
|
@ -419,6 +426,7 @@ export default function PluginSettings() {
|
||||||
<Select
|
<Select
|
||||||
options={[
|
options={[
|
||||||
{ label: "Show All", value: SearchStatus.ALL, default: true },
|
{ label: "Show All", value: SearchStatus.ALL, default: true },
|
||||||
|
{ label: "Show Favorites", value: SearchStatus.FAVORITES },
|
||||||
{ label: "Show Enabled", value: SearchStatus.ENABLED },
|
{ label: "Show Enabled", value: SearchStatus.ENABLED },
|
||||||
{ label: "Show Disabled", value: SearchStatus.DISABLED },
|
{ label: "Show Disabled", value: SearchStatus.DISABLED },
|
||||||
{ label: "Show Equicord", value: SearchStatus.EQUICORD },
|
{ label: "Show Equicord", value: SearchStatus.EQUICORD },
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import { debounce } from "@shared/debounce";
|
||||||
import { classNameFactory } from "@utils/css";
|
import { classNameFactory } from "@utils/css";
|
||||||
import { copyWithToast, openImageModal } from "@utils/discord";
|
import { copyWithToast, openImageModal } from "@utils/discord";
|
||||||
import { classes } from "@utils/misc";
|
import { classes } from "@utils/misc";
|
||||||
|
import { formatDuration } from "@utils/text";
|
||||||
import { ContextMenuApi, FluxDispatcher, Menu, React, useEffect, useState, useStateFromStores } from "@webpack/common";
|
import { ContextMenuApi, FluxDispatcher, Menu, React, useEffect, useState, useStateFromStores } from "@webpack/common";
|
||||||
|
|
||||||
import { settings } from "../settings";
|
import { settings } from "../settings";
|
||||||
|
|
@ -34,13 +35,6 @@ import { SpotifyStore, Track } from "./SpotifyStore";
|
||||||
|
|
||||||
const cl = classNameFactory("vc-spotify-");
|
const cl = classNameFactory("vc-spotify-");
|
||||||
|
|
||||||
function msToHuman(ms: number) {
|
|
||||||
const minutes = ms / 1000 / 60;
|
|
||||||
const m = Math.floor(minutes);
|
|
||||||
const s = Math.floor((minutes - m) * 60);
|
|
||||||
return `${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Svg(path: string, label: string) {
|
function Svg(path: string, label: string) {
|
||||||
return () => (
|
return () => (
|
||||||
<svg
|
<svg
|
||||||
|
|
@ -193,7 +187,7 @@ function SpotifySeekBar() {
|
||||||
className={cl("progress-time") + " " + cl("time-left")}
|
className={cl("progress-time") + " " + cl("time-left")}
|
||||||
aria-label="Progress"
|
aria-label="Progress"
|
||||||
>
|
>
|
||||||
{msToHuman(position)}
|
{formatDuration(position)}
|
||||||
</Span>
|
</Span>
|
||||||
<SeekBar
|
<SeekBar
|
||||||
initialValue={position}
|
initialValue={position}
|
||||||
|
|
@ -201,7 +195,7 @@ function SpotifySeekBar() {
|
||||||
maxValue={duration}
|
maxValue={duration}
|
||||||
onValueChange={onChange}
|
onValueChange={onChange}
|
||||||
asValueChanges={onChange}
|
asValueChanges={onChange}
|
||||||
onValueRender={msToHuman}
|
onValueRender={formatDuration}
|
||||||
/>
|
/>
|
||||||
<Span
|
<Span
|
||||||
size="xs"
|
size="xs"
|
||||||
|
|
@ -209,7 +203,7 @@ function SpotifySeekBar() {
|
||||||
className={cl("progress-time") + " " + cl("time-right")}
|
className={cl("progress-time") + " " + cl("time-right")}
|
||||||
aria-label="Total Duration"
|
aria-label="Total Duration"
|
||||||
>
|
>
|
||||||
{msToHuman(duration)}
|
{formatDuration(duration)}
|
||||||
</Span>
|
</Span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -16,14 +16,16 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import "./ipcMain";
|
||||||
|
|
||||||
import { app, net, protocol } from "electron";
|
import { app, net, protocol } from "electron";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import { pathToFileURL } from "url";
|
import { pathToFileURL } from "url";
|
||||||
|
|
||||||
import { initCsp } from "./csp";
|
import { initCsp } from "./csp";
|
||||||
import { ensureSafePath } from "./ipcMain";
|
|
||||||
import { RendererSettings } from "./settings";
|
import { RendererSettings } from "./settings";
|
||||||
import { IS_VANILLA, THEMES_DIR } from "./utils/constants";
|
import { IS_VANILLA, THEMES_DIR } from "./utils/constants";
|
||||||
|
import { ensureSafePath } from "./utils/ensureSafePath";
|
||||||
import { installExt } from "./utils/extensions";
|
import { installExt } from "./utils/extensions";
|
||||||
|
|
||||||
if (!IS_VANILLA && !IS_EXTENSION) {
|
if (!IS_VANILLA && !IS_EXTENSION) {
|
||||||
|
|
|
||||||
|
|
@ -27,11 +27,12 @@ import monacoHtml from "file://monacoWin.html?minify&base64";
|
||||||
import { FSWatcher, mkdirSync, readFileSync, watch, writeFileSync } from "fs";
|
import { FSWatcher, mkdirSync, readFileSync, watch, writeFileSync } from "fs";
|
||||||
import { open, readdir, readFile, unlink } from "fs/promises";
|
import { open, readdir, readFile, unlink } from "fs/promises";
|
||||||
import { release } from "os";
|
import { release } from "os";
|
||||||
import { join, normalize } from "path";
|
import { join } from "path";
|
||||||
|
|
||||||
import { registerCspIpcHandlers } from "./csp/manager";
|
import { registerCspIpcHandlers } from "./csp/manager";
|
||||||
import { getThemeInfo, stripBOM, UserThemeHeader } from "./themes";
|
import { getThemeInfo, stripBOM, UserThemeHeader } from "./themes";
|
||||||
import { ALLOWED_PROTOCOLS, QUICK_CSS_PATH, SETTINGS_DIR, THEMES_DIR } from "./utils/constants";
|
import { ALLOWED_PROTOCOLS, QUICK_CSS_PATH, SETTINGS_DIR, THEMES_DIR } from "./utils/constants";
|
||||||
|
import { ensureSafePath } from "./utils/ensureSafePath";
|
||||||
import { makeLinksOpenExternally } from "./utils/externalLinks";
|
import { makeLinksOpenExternally } from "./utils/externalLinks";
|
||||||
|
|
||||||
const RENDERER_CSS_PATH = join(__dirname, "renderer.css");
|
const RENDERER_CSS_PATH = join(__dirname, "renderer.css");
|
||||||
|
|
@ -40,13 +41,6 @@ mkdirSync(THEMES_DIR, { recursive: true });
|
||||||
|
|
||||||
registerCspIpcHandlers();
|
registerCspIpcHandlers();
|
||||||
|
|
||||||
export function ensureSafePath(basePath: string, path: string) {
|
|
||||||
const normalizedBasePath = normalize(basePath + "/");
|
|
||||||
const newPath = join(basePath, path);
|
|
||||||
const normalizedPath = normalize(newPath);
|
|
||||||
return normalizedPath.startsWith(normalizedBasePath) ? normalizedPath : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function readCss() {
|
function readCss() {
|
||||||
return readFile(QUICK_CSS_PATH, "utf-8").catch(() => "");
|
return readFile(QUICK_CSS_PATH, "utf-8").catch(() => "");
|
||||||
}
|
}
|
||||||
|
|
@ -86,7 +80,8 @@ ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => {
|
||||||
if (!ALLOWED_PROTOCOLS.includes(protocol))
|
if (!ALLOWED_PROTOCOLS.includes(protocol))
|
||||||
throw "Disallowed protocol.";
|
throw "Disallowed protocol.";
|
||||||
|
|
||||||
shell.openExternal(url);
|
shell.openExternal(url)
|
||||||
|
.catch(err => console.error("[Vencord] Failed to open external link", url, err));
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle(IpcEvents.GET_QUICK_CSS, () => readCss());
|
ipcMain.handle(IpcEvents.GET_QUICK_CSS, () => readCss());
|
||||||
|
|
@ -116,7 +111,11 @@ ipcMain.handle(IpcEvents.GET_THEME_SYSTEM_VALUES, () => {
|
||||||
ipcMain.handle(IpcEvents.OPEN_THEMES_FOLDER, () => shell.openPath(THEMES_DIR));
|
ipcMain.handle(IpcEvents.OPEN_THEMES_FOLDER, () => shell.openPath(THEMES_DIR));
|
||||||
ipcMain.handle(IpcEvents.OPEN_SETTINGS_FOLDER, () => shell.openPath(SETTINGS_DIR));
|
ipcMain.handle(IpcEvents.OPEN_SETTINGS_FOLDER, () => shell.openPath(SETTINGS_DIR));
|
||||||
|
|
||||||
|
let fsWatchers = [] as FSWatcher[];
|
||||||
|
|
||||||
ipcMain.handle(IpcEvents.INIT_FILE_WATCHERS, ({ sender }) => {
|
ipcMain.handle(IpcEvents.INIT_FILE_WATCHERS, ({ sender }) => {
|
||||||
|
fsWatchers.forEach(w => w.close());
|
||||||
|
|
||||||
let quickCssWatcher: FSWatcher | undefined;
|
let quickCssWatcher: FSWatcher | undefined;
|
||||||
let rendererCssWatcher: FSWatcher | undefined;
|
let rendererCssWatcher: FSWatcher | undefined;
|
||||||
|
|
||||||
|
|
@ -137,10 +136,13 @@ ipcMain.handle(IpcEvents.INIT_FILE_WATCHERS, ({ sender }) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fsWatchers = [quickCssWatcher, themesWatcher, rendererCssWatcher].filter(Boolean) as FSWatcher[];
|
||||||
|
|
||||||
sender.once("destroyed", () => {
|
sender.once("destroyed", () => {
|
||||||
quickCssWatcher?.close();
|
quickCssWatcher?.close();
|
||||||
themesWatcher.close();
|
themesWatcher.close();
|
||||||
rendererCssWatcher?.close();
|
rendererCssWatcher?.close();
|
||||||
|
fsWatchers = [];
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
16
src/main/utils/ensureSafePath.ts
Normal file
16
src/main/utils/ensureSafePath.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2026 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { join, normalize } from "path";
|
||||||
|
|
||||||
|
export function ensureSafePath(basePath: string, path: string) {
|
||||||
|
const normalizedBasePath = normalize(basePath + "/");
|
||||||
|
const newPath = join(basePath, path);
|
||||||
|
const normalizedPath = normalize(newPath);
|
||||||
|
return normalizedPath === normalize(basePath) || normalizedPath.startsWith(normalizedBasePath)
|
||||||
|
? normalizedPath
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
@ -24,6 +24,7 @@ import { join } from "path";
|
||||||
|
|
||||||
import { DATA_DIR } from "./constants";
|
import { DATA_DIR } from "./constants";
|
||||||
import { crxToZip } from "./crxToZip";
|
import { crxToZip } from "./crxToZip";
|
||||||
|
import { ensureSafePath } from "./ensureSafePath";
|
||||||
import { fetchBuffer } from "./http";
|
import { fetchBuffer } from "./http";
|
||||||
|
|
||||||
const extensionCacheDir = join(DATA_DIR, "ExtensionCache");
|
const extensionCacheDir = join(DATA_DIR, "ExtensionCache");
|
||||||
|
|
@ -33,24 +34,35 @@ async function extract(data: Buffer, outDir: string) {
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
unzip(data, (err, files) => {
|
unzip(data, (err, files) => {
|
||||||
if (err) return void reject(err);
|
if (err) return void reject(err);
|
||||||
|
|
||||||
Promise.all(Object.keys(files).map(async f => {
|
Promise.all(Object.keys(files).map(async f => {
|
||||||
// Signature stuff
|
// Signature stuff
|
||||||
// 'Cannot load extension with file or directory name
|
// 'Cannot load extension with file or directory name
|
||||||
// _metadata. Filenames starting with "_" are reserved for use by the system.';
|
// _metadata. Filenames starting with "_" are reserved for use by the system.';
|
||||||
if (f.startsWith("_metadata/")) return;
|
if (f.startsWith("_metadata/")) return;
|
||||||
|
|
||||||
if (f.endsWith("/")) return void mkdir(join(outDir, f), { recursive: true });
|
if (f.includes("\0")) throw new Error(`Invalid filename: "${f}"`);
|
||||||
|
|
||||||
|
if (f.endsWith("/")) {
|
||||||
|
const dir = ensureSafePath(outDir, f);
|
||||||
|
if (!dir) throw new Error(`Path traversal detected: "${f}"`);
|
||||||
|
return void await mkdir(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
const pathElements = f.split("/");
|
const pathElements = f.split("/");
|
||||||
const name = pathElements.pop()!;
|
const directories = pathElements.slice(0, -1).join("/");
|
||||||
const directories = pathElements.join("/");
|
|
||||||
const dir = join(outDir, directories);
|
const dir = ensureSafePath(outDir, directories);
|
||||||
|
if (!dir) throw new Error(`Path traversal detected: "${f}"`);
|
||||||
|
|
||||||
|
const filePath = ensureSafePath(outDir, f);
|
||||||
|
if (!filePath) throw new Error(`Path traversal detected: "${f}"`);
|
||||||
|
|
||||||
if (directories) {
|
if (directories) {
|
||||||
await mkdir(dir, { recursive: true });
|
await mkdir(dir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
await writeFile(join(dir, name), files[f]);
|
await writeFile(filePath, files[f]);
|
||||||
}))
|
}))
|
||||||
.then(() => resolve())
|
.then(() => resolve())
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
|
|
@ -62,7 +74,7 @@ async function extract(data: Buffer, outDir: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function installExt(id: string) {
|
export async function installExt(id: string) {
|
||||||
const extDir = join(extensionCacheDir, `${id}`);
|
const extDir = join(extensionCacheDir, id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await access(extDir, fsConstants.F_OK);
|
await access(extDir, fsConstants.F_OK);
|
||||||
|
|
@ -79,6 +91,9 @@ export async function installExt(id: string) {
|
||||||
.catch(err => console.error(`Failed to extract extension ${id}`, err));
|
.catch(err => console.error(`Failed to extract extension ${id}`, err));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Electron 36 Deprecates session.defaultSession.loadExtension()
|
if (session.defaultSession.extensions) {
|
||||||
session.defaultSession.extensions ? session.defaultSession.extensions.loadExtension(extDir) : session.defaultSession.loadExtension(extDir);
|
session.defaultSession.extensions.loadExtension(extDir);
|
||||||
|
} else {
|
||||||
|
session.defaultSession.loadExtension(extDir);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,11 @@
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { definePluginSettings, migratePluginSettings } from "@api/Settings";
|
import { definePluginSettings } from "@api/Settings";
|
||||||
import { getUserSettingLazy } from "@api/UserSettings";
|
import { getUserSettingLazy } from "@api/UserSettings";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
|
import { UserSettingsProtoStore } from "@webpack/common";
|
||||||
|
|
||||||
let savedStatus: string | null;
|
let savedStatus: string | null;
|
||||||
|
|
||||||
|
|
@ -36,36 +37,58 @@ const settings = definePluginSettings({
|
||||||
value: "invisible",
|
value: "invisible",
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
excludeInvisible: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
description: "Prevent automatic status changes while your status is set to invisible",
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
migratePluginSettings("AutoDNDWhilePlaying", "StatusWhilePlaying");
|
let lastStatus: string | null = null;
|
||||||
|
|
||||||
|
function handleUserSettingsChange() {
|
||||||
|
const status = StatusSettings.getSetting();
|
||||||
|
if (status !== lastStatus) {
|
||||||
|
lastStatus = status;
|
||||||
|
|
||||||
|
savedStatus = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setStatus(status: string) {
|
||||||
|
lastStatus = status;
|
||||||
|
await StatusSettings.updateSetting(status);
|
||||||
|
}
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "AutoDNDWhilePlaying",
|
name: "AutoDNDWhilePlaying",
|
||||||
description: "Automatically updates your online status (online, idle, dnd) when launching games",
|
description: "Automatically updates your online status (online, idle, dnd) when launching games",
|
||||||
tags: ["Activity", "Utility"],
|
tags: ["Activity", "Utility"],
|
||||||
authors: [Devs.thororen],
|
authors: [Devs.thororen],
|
||||||
isModified: true,
|
|
||||||
settings,
|
settings,
|
||||||
|
|
||||||
flux: {
|
flux: {
|
||||||
RUNNING_GAMES_CHANGE({ games }) {
|
async RUNNING_GAMES_CHANGE({ games }) {
|
||||||
const status = StatusSettings.getSetting();
|
const status = StatusSettings.getSetting();
|
||||||
|
|
||||||
if (settings.store.excludeInvisible && (savedStatus ?? status) === "invisible") return;
|
|
||||||
|
|
||||||
if (games.length > 0) {
|
if (games.length > 0) {
|
||||||
if (status !== settings.store.statusToSet) {
|
if (status !== settings.store.statusToSet && status !== "invisible") {
|
||||||
savedStatus = status;
|
savedStatus = status;
|
||||||
StatusSettings.updateSetting(settings.store.statusToSet);
|
await setStatus(settings.store.statusToSet);
|
||||||
|
}
|
||||||
|
} else if (savedStatus) {
|
||||||
|
const toRestore = savedStatus;
|
||||||
|
savedStatus = null;
|
||||||
|
|
||||||
|
if (status !== toRestore) {
|
||||||
|
await setStatus(toRestore);
|
||||||
}
|
}
|
||||||
} else if (savedStatus && savedStatus !== settings.store.statusToSet) {
|
|
||||||
StatusSettings.updateSetting(savedStatus);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
start() {
|
||||||
|
lastStatus = StatusSettings.getSetting();
|
||||||
|
UserSettingsProtoStore.addChangeListener(handleUserSettingsChange);
|
||||||
|
},
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
UserSettingsProtoStore.removeChangeListener(handleUserSettingsChange);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { definePluginSettings } from "@api/Settings";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Devs, EquicordDevs } from "@utils/constants";
|
import { Devs, EquicordDevs } from "@utils/constants";
|
||||||
import { useFixedTimer } from "@utils/react";
|
import { useFixedTimer } from "@utils/react";
|
||||||
import { formatDurationMs } from "@utils/text";
|
import { formatDuration } from "@utils/text";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { PassiveUpdateState, VoiceState } from "@vencord/discord-types";
|
import { PassiveUpdateState, VoiceState } from "@vencord/discord-types";
|
||||||
import { FluxDispatcher, GuildStore, React, UserStore } from "@webpack/common";
|
import { FluxDispatcher, GuildStore, React, UserStore } from "@webpack/common";
|
||||||
|
|
@ -30,7 +30,7 @@ export const settings = definePluginSettings({
|
||||||
label: "30d 23h 00m 42s",
|
label: "30d 23h 00m 42s",
|
||||||
value: "human"
|
value: "human"
|
||||||
}
|
}
|
||||||
]
|
] as const
|
||||||
},
|
},
|
||||||
allCallTimers: {
|
allCallTimers: {
|
||||||
type: OptionType.BOOLEAN,
|
type: OptionType.BOOLEAN,
|
||||||
|
|
@ -273,7 +273,7 @@ export default definePlugin({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p style={{ margin: 0, fontFamily: "var(--font-code)" }}>
|
<p style={{ margin: 0, fontFamily: "var(--font-code)" }}>
|
||||||
{formatDurationMs(time, settings.store.format === "human")}
|
{formatDuration(time, settings.store.format === "human")}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
}, { noop: true }),
|
}, { noop: true }),
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
|
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||||
import { isPluginEnabled } from "@api/PluginManager";
|
import { isPluginEnabled } from "@api/PluginManager";
|
||||||
import ExpressionClonerPlugin from "@plugins/expressionCloner";
|
import ExpressionClonerPlugin from "@plugins/expressionCloner";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
|
|
@ -68,9 +68,7 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (
|
||||||
const sticker = message.stickerItems.find(s => s.id === favoriteableId);
|
const sticker = message.stickerItems.find(s => s.id === favoriteableId);
|
||||||
if (!sticker?.format_type) return;
|
if (!sticker?.format_type) return;
|
||||||
|
|
||||||
const idx = children.findIndex(c => Array.isArray(c) && findGroupChildrenByChildId("vc-copy-sticker-url", c) != null);
|
children.splice(-1, 0, buildMenuItem(sticker, false));
|
||||||
|
|
||||||
children.splice(idx, 0, buildMenuItem(sticker, idx !== -1));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const expressionPickerPatch: NavContextMenuPatchCallback = (children, props: { target: HTMLElement; }) => {
|
const expressionPickerPatch: NavContextMenuPatchCallback = (children, props: { target: HTMLElement; }) => {
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
import "./styles.css";
|
import "./styles.css";
|
||||||
|
|
||||||
import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, registerCommand, sendBotMessage } from "@api/Commands";
|
import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, registerCommand, sendBotMessage, unregisterCommand } from "@api/Commands";
|
||||||
import { migratePluginSettings } from "@api/Settings";
|
import { migratePluginSettings } from "@api/Settings";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { sendMessage } from "@utils/discord";
|
import { sendMessage } from "@utils/discord";
|
||||||
|
|
@ -96,11 +96,12 @@ export default definePlugin({
|
||||||
tags: ["Commands", "Customisation", "Utility"],
|
tags: ["Commands", "Customisation", "Utility"],
|
||||||
settings,
|
settings,
|
||||||
|
|
||||||
async start() {
|
start() {
|
||||||
const tags = getTags();
|
getTags().forEach(registerTagCommand);
|
||||||
for (const tagName in tags) {
|
},
|
||||||
registerTagCommand(tags[tagName]);
|
|
||||||
}
|
stop() {
|
||||||
|
getTags().forEach(tag => unregisterCommand(tag.name));
|
||||||
},
|
},
|
||||||
|
|
||||||
commands: [
|
commands: [
|
||||||
|
|
@ -160,7 +161,7 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
|
|
||||||
case "list": {
|
case "list": {
|
||||||
const content = Object.values(getTags())
|
const content = getTags()
|
||||||
.map(tag => `\`${tag.name}\`: ${tag.message.slice(0, 72).replaceAll("\\n", " ")}${tag.message.length > 72 ? "..." : ""}`)
|
.map(tag => `\`${tag.name}\`: ${tag.message.slice(0, 72).replaceAll("\\n", " ")}${tag.message.length > 72 ? "..." : ""}`)
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ export interface Tag {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTags() {
|
export function getTags() {
|
||||||
return settings.store.tagsList;
|
return Object.values(settings.store.tagsList);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTag(name: string) {
|
export function getTag(name: string) {
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,10 @@ function SelectSetting<T>({ settingsKey, label, options, disabled }: SelectOptio
|
||||||
options={options}
|
options={options}
|
||||||
maxVisibleItems={5}
|
maxVisibleItems={5}
|
||||||
closeOnSelect={true}
|
closeOnSelect={true}
|
||||||
select={v => settings.store[settingsKey] = v}
|
select={v => {
|
||||||
|
settings.store[settingsKey] = v;
|
||||||
|
updateRPC();
|
||||||
|
}}
|
||||||
isSelected={v => v === settings.store[settingsKey]}
|
isSelected={v => v === settings.store[settingsKey]}
|
||||||
serialize={v => String(v)}
|
serialize={v => String(v)}
|
||||||
isDisabled={disabled}
|
isDisabled={disabled}
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,6 @@ function DearrowButton({ component }: { component: Component<Props>; }) {
|
||||||
className={"vc-dearrow-toggle-" + (embed.dearrow.enabled ? "on" : "off")}
|
className={"vc-dearrow-toggle-" + (embed.dearrow.enabled ? "on" : "off")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const { enabled, oldThumb, oldTitle } = embed.dearrow;
|
const { enabled, oldThumb, oldTitle } = embed.dearrow;
|
||||||
settings.store.dearrowByDefault = !enabled;
|
|
||||||
embed.dearrow.enabled = !enabled;
|
embed.dearrow.enabled = !enabled;
|
||||||
if (oldTitle) {
|
if (oldTitle) {
|
||||||
embed.dearrow.oldTitle = embed.rawTitle;
|
embed.dearrow.oldTitle = embed.rawTitle;
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,10 @@ const fakeNitroEmojiRegex = /\/emojis\/(\d+?)\.(png|webp|gif)/;
|
||||||
const fakeNitroStickerRegex = /\/stickers\/(\d+?)\./;
|
const fakeNitroStickerRegex = /\/stickers\/(\d+?)\./;
|
||||||
const fakeNitroGifStickerRegex = /\/attachments\/\d+?\/\d+?\/(\d+?)\.gif/;
|
const fakeNitroGifStickerRegex = /\/attachments\/\d+?\/\d+?\/(\d+?)\.gif/;
|
||||||
const hyperLinkRegex = /\[.+?\]\((https?:\/\/.+?)\)/;
|
const hyperLinkRegex = /\[.+?\]\((https?:\/\/.+?)\)/;
|
||||||
|
const mediaSizes = [16, 32, 48, 56, 64, 96, 128, 160, 256, 512, 1024];
|
||||||
|
|
||||||
|
const DEFAULT_EMOJI_SIZE = 48;
|
||||||
|
const DEFAULT_STICKER_SIZE = 160;
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
const settings = definePluginSettings({
|
||||||
enableEmojiBypass: {
|
enableEmojiBypass: {
|
||||||
|
|
@ -83,9 +87,12 @@ const settings = definePluginSettings({
|
||||||
},
|
},
|
||||||
emojiSize: {
|
emojiSize: {
|
||||||
description: "Size of the emojis when sending",
|
description: "Size of the emojis when sending",
|
||||||
type: OptionType.SLIDER,
|
type: OptionType.SELECT,
|
||||||
default: 48,
|
default: DEFAULT_EMOJI_SIZE,
|
||||||
markers: [32, 48, 56, 64, 96, 128, 160, 256, 512]
|
options: mediaSizes.map(size => ({
|
||||||
|
label: `${size}px`,
|
||||||
|
value: size
|
||||||
|
}))
|
||||||
},
|
},
|
||||||
transformEmojis: {
|
transformEmojis: {
|
||||||
description: "Whether to transform fake emojis into real ones",
|
description: "Whether to transform fake emojis into real ones",
|
||||||
|
|
@ -101,9 +108,12 @@ const settings = definePluginSettings({
|
||||||
},
|
},
|
||||||
stickerSize: {
|
stickerSize: {
|
||||||
description: "Size of the stickers when sending",
|
description: "Size of the stickers when sending",
|
||||||
type: OptionType.SLIDER,
|
type: OptionType.SELECT,
|
||||||
default: 160,
|
default: DEFAULT_STICKER_SIZE,
|
||||||
markers: [32, 64, 128, 160, 256, 512]
|
options: mediaSizes.map(size => ({
|
||||||
|
label: `${size}px`,
|
||||||
|
value: size
|
||||||
|
}))
|
||||||
},
|
},
|
||||||
transformStickers: {
|
transformStickers: {
|
||||||
description: "Whether to transform fake stickers into real ones",
|
description: "Whether to transform fake stickers into real ones",
|
||||||
|
|
@ -752,7 +762,7 @@ export default definePlugin({
|
||||||
.then(parseAPNG);
|
.then(parseAPNG);
|
||||||
|
|
||||||
const gif = GIFEncoder();
|
const gif = GIFEncoder();
|
||||||
const resolution = settings.store.stickerSize;
|
const resolution = settings.store.stickerSize ?? DEFAULT_STICKER_SIZE;
|
||||||
|
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
canvas.width = resolution;
|
canvas.width = resolution;
|
||||||
|
|
@ -892,10 +902,11 @@ export default definePlugin({
|
||||||
|
|
||||||
hasBypass = true;
|
hasBypass = true;
|
||||||
|
|
||||||
|
const emojiSize = s.emojiSize ?? DEFAULT_EMOJI_SIZE;
|
||||||
const emojiString = `<${emoji.animated ? "a" : ""}:${emoji.originalName || emoji.name}:${emoji.id}>`;
|
const emojiString = `<${emoji.animated ? "a" : ""}:${emoji.originalName || emoji.name}:${emoji.id}>`;
|
||||||
|
|
||||||
const url = new URL(IconUtils.getEmojiURL({ id: emoji.id, animated: emoji.animated, size: s.emojiSize }));
|
const url = new URL(IconUtils.getEmojiURL({ id: emoji.id, animated: emoji.animated, size: emojiSize }));
|
||||||
url.searchParams.set("size", s.emojiSize.toString());
|
url.searchParams.set("size", emojiSize.toString());
|
||||||
url.searchParams.set("name", emoji.name);
|
url.searchParams.set("name", emoji.name);
|
||||||
url.searchParams.set("lossless", "true");
|
url.searchParams.set("lossless", "true");
|
||||||
|
|
||||||
|
|
@ -930,8 +941,10 @@ export default definePlugin({
|
||||||
|
|
||||||
hasBypass = true;
|
hasBypass = true;
|
||||||
|
|
||||||
const url = new URL(IconUtils.getEmojiURL({ id: emoji.id, animated: emoji.animated, size: s.emojiSize }));
|
const emojiSize = s.emojiSize ?? DEFAULT_EMOJI_SIZE;
|
||||||
url.searchParams.set("size", s.emojiSize.toString());
|
|
||||||
|
const url = new URL(IconUtils.getEmojiURL({ id: emoji.id, animated: emoji.animated, size: emojiSize }));
|
||||||
|
url.searchParams.set("size", emojiSize.toString());
|
||||||
url.searchParams.set("name", emoji.name);
|
url.searchParams.set("name", emoji.name);
|
||||||
url.searchParams.set("lossless", "true");
|
url.searchParams.set("lossless", "true");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,14 +50,14 @@ app.on("browser-window-created", (_, win) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
RendererSettings.addChangeListener("plugins.FixSpotifyEmbeds.volume", newVolume => {
|
|
||||||
try {
|
RendererSettings.addChangeListener("plugins.FixSpotifyEmbeds.volume", newVolume => {
|
||||||
cleanUpAndGetSpotifyFrames().forEach(frame =>
|
try {
|
||||||
frame.executeJavaScript(`globalThis._vcVolume = ${newVolume / 100}`)
|
cleanUpAndGetSpotifyFrames().forEach(frame =>
|
||||||
);
|
frame.executeJavaScript(`globalThis._vcVolume = ${newVolume / 100}`)
|
||||||
} catch (e) {
|
);
|
||||||
console.error("FixSpotifyEmbeds: Failed to update volume", e);
|
} catch (e) {
|
||||||
}
|
console.error("FixSpotifyEmbeds: Failed to update volume", e);
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ export default definePlugin({
|
||||||
sendBotMessage(ctx.channel.id, {
|
sendBotMessage(ctx.channel.id, {
|
||||||
content: `
|
content: `
|
||||||
discord.gg/${invite.code} ·
|
discord.gg/${invite.code} ·
|
||||||
Expires: <t:${new Date(invite.expires_at).getTime() / 1000}:R> ·
|
Expires: <t:${Math.round(new Date(invite.expires_at).getTime() / 1000)}:R> ·
|
||||||
Max uses: \`${invite.max_uses}\`
|
Max uses: \`${invite.max_uses}\`
|
||||||
`.trim().replace(/\s+/g, " ")
|
`.trim().replace(/\s+/g, " ")
|
||||||
});
|
});
|
||||||
|
|
@ -56,7 +56,7 @@ export default definePlugin({
|
||||||
const friendInviteList = invites.map(i =>
|
const friendInviteList = invites.map(i =>
|
||||||
`
|
`
|
||||||
_discord.gg/${i.code}_ ·
|
_discord.gg/${i.code}_ ·
|
||||||
Expires: <t:${new Date(i.expires_at).getTime() / 1000}:R> ·
|
Expires: <t:${Math.round(new Date(i.expires_at).getTime() / 1000)}:R> ·
|
||||||
Times used: \`${i.uses}/${i.max_uses}\`
|
Times used: \`${i.uses}/${i.max_uses}\`
|
||||||
`.trim().replace(/\s+/g, " ")
|
`.trim().replace(/\s+/g, " ")
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,28 @@
|
||||||
|
|
||||||
import { isPluginEnabled } from "@api/PluginManager";
|
import { isPluginEnabled } from "@api/PluginManager";
|
||||||
import { definePluginSettings } from "@api/Settings";
|
import { definePluginSettings } from "@api/Settings";
|
||||||
import { UserAreaButton, UserAreaRenderProps } from "@api/UserArea";
|
import { UserAreaRenderProps } from "@api/UserArea";
|
||||||
import { getUserSettingLazy } from "@api/UserSettings";
|
import { getUserSettingLazy } from "@api/UserSettings";
|
||||||
import equicordToolbox from "@equicordplugins/equicordToolbox";
|
import equicordToolbox from "@equicordplugins/equicordToolbox";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { Menu } from "@webpack/common";
|
import { FluxStore } from "@vencord/discord-types";
|
||||||
|
import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
|
||||||
|
import { Menu, Popout, useRef, useState, useStateFromStores } from "@webpack/common";
|
||||||
|
interface ConnectedAccount {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
revoked: boolean;
|
||||||
|
showActivity: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConnectedAccountsStore extends FluxStore {
|
||||||
|
getAccounts(): ConnectedAccount[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button = findComponentByCodeLazy(".GREEN,positionKeyStemOverride:");
|
||||||
|
const ConnectedAccountsStore = findStoreLazy("ConnectedAccountsStore") as ConnectedAccountsStore;
|
||||||
|
const ConnectedAccountActions = findByPropsLazy("setShowActivity");
|
||||||
|
|
||||||
const ShowCurrentGame = getUserSettingLazy<boolean>("status", "showCurrentGame")!;
|
const ShowCurrentGame = getUserSettingLazy<boolean>("status", "showCurrentGame")!;
|
||||||
|
|
||||||
|
|
@ -76,28 +92,68 @@ function Icon({ className }: { className?: string; }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function GameActivityToggleButton({ iconForeground, hideTooltips, nameplate }: UserAreaRenderProps) {
|
function GameActivityToggleButton(props: UserAreaRenderProps) {
|
||||||
const { location } = settings.use(["location"]);
|
const { location } = settings.use(["location"]);
|
||||||
const showCurrentGame = ShowCurrentGame.useSetting();
|
const showCurrentGame = ShowCurrentGame.useSetting();
|
||||||
|
|
||||||
|
const connectedAccounts = useStateFromStores([ConnectedAccountsStore], () => ConnectedAccountsStore.getAccounts());
|
||||||
|
const spotifyAccounts = connectedAccounts.filter(account => account.type === "spotify" && !account.revoked);
|
||||||
|
// The update is an API request which takes a bit to update the store, so we have to use our own state to reflect the change immediately
|
||||||
|
const [shareSpotifyActivity, setShareSpotifyActivity] = useState(spotifyAccounts[0]?.showActivity ?? false);
|
||||||
|
|
||||||
|
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
||||||
|
|
||||||
if (location !== "PANEL" && isPluginEnabled(equicordToolbox.name)) return null;
|
if (location !== "PANEL" && isPluginEnabled(equicordToolbox.name)) return null;
|
||||||
|
|
||||||
|
const buttonProps = {
|
||||||
|
tooltipText: showCurrentGame ? "Disable Game Activity" : "Enable Game Activity",
|
||||||
|
icon: Icon,
|
||||||
|
role: "switch",
|
||||||
|
ariaChecked: !showCurrentGame,
|
||||||
|
redGlow: !showCurrentGame,
|
||||||
|
plated: props?.nameplate != null,
|
||||||
|
onClick: () => ShowCurrentGame.updateSetting(old => !old)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only show switch if there's exactly one Spotify account connected. Otherwise it may lead to confusion
|
||||||
|
if (spotifyAccounts.length !== 1)
|
||||||
|
return <Button {...buttonProps} />;
|
||||||
|
|
||||||
|
const spotifyAccount = spotifyAccounts[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserAreaButton
|
<Popout
|
||||||
tooltipText={hideTooltips ? void 0 : showCurrentGame ? "Disable Game Activity" : "Enable Game Activity"}
|
position="top"
|
||||||
icon={<Icon className={iconForeground} />}
|
align="left"
|
||||||
role="switch"
|
targetElementRef={buttonRef}
|
||||||
aria-checked={!showCurrentGame}
|
renderPopout={({ closePopout }) => (
|
||||||
redGlow={!showCurrentGame}
|
<Menu.Menu navId="vc-gameActivityToggle-menu" onClose={closePopout}>
|
||||||
plated={nameplate != null}
|
<Menu.MenuCheckboxItem
|
||||||
onClick={() => ShowCurrentGame.updateSetting(old => !old)}
|
id="vc-toggle-spotify"
|
||||||
/>
|
label="Share Spotify Activity"
|
||||||
|
checked={shareSpotifyActivity}
|
||||||
|
action={async () => {
|
||||||
|
ConnectedAccountActions.setShowActivity(spotifyAccount.type, spotifyAccount.id, !shareSpotifyActivity);
|
||||||
|
setShareSpotifyActivity(!shareSpotifyActivity);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Menu.Menu>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{popoutProps => (
|
||||||
|
<Button
|
||||||
|
ref={buttonRef}
|
||||||
|
onContextMenu={popoutProps.onClick}
|
||||||
|
{...buttonProps}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Popout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "GameActivityToggle",
|
name: "GameActivityToggle",
|
||||||
description: "Adds a button next to the mic and deafen button to toggle game activity.",
|
description: "Adds a button next to the mic and deafen button to toggle game activity. Right click it to toggle Spotify activity.",
|
||||||
tags: ["Activity", "Shortcuts"],
|
tags: ["Activity", "Shortcuts"],
|
||||||
authors: [Devs.Nuckyz, Devs.RuukuLada],
|
authors: [Devs.Nuckyz, Devs.RuukuLada],
|
||||||
dependencies: ["UserSettingsAPI", "UserAreaAPI"],
|
dependencies: ["UserSettingsAPI", "UserAreaAPI"],
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,17 @@ export function MemberCount({ isTooltip, tooltipGuildId }: { isTooltip?: true; t
|
||||||
const { voiceActivity } = settings.use(["voiceActivity"]);
|
const { voiceActivity } = settings.use(["voiceActivity"]);
|
||||||
const includeVoice = voiceActivity && !isTooltip;
|
const includeVoice = voiceActivity && !isTooltip;
|
||||||
|
|
||||||
const currentChannel = useStateFromStores([SelectedChannelStore], () => getCurrentChannel());
|
const currentChannel = useStateFromStores(
|
||||||
const guildId = isTooltip ? tooltipGuildId! : currentChannel?.guild_id;
|
[SelectedChannelStore], () => isTooltip ? undefined : getCurrentChannel(),
|
||||||
|
[], (a, b) => a?.id === b?.id
|
||||||
|
);
|
||||||
|
|
||||||
|
const guildId = tooltipGuildId ?? currentChannel?.guild_id;
|
||||||
|
|
||||||
const voiceActivityCount = useStateFromStores(
|
const voiceActivityCount = useStateFromStores(
|
||||||
[VoiceStateStore],
|
[VoiceStateStore],
|
||||||
() => {
|
() => {
|
||||||
if (!includeVoice) return 0;
|
if (!includeVoice || !guildId) return 0;
|
||||||
|
|
||||||
const voiceStates = VoiceStateStore.getVoiceStates(guildId);
|
const voiceStates = VoiceStateStore.getVoiceStates(guildId);
|
||||||
if (!voiceStates) return 0;
|
if (!voiceStates) return 0;
|
||||||
|
|
@ -31,6 +35,7 @@ export function MemberCount({ isTooltip, tooltipGuildId }: { isTooltip?: true; t
|
||||||
return Object.values(voiceStates)
|
return Object.values(voiceStates)
|
||||||
.filter(({ channelId }) => {
|
.filter(({ channelId }) => {
|
||||||
if (!channelId) return false;
|
if (!channelId) return false;
|
||||||
|
|
||||||
const channel = ChannelStore.getChannel(channelId);
|
const channel = ChannelStore.getChannel(channelId);
|
||||||
return channel && PermissionStore.can(PermissionsBits.VIEW_CHANNEL, channel);
|
return channel && PermissionStore.can(PermissionsBits.VIEW_CHANNEL, channel);
|
||||||
})
|
})
|
||||||
|
|
@ -40,34 +45,57 @@ export function MemberCount({ isTooltip, tooltipGuildId }: { isTooltip?: true; t
|
||||||
|
|
||||||
const totalCount = useStateFromStores(
|
const totalCount = useStateFromStores(
|
||||||
[GuildMemberCountStore],
|
[GuildMemberCountStore],
|
||||||
() => GuildMemberCountStore.getMemberCount(guildId!)
|
() => guildId ? GuildMemberCountStore.getMemberCount(guildId) : null
|
||||||
);
|
);
|
||||||
|
|
||||||
let onlineCount = useStateFromStores(
|
let onlineCount = useStateFromStores(
|
||||||
[OnlineMemberCountStore],
|
[OnlineMemberCountStore],
|
||||||
() => OnlineMemberCountStore.getCount(guildId)
|
() => guildId ? OnlineMemberCountStore.getCount(guildId) : null
|
||||||
);
|
);
|
||||||
|
|
||||||
const { groups } = useStateFromStores(
|
const memberListOnlineCount = useStateFromStores(
|
||||||
[ChannelMemberStore],
|
[ChannelMemberStore],
|
||||||
() => ChannelMemberStore.getProps(guildId, currentChannel?.id)
|
() => {
|
||||||
|
if (isTooltip || !guildId) return null;
|
||||||
|
|
||||||
|
const { groups } = ChannelMemberStore.getProps(guildId, currentChannel?.id);
|
||||||
|
|
||||||
|
if (groups.length >= 1 || groups[0].id !== "unknown") {
|
||||||
|
return groups.reduce(
|
||||||
|
(total, curr) => total + (curr.id === "offline" ? 0 : curr.count),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const threadGroups = useStateFromStores(
|
const threadListOnlineCount = useStateFromStores(
|
||||||
[ThreadMemberListStore],
|
[ThreadMemberListStore],
|
||||||
() => ThreadMemberListStore.getMemberListSections(currentChannel?.id)
|
() => {
|
||||||
|
if (isTooltip) return null;
|
||||||
|
|
||||||
|
const threadGroups = ThreadMemberListStore.getMemberListSections(currentChannel?.id);
|
||||||
|
|
||||||
|
if (threadGroups && !isObjectEmpty(threadGroups)) {
|
||||||
|
return Object.values(threadGroups).reduce(
|
||||||
|
(total, curr) => total + (curr.sectionId === "offline" ? 0 : curr.userIds.length),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isTooltip && (groups.length >= 1 || groups[0].id !== "unknown")) {
|
if (memberListOnlineCount != null) onlineCount = memberListOnlineCount;
|
||||||
onlineCount = groups.reduce((total, curr) => total + (curr.id === "offline" ? 0 : curr.count), 0);
|
if (threadListOnlineCount != null) onlineCount = threadListOnlineCount;
|
||||||
}
|
|
||||||
|
|
||||||
if (!isTooltip && threadGroups && !isObjectEmpty(threadGroups)) {
|
|
||||||
onlineCount = Object.values(threadGroups).reduce((total, curr) => total + (curr.sectionId === "offline" ? 0 : curr.userIds.length), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
OnlineMemberCountStore.ensureCount(guildId);
|
if (guildId) {
|
||||||
|
OnlineMemberCountStore.ensureCount(guildId);
|
||||||
|
}
|
||||||
}, [guildId]);
|
}, [guildId]);
|
||||||
|
|
||||||
if (totalCount == null)
|
if (totalCount == null)
|
||||||
|
|
@ -86,6 +114,7 @@ export function MemberCount({ isTooltip, tooltipGuildId }: { isTooltip?: true; t
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip text={`${numberFormat(totalCount)} total server members`} position="bottom">
|
<Tooltip text={`${numberFormat(totalCount)} total server members`} position="bottom">
|
||||||
{props => (
|
{props => (
|
||||||
<div {...props} className={cl("container")}>
|
<div {...props} className={cl("container")}>
|
||||||
|
|
@ -94,6 +123,7 @@ export function MemberCount({ isTooltip, tooltipGuildId }: { isTooltip?: true; t
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{includeVoice && voiceActivityCount > 0 &&
|
{includeVoice && voiceActivityCount > 0 &&
|
||||||
<Tooltip text={`${formattedVoiceCount} members in voice`} position="bottom">
|
<Tooltip text={`${formattedVoiceCount} members in voice`} position="bottom">
|
||||||
{props => (
|
{props => (
|
||||||
|
|
|
||||||
|
|
@ -65,16 +65,44 @@ export default definePlugin({
|
||||||
|
|
||||||
videoClone.loop = settings.store.loop;
|
videoClone.loop = settings.store.loop;
|
||||||
videoClone.style.display = "none";
|
videoClone.style.display = "none";
|
||||||
videoClone.onleavepictureinpicture = () => videoClone.remove();
|
|
||||||
|
|
||||||
function launchPiP() {
|
let cleaned = false;
|
||||||
videoClone.currentTime = video.currentTime;
|
function cleanup() {
|
||||||
videoClone.requestPictureInPicture();
|
if (cleaned) return;
|
||||||
video.pause();
|
cleaned = true;
|
||||||
videoClone.play();
|
|
||||||
|
const { currentTime } = videoClone;
|
||||||
|
|
||||||
|
videoClone.onloadedmetadata = null;
|
||||||
|
videoClone.onleavepictureinpicture = null;
|
||||||
|
|
||||||
|
videoClone.pause();
|
||||||
|
videoClone.removeAttribute("src");
|
||||||
|
videoClone.load();
|
||||||
|
videoClone.remove();
|
||||||
|
|
||||||
|
// resume original if still in the document
|
||||||
|
if (video.isConnected) {
|
||||||
|
video.currentTime = currentTime;
|
||||||
|
video.play().catch(() => 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (videoClone.readyState === 4 /* HAVE_ENOUGH_DATA */)
|
videoClone.onleavepictureinpicture = cleanup;
|
||||||
|
|
||||||
|
async function launchPiP() {
|
||||||
|
try {
|
||||||
|
videoClone.currentTime = video.currentTime;
|
||||||
|
video.pause();
|
||||||
|
await videoClone.play();
|
||||||
|
await videoClone.requestPictureInPicture();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to enter Picture-in-Picture", err);
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (videoClone.readyState >= 1)
|
||||||
launchPiP();
|
launchPiP();
|
||||||
else
|
else
|
||||||
videoClone.onloadedmetadata = launchPiP;
|
videoClone.onloadedmetadata = launchPiP;
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ import definePlugin, { IconComponent, StartAt } from "@utils/types";
|
||||||
import { CloudUpload, MessageAttachment } from "@vencord/discord-types";
|
import { CloudUpload, MessageAttachment } from "@vencord/discord-types";
|
||||||
import { DraftStore, DraftType, UploadAttachmentStore, UserStore, useStateFromStores } from "@webpack/common";
|
import { DraftStore, DraftType, UploadAttachmentStore, UserStore, useStateFromStores } from "@webpack/common";
|
||||||
|
|
||||||
|
const objectURLMap = new Map<string, string[]>();
|
||||||
|
|
||||||
const getDraft = (channelId: string) => DraftStore.getDraft(channelId, DraftType.ChannelMessage);
|
const getDraft = (channelId: string) => DraftStore.getDraft(channelId, DraftType.ChannelMessage);
|
||||||
|
|
||||||
const getImageBox = (url: string): Promise<{ width: number, height: number; } | null> =>
|
const getImageBox = (url: string): Promise<{ width: number, height: number; } | null> =>
|
||||||
|
|
@ -42,6 +44,8 @@ const getAttachments = async (channelId: string) =>
|
||||||
UploadAttachmentStore.getUploads(channelId, DraftType.ChannelMessage)
|
UploadAttachmentStore.getUploads(channelId, DraftType.ChannelMessage)
|
||||||
.map(async (upload: CloudUpload) => {
|
.map(async (upload: CloudUpload) => {
|
||||||
const { isImage, filename, spoiler, item: { file } } = upload;
|
const { isImage, filename, spoiler, item: { file } } = upload;
|
||||||
|
|
||||||
|
// FIXME: revoke object url to fix memory leak
|
||||||
const url = URL.createObjectURL(file);
|
const url = URL.createObjectURL(file);
|
||||||
const attachment: MessageAttachment = {
|
const attachment: MessageAttachment = {
|
||||||
id: generateId(),
|
id: generateId(),
|
||||||
|
|
@ -57,13 +61,13 @@ const getAttachments = async (channelId: string) =>
|
||||||
|
|
||||||
if (isImage) {
|
if (isImage) {
|
||||||
const box = await getImageBox(url);
|
const box = await getImageBox(url);
|
||||||
if (!box) return attachment;
|
if (box) {
|
||||||
|
attachment.width = box.width;
|
||||||
attachment.width = box.width;
|
attachment.height = box.height;
|
||||||
attachment.height = box.height;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return attachment;
|
return { attachment, objectURL: url };
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -96,15 +100,20 @@ const PreviewButton: ChatBarButtonFactory = ({ isAnyChat, isEmpty, type: { attac
|
||||||
return (
|
return (
|
||||||
<ChatBarButton
|
<ChatBarButton
|
||||||
tooltip="Preview Message"
|
tooltip="Preview Message"
|
||||||
onClick={async () =>
|
onClick={async () => {
|
||||||
sendBotMessage(
|
const attachments = hasAttachments ? await getAttachments(channelId) : undefined;
|
||||||
|
const message = sendBotMessage(
|
||||||
channelId,
|
channelId,
|
||||||
{
|
{
|
||||||
content: getDraft(channelId),
|
content: getDraft(channelId),
|
||||||
author: UserStore.getCurrentUser(),
|
author: UserStore.getCurrentUser(),
|
||||||
attachments: hasAttachments ? await getAttachments(channelId) : undefined,
|
attachments: attachments?.map(a => a.attachment),
|
||||||
}
|
}
|
||||||
)}
|
);
|
||||||
|
|
||||||
|
if (attachments)
|
||||||
|
objectURLMap.set(message.id, attachments.map(a => a.objectURL));
|
||||||
|
}}
|
||||||
buttonProps={{
|
buttonProps={{
|
||||||
style: {
|
style: {
|
||||||
translate: "0 2px"
|
translate: "0 2px"
|
||||||
|
|
@ -130,5 +139,15 @@ export default definePlugin({
|
||||||
chatBarButton: {
|
chatBarButton: {
|
||||||
icon: PreviewIcon,
|
icon: PreviewIcon,
|
||||||
render: PreviewButton
|
render: PreviewButton
|
||||||
|
},
|
||||||
|
|
||||||
|
flux: {
|
||||||
|
MESSAGE_DELETE({ id: messageId }) {
|
||||||
|
const objectURLs = objectURLMap.get(messageId);
|
||||||
|
if (objectURLs) {
|
||||||
|
objectURLs.forEach(url => URL.revokeObjectURL(url));
|
||||||
|
objectURLMap.delete(messageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -232,7 +232,7 @@ export default function ReviewComponent({ review, refetch, profileId }: { review
|
||||||
<>
|
<>
|
||||||
{Parser.parseGuildEventDescription(review.comment.substring(0, 200))}...
|
{Parser.parseGuildEventDescription(review.comment.substring(0, 200))}...
|
||||||
<br />
|
<br />
|
||||||
<a onClick={() => setShowAll(true)}>Read more</a>]
|
<a onClick={() => setShowAll(true)}>Read more</a>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
: Parser.parseGuildEventDescription(review.comment)}
|
: Parser.parseGuildEventDescription(review.comment)}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import PermissionsViewerPlugin from "@plugins/permissionsViewer";
|
||||||
import openRolesAndUsersPermissionsModal from "@plugins/permissionsViewer/components/RolesAndUsersPermissions";
|
import openRolesAndUsersPermissionsModal from "@plugins/permissionsViewer/components/RolesAndUsersPermissions";
|
||||||
import { sortPermissionOverwrites } from "@plugins/permissionsViewer/utils";
|
import { sortPermissionOverwrites } from "@plugins/permissionsViewer/utils";
|
||||||
import { classes } from "@utils/misc";
|
import { classes } from "@utils/misc";
|
||||||
import { formatDuration } from "@utils/text";
|
import { formatDurationVerbose } from "@utils/text";
|
||||||
import type { Channel, RoleOrUserPermission } from "@vencord/discord-types";
|
import type { Channel, RoleOrUserPermission } from "@vencord/discord-types";
|
||||||
import { findByPropsLazy, findComponentByCodeLazy, findCssClassesLazy } from "@webpack";
|
import { findByPropsLazy, findComponentByCodeLazy, findCssClassesLazy } from "@webpack";
|
||||||
import { EmojiStore, FluxDispatcher, GuildMemberStore, GuildStore, Parser, PermissionsBits, PermissionStore, SnowflakeUtils, Timestamp, Tooltip, useEffect, useState } from "@webpack/common";
|
import { EmojiStore, FluxDispatcher, GuildMemberStore, GuildStore, Parser, PermissionsBits, PermissionStore, SnowflakeUtils, Timestamp, Tooltip, useEffect, useState } from "@webpack/common";
|
||||||
|
|
@ -207,13 +207,11 @@ function HiddenChannelLockScreen({ channel }: { channel: Channel; }) {
|
||||||
</BaseText>
|
</BaseText>
|
||||||
}
|
}
|
||||||
{(rateLimitPerUser ?? 0) > 0 &&
|
{(rateLimitPerUser ?? 0) > 0 &&
|
||||||
<BaseText size="md">
|
<BaseText size="md">Slowmode: {formatDurationVerbose(rateLimitPerUser!, "seconds")}</BaseText>
|
||||||
Slowmode: {formatDuration(rateLimitPerUser!, "seconds")}
|
|
||||||
</BaseText>
|
|
||||||
}
|
}
|
||||||
{(defaultThreadRateLimitPerUser ?? 0) > 0 &&
|
{(defaultThreadRateLimitPerUser ?? 0) > 0 &&
|
||||||
<BaseText size="md">
|
<BaseText size="md">
|
||||||
Default thread slowmode: {formatDuration(defaultThreadRateLimitPerUser!, "seconds")}
|
Default thread slowmode: {formatDurationVerbose(defaultThreadRateLimitPerUser!, "seconds")}
|
||||||
</BaseText>
|
</BaseText>
|
||||||
}
|
}
|
||||||
{((channel.isGuildVoice() || channel.isGuildStageVoice()) && bitrate != null) &&
|
{((channel.isGuildVoice() || channel.isGuildStageVoice()) && bitrate != null) &&
|
||||||
|
|
@ -232,7 +230,7 @@ function HiddenChannelLockScreen({ channel }: { channel: Channel; }) {
|
||||||
{(defaultAutoArchiveDuration ?? 0) > 0 &&
|
{(defaultAutoArchiveDuration ?? 0) > 0 &&
|
||||||
<BaseText size="md">
|
<BaseText size="md">
|
||||||
Default inactivity duration before archiving {channel.isForumChannel() ? "posts" : "threads"}:
|
Default inactivity duration before archiving {channel.isForumChannel() ? "posts" : "threads"}:
|
||||||
{" " + formatDuration(defaultAutoArchiveDuration!, "minutes")}
|
{" " + formatDurationVerbose(defaultAutoArchiveDuration!, "minutes")}
|
||||||
</BaseText>
|
</BaseText>
|
||||||
}
|
}
|
||||||
{defaultForumLayout != null &&
|
{defaultForumLayout != null &&
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (
|
||||||
|
|
||||||
const menuGroup = findGroupChildrenByChildId("delete", children);
|
const menuGroup = findGroupChildrenByChildId("delete", children);
|
||||||
const deleteIndex = menuGroup?.findIndex(i => i?.props?.id === "delete");
|
const deleteIndex = menuGroup?.findIndex(i => i?.props?.id === "delete");
|
||||||
if (!deleteIndex || !menuGroup) return;
|
if (deleteIndex == null || !menuGroup) return;
|
||||||
|
|
||||||
menuGroup.splice(deleteIndex - 1, 0, (
|
menuGroup.splice(deleteIndex - 1, 0, (
|
||||||
<Menu.MenuItem
|
<Menu.MenuItem
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import { openImageModal } from "@utils/discord";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import type { Channel, Guild, User } from "@vencord/discord-types";
|
import type { Channel, Guild, User } from "@vencord/discord-types";
|
||||||
import { GuildMemberStore, IconUtils, Menu } from "@webpack/common";
|
import { GuildMemberStore, IconUtils, Menu } from "@webpack/common";
|
||||||
|
import type { MouseEvent } from "react";
|
||||||
|
|
||||||
interface UserContextProps {
|
interface UserContextProps {
|
||||||
channel: Channel;
|
channel: Channel;
|
||||||
|
|
@ -67,10 +68,18 @@ const settings = definePluginSettings({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const openAvatar = (url: string) => openImage(url, 512, 512);
|
const openAvatar = (url: string, event?: MouseEvent) => openImage({ url, width: 512, height: 512, event });
|
||||||
const openBanner = (url: string) => openImage(url, 1024);
|
const openBanner = (url: string, event?: MouseEvent) => openImage({ url, width: 1024, event });
|
||||||
|
|
||||||
function openImage(url: string, width: number, height?: number) {
|
interface OpenImageProps {
|
||||||
|
url: string;
|
||||||
|
width: number;
|
||||||
|
height?: number;
|
||||||
|
event?: MouseEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openImage({ url, width, height, event }: OpenImageProps) {
|
||||||
|
event?.stopPropagation();
|
||||||
const u = new URL(url, window.location.href);
|
const u = new URL(url, window.location.href);
|
||||||
|
|
||||||
const format = url.startsWith("/")
|
const format = url.startsWith("/")
|
||||||
|
|
@ -214,8 +223,8 @@ export default definePlugin({
|
||||||
{
|
{
|
||||||
find: "return{avatarProps:{",
|
find: "return{avatarProps:{",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<=avatarProps:(\i),eventHandlers:\i.{0,150}?)return null==(?<=onOpenAvatar:(\i).+?)/,
|
match: /(?<=onClick:.{0,30}?)null!=(\i)(?=.{0,200}children:.{0,50}...(\i),imageClassName:)/,
|
||||||
replace: "$2&&=$self.openAvatar.bind(undefined,$1.src);$&",
|
replace: "null!=($1&&=$self.openAvatar.bind(undefined,$2.src))",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Banners
|
// Banners
|
||||||
|
|
@ -223,7 +232,7 @@ export default definePlugin({
|
||||||
find: 'backgroundColor:"COMPLETE"',
|
find: 'backgroundColor:"COMPLETE"',
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(overflow:"visible",.{0,125}?!1\),)style:{(?=.+?backgroundImage:null!=(\i)\?`url\(\$\{\2\}\))/,
|
match: /(overflow:"visible",.{0,125}?!1\),)style:{(?=.+?backgroundImage:null!=(\i)\?`url\(\$\{\2\}\))/,
|
||||||
replace: (_, rest, bannerSrc) => `${rest}onClick:()=>${bannerSrc}!=null&&$self.openBanner(${bannerSrc}),style:{cursor:${bannerSrc}!=null?"pointer":void 0,`
|
replace: (_, rest, bannerSrc) => `${rest}onClick:vcEvent=>${bannerSrc}!=null&&$self.openBanner(${bannerSrc}, vcEvent),style:{cursor:${bannerSrc}!=null?"pointer":void 0,`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Group DMs top small & large icon
|
// Group DMs top small & large icon
|
||||||
|
|
@ -232,7 +241,7 @@ export default definePlugin({
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /null==\i\.icon\?.+?src:(\(0,\i\.\i\).+?\))(?=[,}])/,
|
match: /null==\i\.icon\?.+?src:(\(0,\i\.\i\).+?\))(?=[,}])/,
|
||||||
// We have to check that icon is not an unread GDM in the server bar
|
// We have to check that icon is not an unread GDM in the server bar
|
||||||
replace: (m, iconUrl) => `${m},onClick:()=>arguments[0]?.size!=="SIZE_48"&&$self.openAvatar(${iconUrl})`
|
replace: (m, iconUrl) => `${m},onClick:vcEvent=>arguments[0]?.size!=="SIZE_48"&&$self.openAvatar(${iconUrl},vcEvent)`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// User DMs top small icon
|
// User DMs top small icon
|
||||||
|
|
@ -240,7 +249,7 @@ export default definePlugin({
|
||||||
find: ".channel.getRecipientId(),",
|
find: ".channel.getRecipientId(),",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?=,src:(\i.getAvatarURL\(.+?[)]))/,
|
match: /(?=,src:(\i.getAvatarURL\(.+?[)]))/,
|
||||||
replace: (_, avatarUrl) => `,onClick:()=>$self.openAvatar(${avatarUrl})`
|
replace: (_, avatarUrl) => `,onClick:vcEvent=>$self.openAvatar(${avatarUrl},vcEvent)`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// User Dms top large icon
|
// User Dms top large icon
|
||||||
|
|
@ -248,7 +257,7 @@ export default definePlugin({
|
||||||
find: ".EMPTY_GROUP_DM)",
|
find: ".EMPTY_GROUP_DM)",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<=SIZE_80,)(?=src:(.+?\))[,}])/,
|
match: /(?<=SIZE_80,)(?=src:(.+?\))[,}])/,
|
||||||
replace: (_, avatarUrl) => `onClick:()=>$self.openAvatar(${avatarUrl}),`
|
replace: (_, avatarUrl) => `onClick:vcEvent=>$self.openAvatar(${avatarUrl},vcEvent),`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ export const VoiceRecorderWeb: VoiceRecorder = ({ setAudioBlob, onRecordingChang
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
recorder?.stop();
|
recorder?.stop();
|
||||||
|
recorder?.stream.getTracks().forEach(track => track.stop());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -303,6 +303,7 @@ export default definePlugin({
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
socket.close();
|
socket.close();
|
||||||
|
Native.closeSocket();
|
||||||
},
|
},
|
||||||
|
|
||||||
settingsAboutComponent: () => (
|
settingsAboutComponent: () => (
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
import { createSocket, Socket } from "dgram";
|
import { createSocket, Socket } from "dgram";
|
||||||
|
|
||||||
let xsoSocket: Socket;
|
let xsoSocket: Socket | null = null;
|
||||||
|
|
||||||
export function sendToOverlay(_, data: any) {
|
export function sendToOverlay(_, data: any) {
|
||||||
data.messageType = data.type;
|
data.messageType = data.type;
|
||||||
|
|
@ -14,3 +14,8 @@ export function sendToOverlay(_, data: any) {
|
||||||
xsoSocket ??= createSocket("udp4");
|
xsoSocket ??= createSocket("udp4");
|
||||||
xsoSocket.send(json, 42069, "127.0.0.1");
|
xsoSocket.send(json, 42069, "127.0.0.1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function closeSocket() {
|
||||||
|
xsoSocket?.close();
|
||||||
|
xsoSocket = null;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -670,6 +670,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
||||||
name: "Lunascape",
|
name: "Lunascape",
|
||||||
id: 383365021415243776n
|
id: 383365021415243776n
|
||||||
},
|
},
|
||||||
|
jax: {
|
||||||
|
name: "jax",
|
||||||
|
id: 1493703027801194598n
|
||||||
|
},
|
||||||
yuna0x0: {
|
yuna0x0: {
|
||||||
name: "yuna0x0",
|
name: "yuna0x0",
|
||||||
id: 213656926414831616n
|
id: 213656926414831616n
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@ export function useAwaiter<T>(factory: () => Promise<T>, providedOpts?: AwaiterO
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
if (!isAlive) return;
|
if (!isAlive) return;
|
||||||
setState({ value: null, error, pending: false });
|
setState({ value: opts.fallbackValue, error, pending: false });
|
||||||
opts.onError?.(error);
|
opts.onError?.(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { isTruthy } from "./guards";
|
||||||
|
|
||||||
// Utils for readable text transformations eg: `toTitle(fromKebab())`
|
// Utils for readable text transformations eg: `toTitle(fromKebab())`
|
||||||
|
|
||||||
// Case style to words
|
// Case style to words
|
||||||
|
|
@ -51,7 +53,7 @@ function getUnitStr(unit: Units, isOne: boolean, short: boolean) {
|
||||||
* @param unit The unit the time is on
|
* @param unit The unit the time is on
|
||||||
* @param short Whether to use short units like "d" instead of "days"
|
* @param short Whether to use short units like "d" instead of "days"
|
||||||
*/
|
*/
|
||||||
export function formatDuration(time: number, unit: Units, short: boolean = false) {
|
export function formatDurationVerbose(time: number, unit: Units, short: boolean = false) {
|
||||||
const { moment } = require("@webpack/common") as typeof import("@webpack/common");
|
const { moment } = require("@webpack/common") as typeof import("@webpack/common");
|
||||||
const dur = moment.duration(time, unit);
|
const dur = moment.duration(time, unit);
|
||||||
|
|
||||||
|
|
@ -93,6 +95,33 @@ export function formatDuration(time: number, unit: Units, short: boolean = false
|
||||||
return res.length ? res : `0 ${getUnitStr(unit, false, short)}`;
|
return res.length ? res : `0 ${getUnitStr(unit, false, short)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatDuration(ms: number, human = false) {
|
||||||
|
const DAY = 86_400_000;
|
||||||
|
const HOUR = 3_600_000;
|
||||||
|
const MINUTE = 60_000;
|
||||||
|
|
||||||
|
const days = Math.floor(ms / DAY);
|
||||||
|
const hours = Math.floor((ms % DAY) / HOUR);
|
||||||
|
const minutes = Math.floor((ms % HOUR) / MINUTE);
|
||||||
|
const seconds = Math.floor((ms % MINUTE) / 1000);
|
||||||
|
|
||||||
|
const pad = (n: number) => human ? n : n.toString().padStart(2, "0");
|
||||||
|
const unit = (s: string) => human ? s : "";
|
||||||
|
|
||||||
|
const showDays = days !== 0;
|
||||||
|
const showHours = showDays || hours !== 0;
|
||||||
|
const showMinutes = showHours || minutes !== 0 || !human;
|
||||||
|
|
||||||
|
const duration = [
|
||||||
|
showHours && `${pad(hours)}${unit("h")}`,
|
||||||
|
showMinutes && `${pad(minutes)}${unit("m")}`,
|
||||||
|
`${pad(seconds)}${unit("s")}`
|
||||||
|
].filter(isTruthy).join(human ? " " : ":");
|
||||||
|
|
||||||
|
const prefix = showDays ? `${days}d ` : "";
|
||||||
|
return prefix + duration;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `formatDurationMs` function formats a duration in milliseconds into a human-readable string,
|
* The `formatDurationMs` function formats a duration in milliseconds into a human-readable string,
|
||||||
* with the option to include units such as days, hours, minutes, and seconds.
|
* with the option to include units such as days, hours, minutes, and seconds.
|
||||||
|
|
|
||||||
|
|
@ -507,8 +507,8 @@ function patchFactory(moduleId: PropertyKey, originalFactory: AnyModuleFactory):
|
||||||
const isArrowFunction = originalFactoryCode.startsWith("(");
|
const isArrowFunction = originalFactoryCode.startsWith("(");
|
||||||
|
|
||||||
// 0, prefix to turn it into an expression: 0,function(){} would be invalid syntax without the 0,
|
// 0, prefix to turn it into an expression: 0,function(){} would be invalid syntax without the 0,
|
||||||
let code = "0," + (!isArrowFunction ? "function" : "") + originalFactoryCode.slice(originalFactoryCode.indexOf("("));
|
let patchedCode = "0," + (!isArrowFunction ? "function" : "") + originalFactoryCode.slice(originalFactoryCode.indexOf("("));
|
||||||
let patchedSource = code;
|
let patchedSource = patchedCode;
|
||||||
let patchedFactory = originalFactory;
|
let patchedFactory = originalFactory;
|
||||||
|
|
||||||
const patchedBy = new Set<string>();
|
const patchedBy = new Set<string>();
|
||||||
|
|
@ -521,58 +521,59 @@ function patchFactory(moduleId: PropertyKey, originalFactory: AnyModuleFactory):
|
||||||
|
|
||||||
if (
|
if (
|
||||||
shouldCheckBuildNumber &&
|
shouldCheckBuildNumber &&
|
||||||
(patch.fromBuild != null && buildNumber < patch.fromBuild) ||
|
((patch.fromBuild != null && buildNumber < patch.fromBuild) || (patch.toBuild != null && buildNumber > patch.toBuild))
|
||||||
(patch.toBuild != null && buildNumber > patch.toBuild)
|
|
||||||
) {
|
) {
|
||||||
patches.splice(i--, 1);
|
patches.splice(i--, 1);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const moduleMatches = typeof patch.find === "string"
|
const moduleMatches = typeof patch.find === "string"
|
||||||
? code.includes(patch.find)
|
? originalFactoryCode.includes(patch.find)
|
||||||
: (patch.find.global && (patch.find.lastIndex = 0), patch.find.test(code));
|
: (patch.find.global && (patch.find.lastIndex = 0), patch.find.test(originalFactoryCode));
|
||||||
|
|
||||||
if (!moduleMatches) {
|
if (!moduleMatches) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save the result from the previous patch so we can restore it in case a patch group fails.
|
||||||
|
const previousPatchedCode = patchedCode;
|
||||||
|
const previousPatchedSource = patchedSource;
|
||||||
|
const previousPatchedFactory = patchedFactory;
|
||||||
|
|
||||||
|
let shouldRestorePrevious = false;
|
||||||
|
let markedAsPatched = false;
|
||||||
|
|
||||||
const executePatch = traceFunctionWithResults(`patch by ${patch.plugin}`, (match: string | RegExp, replace: string) => {
|
const executePatch = traceFunctionWithResults(`patch by ${patch.plugin}`, (match: string | RegExp, replace: string) => {
|
||||||
if (typeof match !== "string" && match.global) {
|
if (typeof match !== "string" && match.global) {
|
||||||
match.lastIndex = 0;
|
match.lastIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return code.replace(match, replace);
|
return patchedCode.replace(match, replace);
|
||||||
});
|
});
|
||||||
|
|
||||||
const previousCode = code;
|
|
||||||
const previousFactory = originalFactory;
|
|
||||||
let markedAsPatched = false;
|
|
||||||
|
|
||||||
// We change all patch.replacement to array in PluginManager
|
// We change all patch.replacement to array in PluginManager
|
||||||
for (const replacement of patch.replacement as PatchReplacement[]) {
|
for (const replacement of patch.replacement as PatchReplacement[]) {
|
||||||
if (
|
if (
|
||||||
shouldCheckBuildNumber &&
|
shouldCheckBuildNumber &&
|
||||||
(replacement.fromBuild != null && buildNumber < replacement.fromBuild) ||
|
((replacement.fromBuild != null && buildNumber < replacement.fromBuild) || (replacement.toBuild != null && buildNumber > replacement.toBuild))
|
||||||
(replacement.toBuild != null && buildNumber > replacement.toBuild)
|
|
||||||
) {
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastCode = code;
|
let newPatchedCode: string = "";
|
||||||
const lastFactory = originalFactory;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [newCode, totalTime] = executePatch(replacement.match, replacement.replace as string);
|
const [patchResult, totalTime] = executePatch(replacement.match, replacement.replace as string);
|
||||||
|
newPatchedCode = patchResult;
|
||||||
|
|
||||||
if (IS_REPORTER) {
|
if (IS_REPORTER) {
|
||||||
patchTimings.push([patch.plugin, moduleId, replacement.match, totalTime]);
|
patchTimings.push([patch.plugin, moduleId, replacement.match, totalTime]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newCode === code) {
|
if (newPatchedCode === patchedCode) {
|
||||||
if (!(patch.noWarn || replacement.noWarn)) {
|
if (!(patch.noWarn || replacement.noWarn)) {
|
||||||
logger.warn(`Patch by ${patch.plugin} had no effect (Module id is ${String(moduleId)}): ${replacement.match}`);
|
logger.warn(`Patch by ${patch.plugin} had no effect (Module id is ${String(moduleId)}): ${replacement.match}`);
|
||||||
if (IS_DEV) {
|
if (IS_DEV) {
|
||||||
logger.debug("Function Source:\n", code);
|
logger.debug("Function Source:\n", patchedCode);
|
||||||
}
|
}
|
||||||
if (IS_COMPANION_TEST)
|
if (IS_COMPANION_TEST)
|
||||||
reporterData.failedPatches.hadNoEffect.push({
|
reporterData.failedPatches.hadNoEffect.push({
|
||||||
|
|
@ -583,8 +584,6 @@ function patchFactory(moduleId: PropertyKey, originalFactory: AnyModuleFactory):
|
||||||
|
|
||||||
if (patch.group) {
|
if (patch.group) {
|
||||||
logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} had no effect`);
|
logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} had no effect`);
|
||||||
code = previousCode;
|
|
||||||
patchedFactory = previousFactory;
|
|
||||||
|
|
||||||
if (markedAsPatched) {
|
if (markedAsPatched) {
|
||||||
patchedBy.delete(patch.plugin);
|
patchedBy.delete(patch.plugin);
|
||||||
|
|
@ -596,6 +595,8 @@ function patchFactory(moduleId: PropertyKey, originalFactory: AnyModuleFactory):
|
||||||
id: moduleId
|
id: moduleId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
shouldRestorePrevious = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -607,14 +608,17 @@ function patchFactory(moduleId: PropertyKey, originalFactory: AnyModuleFactory):
|
||||||
pluginsList.push(patch.plugin);
|
pluginsList.push(patch.plugin);
|
||||||
}
|
}
|
||||||
|
|
||||||
code = newCode;
|
const newPatchedSource = `// Webpack Module ${String(moduleId)} - Patched by ${pluginsList.join(", ")}\n${newPatchedCode}\n//# sourceURL=file:///WebpackModule${String(moduleId)}`;
|
||||||
patchedSource = `// Webpack Module ${String(moduleId)} - Patched by ${pluginsList.join(", ")}\n${code}\n//# sourceURL=file:///WebpackModule${String(moduleId)}`;
|
const newPatchedFactory = (0, eval)(newPatchedSource);
|
||||||
patchedFactory = (0, eval)(patchedSource);
|
|
||||||
|
|
||||||
if (!patchedBy.has(patch.plugin)) {
|
if (!patchedBy.has(patch.plugin)) {
|
||||||
patchedBy.add(patch.plugin);
|
patchedBy.add(patch.plugin);
|
||||||
markedAsPatched = true;
|
markedAsPatched = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
patchedCode = newPatchedCode;
|
||||||
|
patchedSource = newPatchedSource;
|
||||||
|
patchedFactory = newPatchedFactory;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// FIXME: Maybe fix this properly
|
// FIXME: Maybe fix this properly
|
||||||
const shouldSuppressError = patch.plugin === "ContextMenuAPI" && err instanceof SyntaxError && err.message.includes("arguments");
|
const shouldSuppressError = patch.plugin === "ContextMenuAPI" && err instanceof SyntaxError && err.message.includes("arguments");
|
||||||
|
|
@ -624,20 +628,16 @@ function patchFactory(moduleId: PropertyKey, originalFactory: AnyModuleFactory):
|
||||||
if (IS_COMPANION_TEST)
|
if (IS_COMPANION_TEST)
|
||||||
reporterData.failedPatches.erroredPatch.push({
|
reporterData.failedPatches.erroredPatch.push({
|
||||||
...patch,
|
...patch,
|
||||||
oldModule: lastCode,
|
oldModule: patchedCode,
|
||||||
newModule: code,
|
newModule: newPatchedCode,
|
||||||
id: moduleId
|
id: moduleId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (IS_DEV) {
|
if (IS_DEV) {
|
||||||
diffErroredPatch(code, lastCode, lastCode.match(replacement.match)!);
|
diffErroredPatch(newPatchedCode, patchedCode, patchedCode.match(replacement.match)!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (markedAsPatched) {
|
|
||||||
patchedBy.delete(patch.plugin);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (patch.group) {
|
if (patch.group) {
|
||||||
logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} errored`);
|
logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} errored`);
|
||||||
if (IS_COMPANION_TEST)
|
if (IS_COMPANION_TEST)
|
||||||
|
|
@ -645,13 +645,19 @@ function patchFactory(moduleId: PropertyKey, originalFactory: AnyModuleFactory):
|
||||||
...patch,
|
...patch,
|
||||||
id: moduleId
|
id: moduleId
|
||||||
});
|
});
|
||||||
code = previousCode;
|
shouldRestorePrevious = true;
|
||||||
patchedFactory = previousFactory;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
code = lastCode;
|
if (shouldRestorePrevious) {
|
||||||
patchedFactory = lastFactory;
|
patchedCode = previousPatchedCode;
|
||||||
|
patchedSource = previousPatchedSource;
|
||||||
|
patchedFactory = previousPatchedFactory;
|
||||||
|
|
||||||
|
if (markedAsPatched) {
|
||||||
|
patchedBy.delete(patch.plugin);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -673,9 +679,12 @@ function patchFactory(moduleId: PropertyKey, originalFactory: AnyModuleFactory):
|
||||||
function diffErroredPatch(code: string, lastCode: string, match: RegExpMatchArray) {
|
function diffErroredPatch(code: string, lastCode: string, match: RegExpMatchArray) {
|
||||||
const changeSize = code.length - lastCode.length;
|
const changeSize = code.length - lastCode.length;
|
||||||
|
|
||||||
|
const matchIndex = match?.index ?? 0;
|
||||||
|
const matchLength = match?.[0]?.length ?? 0;
|
||||||
|
|
||||||
// Use 200 surrounding characters of context
|
// Use 200 surrounding characters of context
|
||||||
const start = Math.max(0, match.index! - 200);
|
const start = Math.max(0, matchIndex - 200);
|
||||||
const end = Math.min(lastCode.length, match.index! + match[0].length + 200);
|
const end = Math.min(lastCode.length, matchIndex + matchLength + 200);
|
||||||
// (changeSize may be negative)
|
// (changeSize may be negative)
|
||||||
const endPatched = end + changeSize;
|
const endPatched = end + changeSize;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue