Emote Clone plugin
This commit is contained in:
parent
81edc14070
commit
9110d1f9bd
231
src/plugins/EmoteYoink.tsx
Normal file
231
src/plugins/EmoteYoink.tsx
Normal file
@ -0,0 +1,231 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { CheckedTextInput } from "../components/CheckedTextInput";
|
||||
import { Devs } from "../utils/constants";
|
||||
import { lazyWebpack } from "../utils/misc";
|
||||
import { ModalContent, ModalHeader, ModalRoot, openModal } from "../utils/modal";
|
||||
import definePlugin from "../utils/types";
|
||||
import { filters } from "../webpack";
|
||||
import { Forms, GuildStore, Margins, Menu, PermissionStore, React, Toasts, Tooltip, UserStore } from "../webpack/common";
|
||||
|
||||
const MANAGE_EMOJIS_AND_STICKERS = 1n << 30n;
|
||||
|
||||
const GuildEmojiStore = lazyWebpack(filters.byProps("getGuilds", "getGuildEmoji"));
|
||||
const uploadEmoji = lazyWebpack(filters.byCode('"EMOJI_UPLOAD_START"', "GUILD_EMOJIS("));
|
||||
|
||||
function getGuildCandidates(isAnimated: boolean) {
|
||||
const meId = UserStore.getCurrentUser().id;
|
||||
|
||||
return Object.values(GuildStore.getGuilds()).filter(g => {
|
||||
const canCreate = g.ownerId === meId ||
|
||||
BigInt(PermissionStore.getGuildPermissions({ id: g.id }) & MANAGE_EMOJIS_AND_STICKERS) === MANAGE_EMOJIS_AND_STICKERS;
|
||||
if (!canCreate) return false;
|
||||
|
||||
const emojiSlots = g.getMaxEmojiSlots();
|
||||
const { emojis } = GuildEmojiStore.getGuilds()[g.id];
|
||||
|
||||
let count = 0;
|
||||
for (const emoji of emojis)
|
||||
if (emoji.animated === isAnimated) count++;
|
||||
return count < emojiSlots;
|
||||
}).sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
async function doClone(guildId: string, id: string, name: string, isAnimated: boolean) {
|
||||
const data = await fetch(`https://cdn.discordapp.com/emojis/${id}.${isAnimated ? "gif" : "png"}`)
|
||||
.then(r => r.blob());
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = () => {
|
||||
uploadEmoji({
|
||||
guildId,
|
||||
name,
|
||||
image: reader.result
|
||||
}).then(() => {
|
||||
Toasts.show({
|
||||
message: `Successfully yoinked ${name}!`,
|
||||
type: Toasts.Type.SUCCESS,
|
||||
id: Toasts.genId()
|
||||
});
|
||||
}).catch(e => {
|
||||
console.error("[EmoteYoink] Failed to upload emoji", e);
|
||||
Toasts.show({
|
||||
message: "Oopsie something went wrong :( Check console!!!",
|
||||
type: Toasts.Type.FAILURE,
|
||||
id: Toasts.genId()
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
reader.readAsDataURL(data);
|
||||
}
|
||||
|
||||
const getFontSize = (s: string) => {
|
||||
// [18, 18, 16, 16, 14, 12, 10]
|
||||
const sizes = [20, 20, 18, 18, 16, 14, 12];
|
||||
return sizes[s.length] ?? 4;
|
||||
};
|
||||
|
||||
const nameValidator = /^\w+$/i;
|
||||
|
||||
function CloneModal({ id, name: emojiName, isAnimated }: { id: string; name: string; isAnimated: boolean; }) {
|
||||
const [isCloning, setIsCloning] = React.useState(false);
|
||||
const [name, setName] = React.useState(emojiName);
|
||||
|
||||
const [x, invalidateMemo] = React.useReducer(x => x + 1, 0);
|
||||
|
||||
const guilds = React.useMemo(() => getGuildCandidates(isAnimated), [isAnimated, x]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Forms.FormTitle className={Margins.marginTop20}>Custom Name</Forms.FormTitle>
|
||||
<CheckedTextInput
|
||||
value={name}
|
||||
onChange={setName}
|
||||
validate={v =>
|
||||
(v.length > 1 && v.length < 32 && nameValidator.test(v))
|
||||
|| "Name must be between 2 and 32 characters and only contain alphanumeric characters"
|
||||
}
|
||||
/>
|
||||
<div style={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: "1em",
|
||||
padding: "1em 0.5em",
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}>
|
||||
{guilds.map(g => (
|
||||
<Tooltip text={g.name}>
|
||||
{({ onMouseLeave, onMouseEnter }) => (
|
||||
<div
|
||||
onMouseLeave={onMouseLeave}
|
||||
onMouseEnter={onMouseEnter}
|
||||
role="button"
|
||||
aria-label={"Clone to " + g.name}
|
||||
aria-disabled={isCloning}
|
||||
style={{
|
||||
borderRadius: "50%",
|
||||
backgroundColor: "var(--background-secondary)",
|
||||
display: "inline-flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
width: "4em",
|
||||
height: "4em",
|
||||
cursor: isCloning ? "not-allowed" : "pointer",
|
||||
filter: isCloning ? "brightness(50%)" : "none"
|
||||
}}
|
||||
onClick={isCloning ? void 0 : async () => {
|
||||
setIsCloning(true);
|
||||
doClone(g.id, id, name, isAnimated).finally(() => {
|
||||
invalidateMemo();
|
||||
setIsCloning(false);
|
||||
});
|
||||
}}
|
||||
>
|
||||
{g.icon ? (
|
||||
<img
|
||||
aria-hidden
|
||||
style={{
|
||||
borderRadius: "50%",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
src={g.getIconURL(512, true)}
|
||||
alt={g.name}
|
||||
/>
|
||||
) : (
|
||||
<Forms.FormText
|
||||
style={{
|
||||
fontSize: getFontSize(g.acronym),
|
||||
width: "100%",
|
||||
overflow: "hidden",
|
||||
whiteSpace: "nowrap",
|
||||
textAlign: "center",
|
||||
cursor: isCloning ? "not-allowed" : "pointer",
|
||||
}}
|
||||
>
|
||||
{g.acronym}
|
||||
</Forms.FormText>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "EmoteYoink",
|
||||
description: "Clone emotes to your own server",
|
||||
authors: [Devs.Ven],
|
||||
dependencies: ["MenuItemDeobfuscatorApi"],
|
||||
|
||||
patches: [{
|
||||
// Literally copy pasted from ReverseImageSearch lol
|
||||
find: "open-native-link",
|
||||
replacement: {
|
||||
match: /id:"open-native-link".{0,200}\(\{href:(.{0,3}),.{0,200}\},"open-native-link"\)/,
|
||||
replace: "$&,Vencord.Plugins.plugins.EmoteYoink.makeMenu(arguments[2])"
|
||||
}
|
||||
}],
|
||||
|
||||
makeMenu(htmlElement: HTMLImageElement) {
|
||||
if (htmlElement?.dataset.type !== "emoji")
|
||||
return null;
|
||||
|
||||
const { id } = htmlElement.dataset;
|
||||
const name = htmlElement.alt.match(/:(.*)(?:~\d+)?:/)?.[1];
|
||||
|
||||
if (!name || !id)
|
||||
return null;
|
||||
|
||||
const isAnimated = new URL(htmlElement.src).pathname.endsWith(".gif");
|
||||
|
||||
return <Menu.MenuItem
|
||||
id="yoink"
|
||||
key="yoink"
|
||||
label="Yoink"
|
||||
action={() =>
|
||||
openModal(modalProps => (
|
||||
<ModalRoot {...modalProps}>
|
||||
<ModalHeader>
|
||||
<img
|
||||
role="presentation"
|
||||
aria-hidden
|
||||
src={`https://cdn.discordapp.com/emojis/${id}.${isAnimated ? "gif" : "png"}`}
|
||||
alt=""
|
||||
height={24}
|
||||
width={24}
|
||||
style={{ marginRight: "0.5em" }}
|
||||
/>
|
||||
<Forms.FormText>Clone {name}</Forms.FormText>
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<CloneModal id={id} name={name} isAnimated={isAnimated} />
|
||||
</ModalContent>
|
||||
</ModalRoot>
|
||||
))
|
||||
}
|
||||
>
|
||||
</Menu.MenuItem>;
|
||||
},
|
||||
});
|
@ -56,7 +56,7 @@ export default definePlugin({
|
||||
}],
|
||||
|
||||
makeMenu(src: string, target: HTMLElement) {
|
||||
if (target && target.attributes["data-role"]?.value !== "img")
|
||||
if (target && !(target instanceof HTMLImageElement) && target.attributes["data-role"]?.value !== "img")
|
||||
return null;
|
||||
|
||||
return (
|
||||
|
@ -33,6 +33,7 @@ export let React: typeof import("react");
|
||||
export const ReactDOM: typeof import("react-dom") = lazyWebpack(filters.byProps("createPortal", "render"));
|
||||
|
||||
export const MessageStore = lazyWebpack(filters.byProps("getRawMessages")) as Omit<Stores.MessageStore, "getMessages"> & { getMessages(chanId: string): any; };
|
||||
export const PermissionStore = lazyWebpack(filters.byProps("can", "getGuildPermissions"));
|
||||
export let GuildStore: Stores.GuildStore;
|
||||
export let UserStore: Stores.UserStore;
|
||||
export let SelectedChannelStore: Stores.SelectedChannelStore;
|
||||
|
Loading…
x
Reference in New Issue
Block a user