Equicord/src/equicordplugins/quoter/index.tsx
2025-12-11 21:03:36 -05:00

213 lines
7.7 KiB
TypeScript

/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import { BaseText } from "@components/BaseText";
import { FormSwitch } from "@components/FormSwitch";
import { Devs, EquicordDevs } from "@utils/constants";
import { getCurrentChannel } from "@utils/discord";
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import definePlugin, { OptionType } from "@utils/types";
import { Message } from "@vencord/discord-types";
import { Button, Menu, TextInput, UploadHandler, useEffect, useState } from "@webpack/common";
import { QuoteIcon } from "./components/QuoteIcon";
import { QuoteFont } from "./types";
import { createQuoteImage, ensureFontLoaded, generateFileNamePreview, getFileExtension, getMimeType, resetFontLoading, sizeUpgrade } from "./utils";
const settings = definePluginSettings({
quoteFont: {
type: OptionType.SELECT,
description: "Font for quote text (author/username always use M PLUS Rounded 1c)",
options: [
{ label: "M PLUS Rounded 1c", value: QuoteFont.MPlusRounded, default: true },
{ label: "Open Sans", value: QuoteFont.OpenSans },
{ label: "Momo Signature", value: QuoteFont.MomoSignature },
{ label: "Lora", value: QuoteFont.Lora },
{ label: "Merriweather", value: QuoteFont.Merriweather }
]
},
watermark: {
type: OptionType.STRING,
description: "Custom watermark text (max 32 characters)",
default: "Made with Equicord"
},
grayscale: {
type: OptionType.BOOLEAN,
description: "Enable grayscale by default",
default: true,
hidden: true
},
showWatermark: {
type: OptionType.BOOLEAN,
description: "Show watermark by default",
default: false,
hidden: true
},
saveAsGif: {
type: OptionType.BOOLEAN,
description: "Save as GIF by default",
default: false,
hidden: true
}
});
export default definePlugin({
name: "Quoter",
description: "Adds the ability to create an inspirational quote image from a message.",
authors: [Devs.Samwich, Devs.thororen, EquicordDevs.neoarz, EquicordDevs.Prism],
settings,
async start() {
await ensureFontLoaded();
},
stop() {
const style = document.getElementById("quoter-font-style");
if (style) style.remove();
resetFontLoading();
},
contextMenus: {
"message": (children, { message }) => {
if (!message.content) return;
const buttonElement = (
<Menu.MenuItem
id="vc-quote"
label="Quote"
icon={QuoteIcon}
action={() => openModal(props => <QuoteModal message={message} {...props} />)}
/>
);
children.push(buttonElement);
}
}
});
function QuoteModal({ message, ...props }: ModalProps & { message: Message; }) {
const [gray, setGray] = useState(settings.store.grayscale);
const [showWatermark, setShowWatermark] = useState(settings.store.showWatermark);
const [saveAsGif, setSaveAsGif] = useState(settings.store.saveAsGif);
const [watermarkText, setWatermarkText] = useState(settings.store.watermark);
const [quoteImage, setQuoteImage] = useState<Blob | null>(null);
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const { quoteFont } = settings.store;
useEffect(() => {
settings.store.grayscale = gray;
settings.store.showWatermark = showWatermark;
settings.store.saveAsGif = saveAsGif;
}, [gray, showWatermark, saveAsGif]);
const generateImage = async () => {
const image = await createQuoteImage({
avatarUrl: sizeUpgrade(message.author.getAvatarURL()),
quote: message.content,
grayScale: gray,
author: message.author,
watermark: watermarkText,
showWatermark,
saveAsGif,
quoteFont
});
setQuoteImage(image);
if (previewUrl) URL.revokeObjectURL(previewUrl);
const newUrl = URL.createObjectURL(image);
setPreviewUrl(newUrl);
document.getElementById("quoterPreview")?.setAttribute("src", newUrl);
};
useEffect(() => { generateImage(); }, [gray, showWatermark, saveAsGif, watermarkText, quoteFont]);
useEffect(() => {
return () => {
if (previewUrl) {
URL.revokeObjectURL(previewUrl);
}
};
}, [previewUrl]);
const handleExport = () => {
if (!quoteImage) return;
const preview = generateFileNamePreview(message.content);
const extension = getFileExtension(saveAsGif);
const url = URL.createObjectURL(quoteImage);
const link = document.createElement("a");
link.href = url;
link.download = `${preview} - ${message.author.username}.${extension}`;
link.click();
link.remove();
URL.revokeObjectURL(url);
};
const handleSendInChat = () => {
if (!quoteImage) return;
const channel = getCurrentChannel();
if (!channel) return;
const preview = generateFileNamePreview(message.content);
const extension = getFileExtension(saveAsGif);
const mimeType = getMimeType(saveAsGif);
const file = new File([quoteImage], `${preview} - ${message.author.username}.${extension}`, { type: mimeType });
UploadHandler.promptToUpload([file], channel, 0);
props.onClose?.();
};
return (
<ModalRoot {...props} size={ModalSize.MEDIUM}>
<ModalHeader separator={false}>
<BaseText color="text-strong" size="lg" weight="semibold" tag="h1" style={{ flexGrow: 1 }}>
Catch Them In 4K.
</BaseText>
<ModalCloseButton onClick={props.onClose} />
</ModalHeader>
<ModalContent scrollbarType="none">
<img alt="Quote preview" src="" id="quoterPreview" style={{ borderRadius: "20px", width: "100%", marginBottom: "20px" }} />
<FormSwitch title="Grayscale" value={gray} onChange={setGray} />
<FormSwitch
title="Save as GIF"
value={saveAsGif}
onChange={setSaveAsGif}
description="Saves/Sends the image as a GIF instead of a PNG"
/>
<FormSwitch
title="Show Watermark"
value={showWatermark}
onChange={setShowWatermark}
hideBorder={showWatermark}
/>
{showWatermark && (
<div style={{ marginTop: "8px", marginBottom: "20px" }}>
<TextInput
value={watermarkText}
onChange={setWatermarkText}
placeholder="Watermark text (max 32 characters)"
maxLength={32}
/>
</div>
)}
<div style={{ display: "flex", gap: "8px", marginTop: "16px", marginBottom: "16px" }}>
<Button color={Button.Colors.BRAND} size={Button.Sizes.MEDIUM} onClick={handleExport}>
Export
</Button>
<Button color={Button.Colors.BRAND} size={Button.Sizes.MEDIUM} onClick={handleSendInChat}>
Send
</Button>
</div>
</ModalContent>
</ModalRoot>
);
}