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",
|
||||
"private": "true",
|
||||
"version": "1.14.16.0",
|
||||
"version": "1.15.0.0",
|
||||
"description": "The other cutest Discord client mod",
|
||||
"homepage": "https://github.com/Equicord/Equicord#readme",
|
||||
"bugs": {
|
||||
|
|
@ -97,7 +97,7 @@
|
|||
"typescript-transform-paths": "^3.5.6",
|
||||
"zip-local": "^0.3.5"
|
||||
},
|
||||
"packageManager": "pnpm@11.13.0",
|
||||
"packageManager": "pnpm@11.17.0",
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import { appendFile, mkdir, readdir, readFile, rm, writeFile } from "fs/promises
|
|||
import path, { join } from "path";
|
||||
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}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export interface ProfileBadge {
|
|||
/** Action to perform when you click the badge */
|
||||
onClick?(event: React.MouseEvent, props: ProfileBadge & BadgeUserArgs): void;
|
||||
/** 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? */
|
||||
shouldShow?(userInfo: BadgeUserArgs): boolean;
|
||||
/** 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;
|
||||
|
||||
// Discord's command list has random gaps for some reason, which can cause issues while rendering the commands
|
||||
// Add this offset to every added command to keep them unique
|
||||
let commandIdOffset: number;
|
||||
let idCounter = 99;
|
||||
|
||||
export const _init = function (cmds: VencordCommand[]) {
|
||||
try {
|
||||
BUILT_IN = cmds;
|
||||
OptionalMessageOption = cmds.find(c => (c.untranslatedName || c.displayName) === "shrug")!.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) {
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
// TODO: This probably doesn't support nested subcommands. If that is ever needed,
|
||||
// investigate
|
||||
function registerSubCommands(cmd: VencordCommand, plugin: string) {
|
||||
cmd.options?.forEach(o => {
|
||||
if (o.type !== ApplicationCommandOptionType.SUB_COMMAND)
|
||||
throw new Error("When specifying sub-command options, all options must be sub-commands.");
|
||||
|
||||
const subCmd = {
|
||||
...cmd,
|
||||
...o,
|
||||
options: o.options !== undefined ? o.options : undefined,
|
||||
type: ApplicationCommandType.CHAT_INPUT,
|
||||
name: `${cmd.name} ${o.name}`,
|
||||
id: `${o.name}-${cmd.id}`,
|
||||
displayName: `${cmd.name} ${o.name}`,
|
||||
name: getSubCommandName(cmd, o),
|
||||
displayName: getSubCommandName(cmd, o),
|
||||
subCommandPath: [{
|
||||
name: o.name,
|
||||
type: o.type,
|
||||
|
|
@ -128,10 +128,18 @@ function registerSubCommands(cmd: VencordCommand, plugin: string) {
|
|||
}],
|
||||
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) {
|
||||
if (!BUILT_IN) {
|
||||
console.warn(
|
||||
|
|
@ -148,24 +156,30 @@ export function registerCommand<C extends VencordCommand>(command: C, plugin: st
|
|||
command.isVencordCommand = true;
|
||||
command.untranslatedName ??= command.name;
|
||||
command.untranslatedDescription ??= command.description;
|
||||
command.id ??= `-${BUILT_IN.length + commandIdOffset + 1}`;
|
||||
command.id ??= `-${idCounter++}`;
|
||||
command.applicationId ??= "-1"; // BUILT_IN;
|
||||
command.type ??= ApplicationCommandType.CHAT_INPUT;
|
||||
command.inputType ??= ApplicationCommandInputType.BUILT_IN_TEXT;
|
||||
command.plugin ||= plugin;
|
||||
|
||||
prepareOption(command);
|
||||
commands[command.name] = command;
|
||||
|
||||
if (command.options?.[0]?.type === ApplicationCommandOptionType.SUB_COMMAND) {
|
||||
if (isSubCommandParent(command)) {
|
||||
registerSubCommands(command, plugin);
|
||||
return;
|
||||
}
|
||||
|
||||
commands[command.name] = 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);
|
||||
if (idx === -1)
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -9,4 +9,5 @@ export { ApplicationCommandInputType, ApplicationCommandOptionType, ApplicationC
|
|||
|
||||
export interface VencordCommand extends Command {
|
||||
isVencordCommand?: boolean;
|
||||
rootCommand?: VencordCommand;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,14 +75,21 @@ export async function persistNotification(notification: NotificationData) {
|
|||
signals.forEach(x => x());
|
||||
}
|
||||
|
||||
export async function deleteNotification(timestamp: number) {
|
||||
const log = await getLog();
|
||||
const index = log.findIndex(x => x.timestamp === timestamp);
|
||||
if (index === -1) return;
|
||||
export async function deleteNotification(id: string) {
|
||||
let found = false;
|
||||
|
||||
log.splice(index, 1);
|
||||
await DataStore.set(KEY, log);
|
||||
signals.forEach(x => x());
|
||||
await DataStore.update(KEY, (old: PersistentNotificationData[] | undefined) => {
|
||||
const log = old ?? [];
|
||||
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() {
|
||||
|
|
@ -114,7 +121,7 @@ function NotificationEntry({ data }: { data: PersistentNotificationData; }) {
|
|||
if (removing) return;
|
||||
setRemoving(true);
|
||||
|
||||
setTimeout(() => deleteNotification(data.timestamp), 200);
|
||||
setTimeout(() => deleteNotification(data.id), 200);
|
||||
}}
|
||||
richBody={
|
||||
<div className={cl("body-wrapper")}>
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ export interface Settings {
|
|||
plugins: {
|
||||
[plugin: string]: {
|
||||
enabled: boolean;
|
||||
isFavorite?: boolean;
|
||||
[setting: string]: any;
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
import { Settings, SettingsStore, type ThemeActivationMode } from "@api/Settings";
|
||||
import { createAndAppendStyle } from "@utils/css";
|
||||
import { isNonNullish } from "@utils/guards";
|
||||
import { ThemeStore } from "@vencord/discord-types";
|
||||
import { PopoutWindowStore } from "@webpack/common";
|
||||
|
||||
|
|
@ -52,6 +53,9 @@ async function toggle(isEnabled: boolean) {
|
|||
style.disabled = !isEnabled;
|
||||
}
|
||||
|
||||
// for cleanup
|
||||
let previousThemeBlobObjectURLs = [] as string[];
|
||||
|
||||
async function initThemes() {
|
||||
themesStyle ??= createAndAppendStyle("vencord-themes", userStyleRootNode);
|
||||
|
||||
|
|
@ -78,15 +82,22 @@ async function initThemes() {
|
|||
}
|
||||
|
||||
if (IS_WEB) {
|
||||
for (const theme of enabledThemes) {
|
||||
const mode = getThemeActivationMode(theme);
|
||||
if (!shouldApplyTheme(mode, activeTheme)) continue;
|
||||
previousThemeBlobObjectURLs.forEach(url => URL.revokeObjectURL(url));
|
||||
|
||||
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);
|
||||
if (!themeData) continue;
|
||||
if (!themeData) return null;
|
||||
|
||||
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 {
|
||||
const version = Date.now();
|
||||
for (const theme of enabledThemes) {
|
||||
|
|
|
|||
|
|
@ -1224,3 +1224,31 @@ export function DoubleCheckmarkIcon(props: IconProps) {
|
|||
</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,
|
||||
...restProps
|
||||
}: PropsWithChildren<LinkProps>) {
|
||||
|
||||
const isInternal = href && /^(?:discord:\/)?\/[a-zA-Z0-9_-]/.test(href);
|
||||
const needsSafeAttrs = href && !isInternal;
|
||||
|
||||
|
|
|
|||
|
|
@ -17,13 +17,14 @@ export interface QuickActionProps {
|
|||
text: ReactNode;
|
||||
action?: () => void;
|
||||
disabled?: boolean;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export function QuickAction(props: QuickActionProps) {
|
||||
const { Icon, action, text, disabled } = props;
|
||||
const { Icon, action, text, disabled, style } = props;
|
||||
|
||||
return (
|
||||
<button className={cl("pill")} onClick={action} disabled={disabled}>
|
||||
<button className={cl("pill")} onClick={action} disabled={disabled} style={style}>
|
||||
<Icon className={cl("img")} />
|
||||
{text}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ import { Modal, openModal, showToast, useEffect, useMemo, UserProfileStore, useS
|
|||
|
||||
import Plugins, { PluginMeta } from "~plugins";
|
||||
|
||||
import { GithubButton, WebsiteButton } from "./LinkIconButton";
|
||||
import { PluginCard } from "./PluginCard";
|
||||
import { GithubButton, WebsiteButton } from "./PluginModalButtons";
|
||||
|
||||
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 { openContributorModal } from "./ContributorModal";
|
||||
import { GithubButton, WebsiteButton } from "./LinkIconButton";
|
||||
import { FavoriteButton, GithubButton, WebsiteButton } from "./PluginModalButtons";
|
||||
|
||||
const cl = classNameFactory("vc-plugin-modal-");
|
||||
|
||||
|
|
@ -258,6 +258,10 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||
) : <div />}
|
||||
{!pluginMeta.userPlugin && (
|
||||
<div className={cl("links")}>
|
||||
<FavoriteButton
|
||||
isFavorite={pluginSettings.isFavorite ?? false}
|
||||
onClick={() => pluginSettings.isFavorite = !pluginSettings.isFavorite}
|
||||
/>
|
||||
<WebsiteButton
|
||||
text="Website"
|
||||
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 {
|
||||
ALL,
|
||||
FAVORITES,
|
||||
ENABLED,
|
||||
DISABLED,
|
||||
EQUICORD,
|
||||
|
|
@ -170,10 +171,10 @@ export default function PluginSettings() {
|
|||
<p>The following plugins require a restart:</p>
|
||||
<div>
|
||||
{displayed.map((s, i) => (
|
||||
<span key={i}>
|
||||
<React.Fragment key={i}>
|
||||
{i > 0 && ", "}
|
||||
{Parser.parse("`" + s + "`")}
|
||||
</span>
|
||||
</React.Fragment>
|
||||
))}
|
||||
{remainingCount > 0 && <span> and {remainingCount} more</span>}
|
||||
</div>
|
||||
|
|
@ -197,8 +198,11 @@ export default function PluginSettings() {
|
|||
return o;
|
||||
}, []);
|
||||
|
||||
const sortedPlugins = useMemo(() => Object.values(Plugins)
|
||||
.sort((a, b) => a.name.localeCompare(b.name)), []);
|
||||
const sortedPlugins = useMemo(() =>
|
||||
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), []);
|
||||
|
||||
|
|
@ -211,6 +215,9 @@ export default function PluginSettings() {
|
|||
const { status, tags } = searchValue;
|
||||
|
||||
switch (status) {
|
||||
case SearchStatus.FAVORITES:
|
||||
if (!settings.plugins[plugin.name]?.isFavorite) return false;
|
||||
break;
|
||||
case SearchStatus.DISABLED:
|
||||
if (isPluginEnabled(plugin.name)) return false;
|
||||
break;
|
||||
|
|
@ -419,6 +426,7 @@ export default function PluginSettings() {
|
|||
<Select
|
||||
options={[
|
||||
{ label: "Show All", value: SearchStatus.ALL, default: true },
|
||||
{ label: "Show Favorites", value: SearchStatus.FAVORITES },
|
||||
{ label: "Show Enabled", value: SearchStatus.ENABLED },
|
||||
{ label: "Show Disabled", value: SearchStatus.DISABLED },
|
||||
{ label: "Show Equicord", value: SearchStatus.EQUICORD },
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import { debounce } from "@shared/debounce";
|
|||
import { classNameFactory } from "@utils/css";
|
||||
import { copyWithToast, openImageModal } from "@utils/discord";
|
||||
import { classes } from "@utils/misc";
|
||||
import { formatDuration } from "@utils/text";
|
||||
import { ContextMenuApi, FluxDispatcher, Menu, React, useEffect, useState, useStateFromStores } from "@webpack/common";
|
||||
|
||||
import { settings } from "../settings";
|
||||
|
|
@ -34,13 +35,6 @@ import { SpotifyStore, Track } from "./SpotifyStore";
|
|||
|
||||
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) {
|
||||
return () => (
|
||||
<svg
|
||||
|
|
@ -193,7 +187,7 @@ function SpotifySeekBar() {
|
|||
className={cl("progress-time") + " " + cl("time-left")}
|
||||
aria-label="Progress"
|
||||
>
|
||||
{msToHuman(position)}
|
||||
{formatDuration(position)}
|
||||
</Span>
|
||||
<SeekBar
|
||||
initialValue={position}
|
||||
|
|
@ -201,7 +195,7 @@ function SpotifySeekBar() {
|
|||
maxValue={duration}
|
||||
onValueChange={onChange}
|
||||
asValueChanges={onChange}
|
||||
onValueRender={msToHuman}
|
||||
onValueRender={formatDuration}
|
||||
/>
|
||||
<Span
|
||||
size="xs"
|
||||
|
|
@ -209,7 +203,7 @@ function SpotifySeekBar() {
|
|||
className={cl("progress-time") + " " + cl("time-right")}
|
||||
aria-label="Total Duration"
|
||||
>
|
||||
{msToHuman(duration)}
|
||||
{formatDuration(duration)}
|
||||
</Span>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -16,14 +16,16 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import "./ipcMain";
|
||||
|
||||
import { app, net, protocol } from "electron";
|
||||
import { join } from "path";
|
||||
import { pathToFileURL } from "url";
|
||||
|
||||
import { initCsp } from "./csp";
|
||||
import { ensureSafePath } from "./ipcMain";
|
||||
import { RendererSettings } from "./settings";
|
||||
import { IS_VANILLA, THEMES_DIR } from "./utils/constants";
|
||||
import { ensureSafePath } from "./utils/ensureSafePath";
|
||||
import { installExt } from "./utils/extensions";
|
||||
|
||||
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 { open, readdir, readFile, unlink } from "fs/promises";
|
||||
import { release } from "os";
|
||||
import { join, normalize } from "path";
|
||||
import { join } from "path";
|
||||
|
||||
import { registerCspIpcHandlers } from "./csp/manager";
|
||||
import { getThemeInfo, stripBOM, UserThemeHeader } from "./themes";
|
||||
import { ALLOWED_PROTOCOLS, QUICK_CSS_PATH, SETTINGS_DIR, THEMES_DIR } from "./utils/constants";
|
||||
import { ensureSafePath } from "./utils/ensureSafePath";
|
||||
import { makeLinksOpenExternally } from "./utils/externalLinks";
|
||||
|
||||
const RENDERER_CSS_PATH = join(__dirname, "renderer.css");
|
||||
|
|
@ -40,13 +41,6 @@ mkdirSync(THEMES_DIR, { recursive: true });
|
|||
|
||||
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() {
|
||||
return readFile(QUICK_CSS_PATH, "utf-8").catch(() => "");
|
||||
}
|
||||
|
|
@ -86,7 +80,8 @@ ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => {
|
|||
if (!ALLOWED_PROTOCOLS.includes(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());
|
||||
|
|
@ -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_SETTINGS_FOLDER, () => shell.openPath(SETTINGS_DIR));
|
||||
|
||||
let fsWatchers = [] as FSWatcher[];
|
||||
|
||||
ipcMain.handle(IpcEvents.INIT_FILE_WATCHERS, ({ sender }) => {
|
||||
fsWatchers.forEach(w => w.close());
|
||||
|
||||
let quickCssWatcher: 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", () => {
|
||||
quickCssWatcher?.close();
|
||||
themesWatcher.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 { crxToZip } from "./crxToZip";
|
||||
import { ensureSafePath } from "./ensureSafePath";
|
||||
import { fetchBuffer } from "./http";
|
||||
|
||||
const extensionCacheDir = join(DATA_DIR, "ExtensionCache");
|
||||
|
|
@ -33,24 +34,35 @@ async function extract(data: Buffer, outDir: string) {
|
|||
return new Promise<void>((resolve, reject) => {
|
||||
unzip(data, (err, files) => {
|
||||
if (err) return void reject(err);
|
||||
|
||||
Promise.all(Object.keys(files).map(async f => {
|
||||
// Signature stuff
|
||||
// 'Cannot load extension with file or directory name
|
||||
// _metadata. Filenames starting with "_" are reserved for use by the system.';
|
||||
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 name = pathElements.pop()!;
|
||||
const directories = pathElements.join("/");
|
||||
const dir = join(outDir, directories);
|
||||
const directories = pathElements.slice(0, -1).join("/");
|
||||
|
||||
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) {
|
||||
await mkdir(dir, { recursive: true });
|
||||
}
|
||||
|
||||
await writeFile(join(dir, name), files[f]);
|
||||
await writeFile(filePath, files[f]);
|
||||
}))
|
||||
.then(() => resolve())
|
||||
.catch(err => {
|
||||
|
|
@ -62,7 +74,7 @@ async function extract(data: Buffer, outDir: string) {
|
|||
}
|
||||
|
||||
export async function installExt(id: string) {
|
||||
const extDir = join(extensionCacheDir, `${id}`);
|
||||
const extDir = join(extensionCacheDir, id);
|
||||
|
||||
try {
|
||||
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));
|
||||
}
|
||||
|
||||
// Electron 36 Deprecates session.defaultSession.loadExtension()
|
||||
session.defaultSession.extensions ? session.defaultSession.extensions.loadExtension(extDir) : session.defaultSession.loadExtension(extDir);
|
||||
if (session.defaultSession.extensions) {
|
||||
session.defaultSession.extensions.loadExtension(extDir);
|
||||
} else {
|
||||
session.defaultSession.loadExtension(extDir);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,11 @@
|
|||
* 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 { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { UserSettingsProtoStore } from "@webpack/common";
|
||||
|
||||
let savedStatus: string | null;
|
||||
|
||||
|
|
@ -36,36 +37,58 @@ const settings = definePluginSettings({
|
|||
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({
|
||||
name: "AutoDNDWhilePlaying",
|
||||
description: "Automatically updates your online status (online, idle, dnd) when launching games",
|
||||
tags: ["Activity", "Utility"],
|
||||
authors: [Devs.thororen],
|
||||
isModified: true,
|
||||
settings,
|
||||
|
||||
flux: {
|
||||
RUNNING_GAMES_CHANGE({ games }) {
|
||||
async RUNNING_GAMES_CHANGE({ games }) {
|
||||
const status = StatusSettings.getSetting();
|
||||
|
||||
if (settings.store.excludeInvisible && (savedStatus ?? status) === "invisible") return;
|
||||
|
||||
if (games.length > 0) {
|
||||
if (status !== settings.store.statusToSet) {
|
||||
if (status !== settings.store.statusToSet && status !== "invisible") {
|
||||
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 { Devs, EquicordDevs } from "@utils/constants";
|
||||
import { useFixedTimer } from "@utils/react";
|
||||
import { formatDurationMs } from "@utils/text";
|
||||
import { formatDuration } from "@utils/text";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { PassiveUpdateState, VoiceState } from "@vencord/discord-types";
|
||||
import { FluxDispatcher, GuildStore, React, UserStore } from "@webpack/common";
|
||||
|
|
@ -30,7 +30,7 @@ export const settings = definePluginSettings({
|
|||
label: "30d 23h 00m 42s",
|
||||
value: "human"
|
||||
}
|
||||
]
|
||||
] as const
|
||||
},
|
||||
allCallTimers: {
|
||||
type: OptionType.BOOLEAN,
|
||||
|
|
@ -273,7 +273,7 @@ export default definePlugin({
|
|||
|
||||
return (
|
||||
<p style={{ margin: 0, fontFamily: "var(--font-code)" }}>
|
||||
{formatDurationMs(time, settings.store.format === "human")}
|
||||
{formatDuration(time, settings.store.format === "human")}
|
||||
</p>
|
||||
);
|
||||
}, { noop: true }),
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
* 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 ExpressionClonerPlugin from "@plugins/expressionCloner";
|
||||
import { Devs } from "@utils/constants";
|
||||
|
|
@ -68,9 +68,7 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (
|
|||
const sticker = message.stickerItems.find(s => s.id === favoriteableId);
|
||||
if (!sticker?.format_type) return;
|
||||
|
||||
const idx = children.findIndex(c => Array.isArray(c) && findGroupChildrenByChildId("vc-copy-sticker-url", c) != null);
|
||||
|
||||
children.splice(idx, 0, buildMenuItem(sticker, idx !== -1));
|
||||
children.splice(-1, 0, buildMenuItem(sticker, false));
|
||||
};
|
||||
|
||||
const expressionPickerPatch: NavContextMenuPatchCallback = (children, props: { target: HTMLElement; }) => {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
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 { Devs } from "@utils/constants";
|
||||
import { sendMessage } from "@utils/discord";
|
||||
|
|
@ -96,11 +96,12 @@ export default definePlugin({
|
|||
tags: ["Commands", "Customisation", "Utility"],
|
||||
settings,
|
||||
|
||||
async start() {
|
||||
const tags = getTags();
|
||||
for (const tagName in tags) {
|
||||
registerTagCommand(tags[tagName]);
|
||||
}
|
||||
start() {
|
||||
getTags().forEach(registerTagCommand);
|
||||
},
|
||||
|
||||
stop() {
|
||||
getTags().forEach(tag => unregisterCommand(tag.name));
|
||||
},
|
||||
|
||||
commands: [
|
||||
|
|
@ -160,7 +161,7 @@ export default definePlugin({
|
|||
}
|
||||
|
||||
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 ? "..." : ""}`)
|
||||
.join("\n");
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export interface Tag {
|
|||
}
|
||||
|
||||
export function getTags() {
|
||||
return settings.store.tagsList;
|
||||
return Object.values(settings.store.tagsList);
|
||||
}
|
||||
|
||||
export function getTag(name: string) {
|
||||
|
|
|
|||
|
|
@ -139,7 +139,10 @@ function SelectSetting<T>({ settingsKey, label, options, disabled }: SelectOptio
|
|||
options={options}
|
||||
maxVisibleItems={5}
|
||||
closeOnSelect={true}
|
||||
select={v => settings.store[settingsKey] = v}
|
||||
select={v => {
|
||||
settings.store[settingsKey] = v;
|
||||
updateRPC();
|
||||
}}
|
||||
isSelected={v => v === settings.store[settingsKey]}
|
||||
serialize={v => String(v)}
|
||||
isDisabled={disabled}
|
||||
|
|
|
|||
|
|
@ -99,7 +99,6 @@ function DearrowButton({ component }: { component: Component<Props>; }) {
|
|||
className={"vc-dearrow-toggle-" + (embed.dearrow.enabled ? "on" : "off")}
|
||||
onClick={() => {
|
||||
const { enabled, oldThumb, oldTitle } = embed.dearrow;
|
||||
settings.store.dearrowByDefault = !enabled;
|
||||
embed.dearrow.enabled = !enabled;
|
||||
if (oldTitle) {
|
||||
embed.dearrow.oldTitle = embed.rawTitle;
|
||||
|
|
|
|||
|
|
@ -73,6 +73,10 @@ const fakeNitroEmojiRegex = /\/emojis\/(\d+?)\.(png|webp|gif)/;
|
|||
const fakeNitroStickerRegex = /\/stickers\/(\d+?)\./;
|
||||
const fakeNitroGifStickerRegex = /\/attachments\/\d+?\/\d+?\/(\d+?)\.gif/;
|
||||
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({
|
||||
enableEmojiBypass: {
|
||||
|
|
@ -83,9 +87,12 @@ const settings = definePluginSettings({
|
|||
},
|
||||
emojiSize: {
|
||||
description: "Size of the emojis when sending",
|
||||
type: OptionType.SLIDER,
|
||||
default: 48,
|
||||
markers: [32, 48, 56, 64, 96, 128, 160, 256, 512]
|
||||
type: OptionType.SELECT,
|
||||
default: DEFAULT_EMOJI_SIZE,
|
||||
options: mediaSizes.map(size => ({
|
||||
label: `${size}px`,
|
||||
value: size
|
||||
}))
|
||||
},
|
||||
transformEmojis: {
|
||||
description: "Whether to transform fake emojis into real ones",
|
||||
|
|
@ -101,9 +108,12 @@ const settings = definePluginSettings({
|
|||
},
|
||||
stickerSize: {
|
||||
description: "Size of the stickers when sending",
|
||||
type: OptionType.SLIDER,
|
||||
default: 160,
|
||||
markers: [32, 64, 128, 160, 256, 512]
|
||||
type: OptionType.SELECT,
|
||||
default: DEFAULT_STICKER_SIZE,
|
||||
options: mediaSizes.map(size => ({
|
||||
label: `${size}px`,
|
||||
value: size
|
||||
}))
|
||||
},
|
||||
transformStickers: {
|
||||
description: "Whether to transform fake stickers into real ones",
|
||||
|
|
@ -752,7 +762,7 @@ export default definePlugin({
|
|||
.then(parseAPNG);
|
||||
|
||||
const gif = GIFEncoder();
|
||||
const resolution = settings.store.stickerSize;
|
||||
const resolution = settings.store.stickerSize ?? DEFAULT_STICKER_SIZE;
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = resolution;
|
||||
|
|
@ -892,10 +902,11 @@ export default definePlugin({
|
|||
|
||||
hasBypass = true;
|
||||
|
||||
const emojiSize = s.emojiSize ?? DEFAULT_EMOJI_SIZE;
|
||||
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 }));
|
||||
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("lossless", "true");
|
||||
|
||||
|
|
@ -930,8 +941,10 @@ export default definePlugin({
|
|||
|
||||
hasBypass = true;
|
||||
|
||||
const url = new URL(IconUtils.getEmojiURL({ id: emoji.id, animated: emoji.animated, size: s.emojiSize }));
|
||||
url.searchParams.set("size", s.emojiSize.toString());
|
||||
const emojiSize = s.emojiSize ?? DEFAULT_EMOJI_SIZE;
|
||||
|
||||
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("lossless", "true");
|
||||
|
||||
|
|
|
|||
|
|
@ -50,14 +50,14 @@ app.on("browser-window-created", (_, win) => {
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
RendererSettings.addChangeListener("plugins.FixSpotifyEmbeds.volume", newVolume => {
|
||||
try {
|
||||
cleanUpAndGetSpotifyFrames().forEach(frame =>
|
||||
frame.executeJavaScript(`globalThis._vcVolume = ${newVolume / 100}`)
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("FixSpotifyEmbeds: Failed to update volume", e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
RendererSettings.addChangeListener("plugins.FixSpotifyEmbeds.volume", newVolume => {
|
||||
try {
|
||||
cleanUpAndGetSpotifyFrames().forEach(frame =>
|
||||
frame.executeJavaScript(`globalThis._vcVolume = ${newVolume / 100}`)
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("FixSpotifyEmbeds: Failed to update volume", e);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export default definePlugin({
|
|||
sendBotMessage(ctx.channel.id, {
|
||||
content: `
|
||||
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}\`
|
||||
`.trim().replace(/\s+/g, " ")
|
||||
});
|
||||
|
|
@ -56,7 +56,7 @@ export default definePlugin({
|
|||
const friendInviteList = invites.map(i =>
|
||||
`
|
||||
_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}\`
|
||||
`.trim().replace(/\s+/g, " ")
|
||||
);
|
||||
|
|
|
|||
|
|
@ -18,12 +18,28 @@
|
|||
|
||||
import { isPluginEnabled } from "@api/PluginManager";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { UserAreaButton, UserAreaRenderProps } from "@api/UserArea";
|
||||
import { UserAreaRenderProps } from "@api/UserArea";
|
||||
import { getUserSettingLazy } from "@api/UserSettings";
|
||||
import equicordToolbox from "@equicordplugins/equicordToolbox";
|
||||
import { Devs } from "@utils/constants";
|
||||
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")!;
|
||||
|
||||
|
|
@ -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 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;
|
||||
|
||||
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 (
|
||||
<UserAreaButton
|
||||
tooltipText={hideTooltips ? void 0 : showCurrentGame ? "Disable Game Activity" : "Enable Game Activity"}
|
||||
icon={<Icon className={iconForeground} />}
|
||||
role="switch"
|
||||
aria-checked={!showCurrentGame}
|
||||
redGlow={!showCurrentGame}
|
||||
plated={nameplate != null}
|
||||
onClick={() => ShowCurrentGame.updateSetting(old => !old)}
|
||||
/>
|
||||
<Popout
|
||||
position="top"
|
||||
align="left"
|
||||
targetElementRef={buttonRef}
|
||||
renderPopout={({ closePopout }) => (
|
||||
<Menu.Menu navId="vc-gameActivityToggle-menu" onClose={closePopout}>
|
||||
<Menu.MenuCheckboxItem
|
||||
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({
|
||||
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"],
|
||||
authors: [Devs.Nuckyz, Devs.RuukuLada],
|
||||
dependencies: ["UserSettingsAPI", "UserAreaAPI"],
|
||||
|
|
|
|||
|
|
@ -17,13 +17,17 @@ export function MemberCount({ isTooltip, tooltipGuildId }: { isTooltip?: true; t
|
|||
const { voiceActivity } = settings.use(["voiceActivity"]);
|
||||
const includeVoice = voiceActivity && !isTooltip;
|
||||
|
||||
const currentChannel = useStateFromStores([SelectedChannelStore], () => getCurrentChannel());
|
||||
const guildId = isTooltip ? tooltipGuildId! : currentChannel?.guild_id;
|
||||
const currentChannel = useStateFromStores(
|
||||
[SelectedChannelStore], () => isTooltip ? undefined : getCurrentChannel(),
|
||||
[], (a, b) => a?.id === b?.id
|
||||
);
|
||||
|
||||
const guildId = tooltipGuildId ?? currentChannel?.guild_id;
|
||||
|
||||
const voiceActivityCount = useStateFromStores(
|
||||
[VoiceStateStore],
|
||||
() => {
|
||||
if (!includeVoice) return 0;
|
||||
if (!includeVoice || !guildId) return 0;
|
||||
|
||||
const voiceStates = VoiceStateStore.getVoiceStates(guildId);
|
||||
if (!voiceStates) return 0;
|
||||
|
|
@ -31,6 +35,7 @@ export function MemberCount({ isTooltip, tooltipGuildId }: { isTooltip?: true; t
|
|||
return Object.values(voiceStates)
|
||||
.filter(({ channelId }) => {
|
||||
if (!channelId) return false;
|
||||
|
||||
const channel = ChannelStore.getChannel(channelId);
|
||||
return channel && PermissionStore.can(PermissionsBits.VIEW_CHANNEL, channel);
|
||||
})
|
||||
|
|
@ -40,34 +45,57 @@ export function MemberCount({ isTooltip, tooltipGuildId }: { isTooltip?: true; t
|
|||
|
||||
const totalCount = useStateFromStores(
|
||||
[GuildMemberCountStore],
|
||||
() => GuildMemberCountStore.getMemberCount(guildId!)
|
||||
() => guildId ? GuildMemberCountStore.getMemberCount(guildId) : null
|
||||
);
|
||||
|
||||
let onlineCount = useStateFromStores(
|
||||
[OnlineMemberCountStore],
|
||||
() => OnlineMemberCountStore.getCount(guildId)
|
||||
() => guildId ? OnlineMemberCountStore.getCount(guildId) : null
|
||||
);
|
||||
|
||||
const { groups } = useStateFromStores(
|
||||
const memberListOnlineCount = useStateFromStores(
|
||||
[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.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")) {
|
||||
onlineCount = groups.reduce((total, curr) => total + (curr.id === "offline" ? 0 : curr.count), 0);
|
||||
}
|
||||
|
||||
if (!isTooltip && threadGroups && !isObjectEmpty(threadGroups)) {
|
||||
onlineCount = Object.values(threadGroups).reduce((total, curr) => total + (curr.sectionId === "offline" ? 0 : curr.userIds.length), 0);
|
||||
}
|
||||
if (memberListOnlineCount != null) onlineCount = memberListOnlineCount;
|
||||
if (threadListOnlineCount != null) onlineCount = threadListOnlineCount;
|
||||
|
||||
useEffect(() => {
|
||||
OnlineMemberCountStore.ensureCount(guildId);
|
||||
if (guildId) {
|
||||
OnlineMemberCountStore.ensureCount(guildId);
|
||||
}
|
||||
}, [guildId]);
|
||||
|
||||
if (totalCount == null)
|
||||
|
|
@ -86,6 +114,7 @@ export function MemberCount({ isTooltip, tooltipGuildId }: { isTooltip?: true; t
|
|||
</div>
|
||||
)}
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip text={`${numberFormat(totalCount)} total server members`} position="bottom">
|
||||
{props => (
|
||||
<div {...props} className={cl("container")}>
|
||||
|
|
@ -94,6 +123,7 @@ export function MemberCount({ isTooltip, tooltipGuildId }: { isTooltip?: true; t
|
|||
</div>
|
||||
)}
|
||||
</Tooltip>
|
||||
|
||||
{includeVoice && voiceActivityCount > 0 &&
|
||||
<Tooltip text={`${formattedVoiceCount} members in voice`} position="bottom">
|
||||
{props => (
|
||||
|
|
|
|||
|
|
@ -65,16 +65,44 @@ export default definePlugin({
|
|||
|
||||
videoClone.loop = settings.store.loop;
|
||||
videoClone.style.display = "none";
|
||||
videoClone.onleavepictureinpicture = () => videoClone.remove();
|
||||
|
||||
function launchPiP() {
|
||||
videoClone.currentTime = video.currentTime;
|
||||
videoClone.requestPictureInPicture();
|
||||
video.pause();
|
||||
videoClone.play();
|
||||
let cleaned = false;
|
||||
function cleanup() {
|
||||
if (cleaned) return;
|
||||
cleaned = true;
|
||||
|
||||
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();
|
||||
else
|
||||
videoClone.onloadedmetadata = launchPiP;
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ import definePlugin, { IconComponent, StartAt } from "@utils/types";
|
|||
import { CloudUpload, MessageAttachment } from "@vencord/discord-types";
|
||||
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 getImageBox = (url: string): Promise<{ width: number, height: number; } | null> =>
|
||||
|
|
@ -42,6 +44,8 @@ const getAttachments = async (channelId: string) =>
|
|||
UploadAttachmentStore.getUploads(channelId, DraftType.ChannelMessage)
|
||||
.map(async (upload: CloudUpload) => {
|
||||
const { isImage, filename, spoiler, item: { file } } = upload;
|
||||
|
||||
// FIXME: revoke object url to fix memory leak
|
||||
const url = URL.createObjectURL(file);
|
||||
const attachment: MessageAttachment = {
|
||||
id: generateId(),
|
||||
|
|
@ -57,13 +61,13 @@ const getAttachments = async (channelId: string) =>
|
|||
|
||||
if (isImage) {
|
||||
const box = await getImageBox(url);
|
||||
if (!box) return attachment;
|
||||
|
||||
attachment.width = box.width;
|
||||
attachment.height = box.height;
|
||||
if (box) {
|
||||
attachment.width = box.width;
|
||||
attachment.height = box.height;
|
||||
}
|
||||
}
|
||||
|
||||
return attachment;
|
||||
return { attachment, objectURL: url };
|
||||
})
|
||||
);
|
||||
|
||||
|
|
@ -96,15 +100,20 @@ const PreviewButton: ChatBarButtonFactory = ({ isAnyChat, isEmpty, type: { attac
|
|||
return (
|
||||
<ChatBarButton
|
||||
tooltip="Preview Message"
|
||||
onClick={async () =>
|
||||
sendBotMessage(
|
||||
onClick={async () => {
|
||||
const attachments = hasAttachments ? await getAttachments(channelId) : undefined;
|
||||
const message = sendBotMessage(
|
||||
channelId,
|
||||
{
|
||||
content: getDraft(channelId),
|
||||
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={{
|
||||
style: {
|
||||
translate: "0 2px"
|
||||
|
|
@ -130,5 +139,15 @@ export default definePlugin({
|
|||
chatBarButton: {
|
||||
icon: PreviewIcon,
|
||||
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))}...
|
||||
<br />
|
||||
<a onClick={() => setShowAll(true)}>Read more</a>]
|
||||
<a onClick={() => setShowAll(true)}>Read more</a>
|
||||
</>
|
||||
)
|
||||
: Parser.parseGuildEventDescription(review.comment)}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import PermissionsViewerPlugin from "@plugins/permissionsViewer";
|
|||
import openRolesAndUsersPermissionsModal from "@plugins/permissionsViewer/components/RolesAndUsersPermissions";
|
||||
import { sortPermissionOverwrites } from "@plugins/permissionsViewer/utils";
|
||||
import { classes } from "@utils/misc";
|
||||
import { formatDuration } from "@utils/text";
|
||||
import { formatDurationVerbose } from "@utils/text";
|
||||
import type { Channel, RoleOrUserPermission } from "@vencord/discord-types";
|
||||
import { findByPropsLazy, findComponentByCodeLazy, findCssClassesLazy } from "@webpack";
|
||||
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>
|
||||
}
|
||||
{(rateLimitPerUser ?? 0) > 0 &&
|
||||
<BaseText size="md">
|
||||
Slowmode: {formatDuration(rateLimitPerUser!, "seconds")}
|
||||
</BaseText>
|
||||
<BaseText size="md">Slowmode: {formatDurationVerbose(rateLimitPerUser!, "seconds")}</BaseText>
|
||||
}
|
||||
{(defaultThreadRateLimitPerUser ?? 0) > 0 &&
|
||||
<BaseText size="md">
|
||||
Default thread slowmode: {formatDuration(defaultThreadRateLimitPerUser!, "seconds")}
|
||||
Default thread slowmode: {formatDurationVerbose(defaultThreadRateLimitPerUser!, "seconds")}
|
||||
</BaseText>
|
||||
}
|
||||
{((channel.isGuildVoice() || channel.isGuildStageVoice()) && bitrate != null) &&
|
||||
|
|
@ -232,7 +230,7 @@ function HiddenChannelLockScreen({ channel }: { channel: Channel; }) {
|
|||
{(defaultAutoArchiveDuration ?? 0) > 0 &&
|
||||
<BaseText size="md">
|
||||
Default inactivity duration before archiving {channel.isForumChannel() ? "posts" : "threads"}:
|
||||
{" " + formatDuration(defaultAutoArchiveDuration!, "minutes")}
|
||||
{" " + formatDurationVerbose(defaultAutoArchiveDuration!, "minutes")}
|
||||
</BaseText>
|
||||
}
|
||||
{defaultForumLayout != null &&
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (
|
|||
|
||||
const menuGroup = findGroupChildrenByChildId("delete", children);
|
||||
const deleteIndex = menuGroup?.findIndex(i => i?.props?.id === "delete");
|
||||
if (!deleteIndex || !menuGroup) return;
|
||||
if (deleteIndex == null || !menuGroup) return;
|
||||
|
||||
menuGroup.splice(deleteIndex - 1, 0, (
|
||||
<Menu.MenuItem
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import { openImageModal } from "@utils/discord";
|
|||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import type { Channel, Guild, User } from "@vencord/discord-types";
|
||||
import { GuildMemberStore, IconUtils, Menu } from "@webpack/common";
|
||||
import type { MouseEvent } from "react";
|
||||
|
||||
interface UserContextProps {
|
||||
channel: Channel;
|
||||
|
|
@ -67,10 +68,18 @@ const settings = definePluginSettings({
|
|||
}
|
||||
});
|
||||
|
||||
const openAvatar = (url: string) => openImage(url, 512, 512);
|
||||
const openBanner = (url: string) => openImage(url, 1024);
|
||||
const openAvatar = (url: string, event?: MouseEvent) => openImage({ url, width: 512, height: 512, event });
|
||||
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 format = url.startsWith("/")
|
||||
|
|
@ -214,8 +223,8 @@ export default definePlugin({
|
|||
{
|
||||
find: "return{avatarProps:{",
|
||||
replacement: {
|
||||
match: /(?<=avatarProps:(\i),eventHandlers:\i.{0,150}?)return null==(?<=onOpenAvatar:(\i).+?)/,
|
||||
replace: "$2&&=$self.openAvatar.bind(undefined,$1.src);$&",
|
||||
match: /(?<=onClick:.{0,30}?)null!=(\i)(?=.{0,200}children:.{0,50}...(\i),imageClassName:)/,
|
||||
replace: "null!=($1&&=$self.openAvatar.bind(undefined,$2.src))",
|
||||
}
|
||||
},
|
||||
// Banners
|
||||
|
|
@ -223,7 +232,7 @@ export default definePlugin({
|
|||
find: 'backgroundColor:"COMPLETE"',
|
||||
replacement: {
|
||||
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
|
||||
|
|
@ -232,7 +241,7 @@ export default definePlugin({
|
|||
replacement: {
|
||||
match: /null==\i\.icon\?.+?src:(\(0,\i\.\i\).+?\))(?=[,}])/,
|
||||
// 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
|
||||
|
|
@ -240,7 +249,7 @@ export default definePlugin({
|
|||
find: ".channel.getRecipientId(),",
|
||||
replacement: {
|
||||
match: /(?=,src:(\i.getAvatarURL\(.+?[)]))/,
|
||||
replace: (_, avatarUrl) => `,onClick:()=>$self.openAvatar(${avatarUrl})`
|
||||
replace: (_, avatarUrl) => `,onClick:vcEvent=>$self.openAvatar(${avatarUrl},vcEvent)`
|
||||
}
|
||||
},
|
||||
// User Dms top large icon
|
||||
|
|
@ -248,7 +257,7 @@ export default definePlugin({
|
|||
find: ".EMPTY_GROUP_DM)",
|
||||
replacement: {
|
||||
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 {
|
||||
recorder?.stop();
|
||||
recorder?.stream.getTracks().forEach(track => track.stop());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -303,6 +303,7 @@ export default definePlugin({
|
|||
|
||||
stop() {
|
||||
socket.close();
|
||||
Native.closeSocket();
|
||||
},
|
||||
|
||||
settingsAboutComponent: () => (
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { createSocket, Socket } from "dgram";
|
||||
|
||||
let xsoSocket: Socket;
|
||||
let xsoSocket: Socket | null = null;
|
||||
|
||||
export function sendToOverlay(_, data: any) {
|
||||
data.messageType = data.type;
|
||||
|
|
@ -14,3 +14,8 @@ export function sendToOverlay(_, data: any) {
|
|||
xsoSocket ??= createSocket("udp4");
|
||||
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",
|
||||
id: 383365021415243776n
|
||||
},
|
||||
jax: {
|
||||
name: "jax",
|
||||
id: 1493703027801194598n
|
||||
},
|
||||
yuna0x0: {
|
||||
name: "yuna0x0",
|
||||
id: 213656926414831616n
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ export function useAwaiter<T>(factory: () => Promise<T>, providedOpts?: AwaiterO
|
|||
})
|
||||
.catch(error => {
|
||||
if (!isAlive) return;
|
||||
setState({ value: null, error, pending: false });
|
||||
setState({ value: opts.fallbackValue, error, pending: false });
|
||||
opts.onError?.(error);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { isTruthy } from "./guards";
|
||||
|
||||
// Utils for readable text transformations eg: `toTitle(fromKebab())`
|
||||
|
||||
// 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 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 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)}`;
|
||||
}
|
||||
|
||||
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,
|
||||
* 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("(");
|
||||
|
||||
// 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 patchedSource = code;
|
||||
let patchedCode = "0," + (!isArrowFunction ? "function" : "") + originalFactoryCode.slice(originalFactoryCode.indexOf("("));
|
||||
let patchedSource = patchedCode;
|
||||
let patchedFactory = originalFactory;
|
||||
|
||||
const patchedBy = new Set<string>();
|
||||
|
|
@ -521,58 +521,59 @@ function patchFactory(moduleId: PropertyKey, originalFactory: AnyModuleFactory):
|
|||
|
||||
if (
|
||||
shouldCheckBuildNumber &&
|
||||
(patch.fromBuild != null && buildNumber < patch.fromBuild) ||
|
||||
(patch.toBuild != null && buildNumber > patch.toBuild)
|
||||
((patch.fromBuild != null && buildNumber < patch.fromBuild) || (patch.toBuild != null && buildNumber > patch.toBuild))
|
||||
) {
|
||||
patches.splice(i--, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
const moduleMatches = typeof patch.find === "string"
|
||||
? code.includes(patch.find)
|
||||
: (patch.find.global && (patch.find.lastIndex = 0), patch.find.test(code));
|
||||
? originalFactoryCode.includes(patch.find)
|
||||
: (patch.find.global && (patch.find.lastIndex = 0), patch.find.test(originalFactoryCode));
|
||||
|
||||
if (!moduleMatches) {
|
||||
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) => {
|
||||
if (typeof match !== "string" && match.global) {
|
||||
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
|
||||
for (const replacement of patch.replacement as PatchReplacement[]) {
|
||||
if (
|
||||
shouldCheckBuildNumber &&
|
||||
(replacement.fromBuild != null && buildNumber < replacement.fromBuild) ||
|
||||
(replacement.toBuild != null && buildNumber > replacement.toBuild)
|
||||
((replacement.fromBuild != null && buildNumber < replacement.fromBuild) || (replacement.toBuild != null && buildNumber > replacement.toBuild))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const lastCode = code;
|
||||
const lastFactory = originalFactory;
|
||||
|
||||
let newPatchedCode: string = "";
|
||||
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) {
|
||||
patchTimings.push([patch.plugin, moduleId, replacement.match, totalTime]);
|
||||
}
|
||||
|
||||
if (newCode === code) {
|
||||
if (newPatchedCode === patchedCode) {
|
||||
if (!(patch.noWarn || replacement.noWarn)) {
|
||||
logger.warn(`Patch by ${patch.plugin} had no effect (Module id is ${String(moduleId)}): ${replacement.match}`);
|
||||
if (IS_DEV) {
|
||||
logger.debug("Function Source:\n", code);
|
||||
logger.debug("Function Source:\n", patchedCode);
|
||||
}
|
||||
if (IS_COMPANION_TEST)
|
||||
reporterData.failedPatches.hadNoEffect.push({
|
||||
|
|
@ -583,8 +584,6 @@ function patchFactory(moduleId: PropertyKey, originalFactory: AnyModuleFactory):
|
|||
|
||||
if (patch.group) {
|
||||
logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} had no effect`);
|
||||
code = previousCode;
|
||||
patchedFactory = previousFactory;
|
||||
|
||||
if (markedAsPatched) {
|
||||
patchedBy.delete(patch.plugin);
|
||||
|
|
@ -596,6 +595,8 @@ function patchFactory(moduleId: PropertyKey, originalFactory: AnyModuleFactory):
|
|||
id: moduleId
|
||||
});
|
||||
|
||||
|
||||
shouldRestorePrevious = true;
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -607,14 +608,17 @@ function patchFactory(moduleId: PropertyKey, originalFactory: AnyModuleFactory):
|
|||
pluginsList.push(patch.plugin);
|
||||
}
|
||||
|
||||
code = newCode;
|
||||
patchedSource = `// Webpack Module ${String(moduleId)} - Patched by ${pluginsList.join(", ")}\n${code}\n//# sourceURL=file:///WebpackModule${String(moduleId)}`;
|
||||
patchedFactory = (0, eval)(patchedSource);
|
||||
const newPatchedSource = `// Webpack Module ${String(moduleId)} - Patched by ${pluginsList.join(", ")}\n${newPatchedCode}\n//# sourceURL=file:///WebpackModule${String(moduleId)}`;
|
||||
const newPatchedFactory = (0, eval)(newPatchedSource);
|
||||
|
||||
if (!patchedBy.has(patch.plugin)) {
|
||||
patchedBy.add(patch.plugin);
|
||||
markedAsPatched = true;
|
||||
}
|
||||
|
||||
patchedCode = newPatchedCode;
|
||||
patchedSource = newPatchedSource;
|
||||
patchedFactory = newPatchedFactory;
|
||||
} catch (err) {
|
||||
// FIXME: Maybe fix this properly
|
||||
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)
|
||||
reporterData.failedPatches.erroredPatch.push({
|
||||
...patch,
|
||||
oldModule: lastCode,
|
||||
newModule: code,
|
||||
oldModule: patchedCode,
|
||||
newModule: newPatchedCode,
|
||||
id: moduleId
|
||||
});
|
||||
|
||||
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) {
|
||||
logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} errored`);
|
||||
if (IS_COMPANION_TEST)
|
||||
|
|
@ -645,13 +645,19 @@ function patchFactory(moduleId: PropertyKey, originalFactory: AnyModuleFactory):
|
|||
...patch,
|
||||
id: moduleId
|
||||
});
|
||||
code = previousCode;
|
||||
patchedFactory = previousFactory;
|
||||
shouldRestorePrevious = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
code = lastCode;
|
||||
patchedFactory = lastFactory;
|
||||
if (shouldRestorePrevious) {
|
||||
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) {
|
||||
const changeSize = code.length - lastCode.length;
|
||||
|
||||
const matchIndex = match?.index ?? 0;
|
||||
const matchLength = match?.[0]?.length ?? 0;
|
||||
|
||||
// Use 200 surrounding characters of context
|
||||
const start = Math.max(0, match.index! - 200);
|
||||
const end = Math.min(lastCode.length, match.index! + match[0].length + 200);
|
||||
const start = Math.max(0, matchIndex - 200);
|
||||
const end = Math.min(lastCode.length, matchIndex + matchLength + 200);
|
||||
// (changeSize may be negative)
|
||||
const endPatched = end + changeSize;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue