diff --git a/package.json b/package.json index 6d48e6042..530aef1c0 100644 --- a/package.json +++ b/package.json @@ -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" } diff --git a/scripts/build/buildWeb.mjs b/scripts/build/buildWeb.mjs index e673a1c82..a5043d43d 100644 --- a/scripts/build/buildWeb.mjs +++ b/scripts/build/buildWeb.mjs @@ -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} diff --git a/src/api/Badges.ts b/src/api/Badges.ts index 88e3c9bd9..353c6e37b 100644 --- a/src/api/Badges.ts +++ b/src/api/Badges.ts @@ -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 */ diff --git a/src/api/Commands/index.ts b/src/api/Commands/index.ts index b609fe5bf..9187c46d3 100644 --- a/src/api/Commands/index.ts +++ b/src/api/Commands/index.ts @@ -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(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(command: C, plugin: string) { if (!BUILT_IN) { console.warn( @@ -148,24 +156,30 @@ export function registerCommand(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; diff --git a/src/api/Commands/types.ts b/src/api/Commands/types.ts index c5ecb35ec..dd238a296 100644 --- a/src/api/Commands/types.ts +++ b/src/api/Commands/types.ts @@ -9,4 +9,5 @@ export { ApplicationCommandInputType, ApplicationCommandOptionType, ApplicationC export interface VencordCommand extends Command { isVencordCommand?: boolean; + rootCommand?: VencordCommand; } diff --git a/src/api/Notifications/notificationLog.tsx b/src/api/Notifications/notificationLog.tsx index aea58f4dc..141e4efec 100644 --- a/src/api/Notifications/notificationLog.tsx +++ b/src/api/Notifications/notificationLog.tsx @@ -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={
diff --git a/src/api/Settings.ts b/src/api/Settings.ts index 069b13929..32c2ea42b 100644 --- a/src/api/Settings.ts +++ b/src/api/Settings.ts @@ -76,6 +76,7 @@ export interface Settings { plugins: { [plugin: string]: { enabled: boolean; + isFavorite?: boolean; [setting: string]: any; }; }; diff --git a/src/api/Themes.ts b/src/api/Themes.ts index b4f41cbf9..92fab6c05 100644 --- a/src/api/Themes.ts +++ b/src/api/Themes.ts @@ -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) { diff --git a/src/components/Icons.tsx b/src/components/Icons.tsx index 6ee8523d1..852609963 100644 --- a/src/components/Icons.tsx +++ b/src/components/Icons.tsx @@ -1224,3 +1224,31 @@ export function DoubleCheckmarkIcon(props: IconProps) { ); } + +export function StarFilled(props: IconProps) { + return ( + + + + ); +} + +export function StarOutlined(props: IconProps) { + return ( + + + + ); +} diff --git a/src/components/Link.tsx b/src/components/Link.tsx index df82de115..9220474f0 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -24,7 +24,6 @@ export function Link({ children, ...restProps }: PropsWithChildren) { - const isInternal = href && /^(?:discord:\/)?\/[a-zA-Z0-9_-]/.test(href); const needsSafeAttrs = href && !isInternal; diff --git a/src/components/settings/QuickAction.tsx b/src/components/settings/QuickAction.tsx index e9cc1f86b..f97126f40 100644 --- a/src/components/settings/QuickAction.tsx +++ b/src/components/settings/QuickAction.tsx @@ -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 ( - diff --git a/src/components/settings/tabs/plugins/ContributorModal.tsx b/src/components/settings/tabs/plugins/ContributorModal.tsx index 273a27eef..685eaf929 100644 --- a/src/components/settings/tabs/plugins/ContributorModal.tsx +++ b/src/components/settings/tabs/plugins/ContributorModal.tsx @@ -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-"); diff --git a/src/components/settings/tabs/plugins/LinkIconButton.tsx b/src/components/settings/tabs/plugins/LinkIconButton.tsx deleted file mode 100644 index d6b725f3c..000000000 --- a/src/components/settings/tabs/plugins/LinkIconButton.tsx +++ /dev/null @@ -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 ( - - ); -} - -export function GithubButton({ text, href }: Props) { - return ( - - ); -} diff --git a/src/components/settings/tabs/plugins/PluginModal.tsx b/src/components/settings/tabs/plugins/PluginModal.tsx index 5d397afb5..2feb27a93 100644 --- a/src/components/settings/tabs/plugins/PluginModal.tsx +++ b/src/components/settings/tabs/plugins/PluginModal.tsx @@ -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 ) :
} {!pluginMeta.userPlugin && (
+ pluginSettings.isFavorite = !pluginSettings.isFavorite} + /> getTheme() !== Theme.Light ? "dark" : "light"; + +interface Props { + text: string; + href: string; +} + +function LinkIcon({ text, href, Icon }: Props & { Icon: React.ComponentType; }) { + return ( + + {props => + + + + } + + ); +} + +const GithubLinkIcon = () => ; +const WebsiteLinkIcon = () => ; + +export const GithubButton = (props: Props) => ; +export const WebsiteButton = (props: Props) => ; + +export function FavoriteButton({ isFavorite, onClick }: { isFavorite: boolean; onClick: () => void; }) { + const Icon = isFavorite ? StarFilled : StarOutlined; + + return ( + + {props => + + + + } + + ); +} diff --git a/src/components/settings/tabs/plugins/index.tsx b/src/components/settings/tabs/plugins/index.tsx index b8689f0e6..f4d4564f3 100644 --- a/src/components/settings/tabs/plugins/index.tsx +++ b/src/components/settings/tabs/plugins/index.tsx @@ -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() {

The following plugins require a restart:

{displayed.map((s, i) => ( - + {i > 0 && ", "} {Parser.parse("`" + s + "`")} - + ))} {remainingCount > 0 && and {remainingCount} more}
@@ -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() {