Fix ViewIcons & plugins that use image modals

Co-Authored-By: sadan <117494111+sadan4@users.noreply.github.com>
This commit is contained in:
Vendicated 2024-10-23 03:57:46 +02:00
parent 58c3032bb2
commit a11ccde40f
No known key found for this signature in database
GPG key ID: D66986BAF75ECF18
9 changed files with 114 additions and 54 deletions

View file

@ -0,0 +1,24 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "DynamicImageModalAPI",
authors: [Devs.sadan, Devs.Nuckyz],
description: "Allows you to omit either width or height when opening an image modal",
patches: [
{
find: "SCALE_DOWN:",
replacement: {
match: /!\(null==(\i)\|\|0===\i\|\|null==(\i)\|\|0===\i\)/,
replace: (_, width, height) => `!((null == ${width} || 0 === ${width}) && (null == ${height} || 0 === ${height}))`
}
}
]
});

View file

@ -99,7 +99,11 @@ export default definePlugin({
id="vc-view-role-icon" id="vc-view-role-icon"
label="View Role Icon" label="View Role Icon"
action={() => { action={() => {
openImageModal(`${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/role-icons/${role.id}/${role.icon}.${settings.store.roleIconFileFormat}`); openImageModal({
url: `${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/role-icons/${role.id}/${role.icon}.${settings.store.roleIconFileFormat}`,
height: 128,
width: 128
});
}} }}
icon={ImageIcon} icon={ImageIcon}
/> />

View file

@ -57,7 +57,11 @@ export const handleViewPreview = async ({ guildId, channelId, ownerId }: Applica
const previewUrl = await ApplicationStreamPreviewStore.getPreviewURL(guildId, channelId, ownerId); const previewUrl = await ApplicationStreamPreviewStore.getPreviewURL(guildId, channelId, ownerId);
if (!previewUrl) return; if (!previewUrl) return;
openImageModal(previewUrl); openImageModal({
url: previewUrl,
height: 720,
width: 1280
});
}; };
export const addViewStreamContext: NavContextMenuPatchCallback = (children, { userId }: { userId: string | bigint; }) => { export const addViewStreamContext: NavContextMenuPatchCallback = (children, { userId }: { userId: string | bigint; }) => {

View file

@ -80,7 +80,10 @@ function GuildInfoModal({ guild }: GuildProps) {
className={cl("banner")} className={cl("banner")}
src={bannerUrl} src={bannerUrl}
alt="" alt=""
onClick={() => openImageModal(bannerUrl)} onClick={() => openImageModal({
url: bannerUrl,
width: 1024
})}
/> />
)} )}
@ -89,7 +92,11 @@ function GuildInfoModal({ guild }: GuildProps) {
? <img ? <img
src={iconUrl} src={iconUrl}
alt="" alt=""
onClick={() => openImageModal(iconUrl)} onClick={() => openImageModal({
url: iconUrl,
height: 512,
width: 512,
})}
/> />
: <div aria-hidden className={classes(IconClasses.childWrapper, IconClasses.acronym)}>{guild.acronym}</div> : <div aria-hidden className={classes(IconClasses.childWrapper, IconClasses.acronym)}>{guild.acronym}</div>
} }
@ -151,7 +158,15 @@ function Owner(guildId: string, owner: User) {
return ( return (
<div className={cl("owner")}> <div className={cl("owner")}>
<img src={ownerAvatarUrl} alt="" onClick={() => openImageModal(ownerAvatarUrl)} /> <img
src={ownerAvatarUrl}
alt=""
onClick={() => openImageModal({
url: ownerAvatarUrl,
height: 512,
width: 512
})}
/>
{Parser.parse(`<@${owner.id}>`)} {Parser.parse(`<@${owner.id}>`)}
</div> </div>
); );

View file

@ -229,7 +229,11 @@ function AlbumContextMenu({ track }: { track: Track; }) {
id="view-cover" id="view-cover"
label="View Album Cover" label="View Album Cover"
// trolley // trolley
action={() => openImageModal(track.album.image.url)} action={() => openImageModal({
url: track.album.image.url,
height: 512,
width: 512
})}
icon={ImageIcon} icon={ImageIcon}
/> />
<Menu.MenuControlItem <Menu.MenuControlItem

View file

@ -67,7 +67,10 @@ const settings = definePluginSettings({
} }
}); });
function openImage(url: string) { const openAvatar = (url: string) => openImage(url, 512, 512);
const openBanner = (url: string) => openImage(url, 1024);
function openImage(url: string, width: number, height?: number) {
const format = url.startsWith("/") ? "png" : settings.store.format; const format = url.startsWith("/") ? "png" : settings.store.format;
const u = new URL(url, window.location.href); const u = new URL(url, window.location.href);
@ -76,11 +79,13 @@ function openImage(url: string) {
url = u.toString(); url = u.toString();
u.searchParams.set("size", "4096"); u.searchParams.set("size", "4096");
const originalUrl = u.toString(); const original = u.toString();
openImageModal(url, { openImageModal({
original: originalUrl, url,
height: 256 original,
width,
height
}); });
} }
@ -93,14 +98,14 @@ const UserContext: NavContextMenuPatchCallback = (children, { user, guildId }: U
<Menu.MenuItem <Menu.MenuItem
id="view-avatar" id="view-avatar"
label="View Avatar" label="View Avatar"
action={() => openImage(IconUtils.getUserAvatarURL(user, true))} action={() => openAvatar(IconUtils.getUserAvatarURL(user, true))}
icon={ImageIcon} icon={ImageIcon}
/> />
{memberAvatar && ( {memberAvatar && (
<Menu.MenuItem <Menu.MenuItem
id="view-server-avatar" id="view-server-avatar"
label="View Server Avatar" label="View Server Avatar"
action={() => openImage(IconUtils.getGuildMemberAvatarURLSimple({ action={() => openAvatar(IconUtils.getGuildMemberAvatarURLSimple({
userId: user.id, userId: user.id,
avatar: memberAvatar, avatar: memberAvatar,
guildId: guildId!, guildId: guildId!,
@ -126,7 +131,7 @@ const GuildContext: NavContextMenuPatchCallback = (children, { guild }: GuildCon
id="view-icon" id="view-icon"
label="View Icon" label="View Icon"
action={() => action={() =>
openImage(IconUtils.getGuildIconURL({ openAvatar(IconUtils.getGuildIconURL({
id, id,
icon, icon,
canAnimate: true canAnimate: true
@ -140,7 +145,7 @@ const GuildContext: NavContextMenuPatchCallback = (children, { guild }: GuildCon
id="view-banner" id="view-banner"
label="View Banner" label="View Banner"
action={() => action={() =>
openImage(IconUtils.getGuildBannerURL(guild, true)!) openBanner(IconUtils.getGuildBannerURL(guild, true)!)
} }
icon={ImageIcon} icon={ImageIcon}
/> />
@ -158,7 +163,7 @@ const GroupDMContext: NavContextMenuPatchCallback = (children, { channel }: Grou
id="view-group-channel-icon" id="view-group-channel-icon"
label="View Icon" label="View Icon"
action={() => action={() =>
openImage(IconUtils.getChannelIconURL(channel)!) openAvatar(IconUtils.getChannelIconURL(channel)!)
} }
icon={ImageIcon} icon={ImageIcon}
/> />
@ -171,10 +176,12 @@ export default definePlugin({
authors: [Devs.Ven, Devs.TheKodeToad, Devs.Nuckyz, Devs.nyx], authors: [Devs.Ven, Devs.TheKodeToad, Devs.Nuckyz, Devs.nyx],
description: "Makes avatars and banners in user profiles clickable, adds View Icon/Banner entries in the user, server and group channel context menu.", description: "Makes avatars and banners in user profiles clickable, adds View Icon/Banner entries in the user, server and group channel context menu.",
tags: ["ImageUtilities"], tags: ["ImageUtilities"],
dependencies: ["DynamicImageModalAPI"],
settings, settings,
openImage, openAvatar,
openBanner,
contextMenus: { contextMenus: {
"user-context": UserContext, "user-context": UserContext,
@ -188,7 +195,7 @@ export default definePlugin({
find: ".overlay:void 0,status:", find: ".overlay:void 0,status:",
replacement: { replacement: {
match: /avatarSrc:(\i),eventHandlers:(\i).+?"div",{...\2,/, match: /avatarSrc:(\i),eventHandlers:(\i).+?"div",{...\2,/,
replace: "$&style:{cursor:\"pointer\"},onClick:()=>{$self.openImage($1)}," replace: "$&style:{cursor:\"pointer\"},onClick:()=>{$self.openAvatar($1)},"
}, },
all: true all: true
}, },
@ -197,7 +204,7 @@ export default definePlugin({
find: 'backgroundColor:"COMPLETE"', find: 'backgroundColor:"COMPLETE"',
replacement: { replacement: {
match: /(\.banner,.+?),style:{(?=.+?backgroundImage:null!=(\i)\?"url\("\.concat\(\2,)/, match: /(\.banner,.+?),style:{(?=.+?backgroundImage:null!=(\i)\?"url\("\.concat\(\2,)/,
replace: (_, rest, bannerSrc) => `${rest},onClick:()=>${bannerSrc}!=null&&$self.openImage(${bannerSrc}),style:{cursor:${bannerSrc}!=null?"pointer":void 0,` replace: (_, rest, bannerSrc) => `${rest},onClick:()=>${bannerSrc}!=null&&$self.openBanner(${bannerSrc}),style:{cursor:${bannerSrc}!=null?"pointer":void 0,`
} }
}, },
// Group DMs top small & large icon // Group DMs top small & large icon
@ -205,7 +212,7 @@ export default definePlugin({
find: /\.recipients\.length>=2(?!<isMultiUserDM.{0,50})/, find: /\.recipients\.length>=2(?!<isMultiUserDM.{0,50})/,
replacement: { replacement: {
match: /null==\i\.icon\?.+?src:(\(0,\i\.\i\).+?\))(?=[,}])/, match: /null==\i\.icon\?.+?src:(\(0,\i\.\i\).+?\))(?=[,}])/,
replace: (m, iconUrl) => `${m},onClick:()=>$self.openImage(${iconUrl})` replace: (m, iconUrl) => `${m},onClick:()=>$self.openAvatar(${iconUrl})`
} }
}, },
// User DMs top small icon // User DMs top small icon
@ -213,7 +220,7 @@ export default definePlugin({
find: ".cursorPointer:null,children", find: ".cursorPointer:null,children",
replacement: { replacement: {
match: /.Avatar,.+?src:(.+?\))(?=[,}])/, match: /.Avatar,.+?src:(.+?\))(?=[,}])/,
replace: (m, avatarUrl) => `${m},onClick:()=>$self.openImage(${avatarUrl})` replace: (m, avatarUrl) => `${m},onClick:()=>$self.openAvatar(${avatarUrl})`
} }
}, },
// User Dms top large icon // User Dms top large icon
@ -221,7 +228,7 @@ export default definePlugin({
find: 'experimentLocation:"empty_messages"', find: 'experimentLocation:"empty_messages"',
replacement: { replacement: {
match: /.Avatar,.+?src:(.+?\))(?=[,}])/, match: /.Avatar,.+?src:(.+?\))(?=[,}])/,
replace: (m, avatarUrl) => `${m},onClick:()=>$self.openImage(${avatarUrl})` replace: (m, avatarUrl) => `${m},onClick:()=>$self.openAvatar(${avatarUrl})`
} }
} }
] ]

3
src/utils/discord.css Normal file
View file

@ -0,0 +1,3 @@
.vc-position-inherit {
position: inherit;
}

View file

@ -16,11 +16,13 @@
* 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 "./discord.css";
import { MessageObject } from "@api/MessageEvents"; import { MessageObject } from "@api/MessageEvents";
import { ChannelStore, ComponentDispatch, Constants, FluxDispatcher, GuildStore, InviteActions, MaskedLink, MessageActions, ModalImageClasses, PrivateChannelsStore, RestAPI, SelectedChannelStore, SelectedGuildStore, UserProfileActions, UserProfileStore, UserSettingsActionCreators, UserUtils } from "@webpack/common"; import { ChannelStore, ComponentDispatch, Constants, FluxDispatcher, GuildStore, InviteActions, MessageActions, PrivateChannelsStore, RestAPI, SelectedChannelStore, SelectedGuildStore, UserProfileActions, UserProfileStore, UserSettingsActionCreators, UserUtils } from "@webpack/common";
import { Channel, Guild, Message, User } from "discord-types/general"; import { Channel, Guild, Message, User } from "discord-types/general";
import { ImageModal, ModalRoot, ModalSize, openModal } from "./modal"; import { ImageModal, ImageModalItem, openModal } from "./modal";
/** /**
* Open the invite modal * Open the invite modal
@ -108,25 +110,23 @@ export function sendMessage(
return MessageActions.sendMessage(channelId, messageData, waitForChannelReady, extra); return MessageActions.sendMessage(channelId, messageData, waitForChannelReady, extra);
} }
export function openImageModal(url: string, props?: Partial<React.ComponentProps<ImageModal>>): string { /**
* You must specify either height or width
*/
export function openImageModal(props: Omit<ImageModalItem, "type">): string {
return openModal(modalProps => ( return openModal(modalProps => (
<ModalRoot <ImageModal
{...modalProps} {...modalProps}
className={ModalImageClasses.modal} fit="vc-position-inherit"
size={ModalSize.DYNAMIC}> items={[{
<ImageModal type: "IMAGE",
className={ModalImageClasses.image} original: props.url,
original={url} ...props,
placeholder={url} }]}
src={url} onClose={modalProps.onClose}
renderLinkComponent={props => <MaskedLink {...props} />} shouldHideMediaOptions={false}
// Don't render forward message button shouldAnimate
renderForwardComponent={() => null} />
shouldHideMediaOptions={false}
shouldAnimate
{...props}
/>
</ModalRoot>
)); ));
} }

View file

@ -101,25 +101,24 @@ export const Modals = findByPropsLazy("ModalRoot", "ModalCloseButton") as {
}>; }>;
}; };
export type ImageModal = ComponentType<{ export interface ImageModalItem {
className?: string; type: "IMAGE" | "VIDEO";
src: string; url: string;
placeholder: string;
original: string;
width?: number; width?: number;
height?: number; height?: number;
animated?: boolean; original?: string;
responsive?: boolean; }
renderLinkComponent(props: any): ReactNode;
renderForwardComponent(props: any): ReactNode; export type ImageModal = ComponentType<{
maxWidth?: number; className?: string;
maxHeight?: number; fit?: string;
shouldAnimate?: boolean;
onClose?(): void; onClose?(): void;
shouldHideMediaOptions?: boolean; shouldHideMediaOptions?: boolean;
shouldAnimate?: boolean;
items: ImageModalItem[];
}>; }>;
export const ImageModal = findComponentByCodeLazy(".MEDIA_MODAL_CLOSE", "responsive") as ImageModal; export const ImageModal = findComponentByCodeLazy(".MEDIA_MODAL_CLOSE") as ImageModal;
export const ModalRoot = LazyComponent(() => Modals.ModalRoot); export const ModalRoot = LazyComponent(() => Modals.ModalRoot);
export const ModalHeader = LazyComponent(() => Modals.ModalHeader); export const ModalHeader = LazyComponent(() => Modals.ModalHeader);