diff --git a/src/equicordplugins/quoter/components.tsx b/src/equicordplugins/quoter/components.tsx
new file mode 100644
index 00000000..f7e350cd
--- /dev/null
+++ b/src/equicordplugins/quoter/components.tsx
@@ -0,0 +1,15 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+export function QuoteIcon() {
+ return (
+
+ );
+}
diff --git a/src/equicordplugins/quoter/index.tsx b/src/equicordplugins/quoter/index.tsx
index 22cb852e..66e7af11 100644
--- a/src/equicordplugins/quoter/index.tsx
+++ b/src/equicordplugins/quoter/index.tsx
@@ -5,26 +5,26 @@
*/
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
+import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import { getCurrentChannel } from "@utils/discord";
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
-import definePlugin from "@utils/types";
-import { Button, Menu, Switch, Text, UploadHandler, useEffect, useState } from "@webpack/common";
+import definePlugin, { OptionType } from "@utils/types";
+import { Button, Menu, Select, Switch, Text, TextInput, UploadHandler, useEffect, UserStore, useState } from "@webpack/common";
import { Message } from "discord-types/general";
-let recentmessage: Message;
-let grayscale;
+import { QuoteIcon } from "./components";
+import { canvasToBlob, fetchImageAsBlob, FixUpQuote, wrapText } from "./utils";
-const messagePatch: NavContextMenuPatchCallback = (children, { message }) => () => {
+enum ImageStyle {
+ inspirational
+}
+
+const messagePatch: NavContextMenuPatchCallback = (children, { message }) => {
recentmessage = message;
if (!message.content) return;
- const group = findGroupChildrenByChildId("copy-text", children);
- if (!group) return;
-
- group.splice(
- group.findIndex(c => c?.props?.id === "copy-text") + 1,
- 0,
+ const buttonElement =
()
action={async () => {
openModal(props => );
}}
- />
+ />;
+
+ const group = findGroupChildrenByChildId("copy-text", children);
+ if (!group) {
+ children.push(buttonElement);
+ return;
+ }
+
+ group.splice(
+ group.findIndex(c => c?.props?.id === "copy-text") + 1, 0, buttonElement
);
};
-export function QuoteIcon({
- height = 24,
- width = 24,
- className
-}: {
- height?: number;
- width?: number;
- className?: string;
-}) {
- return (
-
- );
+let recentmessage: Message;
+let grayscale;
+let setStyle: ImageStyle = ImageStyle.inspirational;
+let customMessage: string = "";
+let customImage: string = "";
+let customName: string = "";
+let isUserCustomCapable = false;
+
+enum userIDOptions {
+ usernameNormalized,
+ userName,
+ userId
}
+const settings = definePluginSettings({
+ userIdentifier:
+ {
+ type: OptionType.SELECT,
+ description: "What the author's name should be displayed as",
+ options: [
+ { label: "Username Normalized", value: userIDOptions.usernameNormalized, default: true },
+ { label: "Username", value: userIDOptions.userName },
+ { label: "User ID", value: userIDOptions.userId }
+ ]
+ }
+});
+
+export default definePlugin({
+ name: "Quoter",
+ description: "Adds the ability to create an inspirational quote image from a message",
+ authors: [Devs.Samwich],
+ contextMenus: {
+ "message": messagePatch
+ },
+ settings
+});
function sizeUpgrade(url) {
const u = new URL(url);
- u.searchParams.set("size", "1024");
+ u.searchParams.set("size", "512");
return u.toString();
}
-let preparingSentence: string[] = [];
+const preparingSentence: string[] = [];
const lines: string[] = [];
-async function createQuoteImage(avatarUrl: string, name: string, quoteOld: string, grayScale: boolean): Promise {
- const quote = removeCustomEmojis(quoteOld);
+async function createQuoteImage(avatarUrl: string, quoteOld: string, grayScale: boolean): Promise {
+ let name: string = "";
+
+ switch (settings.store.userIdentifier) {
+ case userIDOptions.usernameNormalized:
+ const meow = recentmessage.author.usernameNormalized;
+ if (meow) {
+ name = meow;
+ }
+ else {
+ name = recentmessage.author.username;
+ }
+ break;
+ case userIDOptions.userName:
+ name = recentmessage.author.username;
+ break;
+ case userIDOptions.userId:
+ name = recentmessage.author.id;
+ break;
+ default:
+ name = "MAN WTF HAPPENED";
+ break;
+ }
+ let quote;
+ if (isUserCustomCapable && customMessage.length > 0 && customImage.length > 0 && customName.length > 0) {
+ quote = FixUpQuote(customMessage);
+ avatarUrl = customImage;
+ name = customName;
+ } else if (isUserCustomCapable && customMessage.length > 0 && customImage.length > 0) {
+ quote = FixUpQuote(customMessage);
+ avatarUrl = customImage;
+ } else if (isUserCustomCapable && customMessage.length > 0 && customName.length > 0) {
+ quote = FixUpQuote(customMessage);
+ name = customName;
+ } else if (isUserCustomCapable && customImage.length > 0 && customName.length > 0) {
+ avatarUrl = customImage;
+ name = customName;
+ } else if (isUserCustomCapable && customImage.length > 0) {
+ avatarUrl = customImage;
+ } else if (isUserCustomCapable && customName.length > 0) {
+ name = customName;
+ } else if (isUserCustomCapable && customMessage.length > 0) {
+ quote = FixUpQuote(customMessage);
+ } else {
+ quote = FixUpQuote(quoteOld);
+ }
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
@@ -72,120 +143,100 @@ async function createQuoteImage(avatarUrl: string, name: string, quoteOld: strin
throw new Error("Cant get 2d rendering context :(");
}
- const cardWidth = 1200;
- const cardHeight = 600;
+ switch (setStyle) {
+ case ImageStyle.inspirational:
- canvas.width = cardWidth;
- canvas.height = cardHeight;
+ const cardWidth = 1200;
+ const cardHeight = 600;
- ctx.fillStyle = "#000";
- ctx.fillRect(0, 0, canvas.width, canvas.height);
+ canvas.width = cardWidth;
+ canvas.height = cardHeight;
- const avatarBlob = await fetchImageAsBlob(avatarUrl);
- const fadeBlob = await fetchImageAsBlob("https://files.catbox.moe/54e96l.png");
+ ctx.fillStyle = "#000";
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
- const avatar = new Image();
- const fade = new Image();
+ const avatarBlob = await fetchImageAsBlob(avatarUrl);
+ const fadeBlob = await fetchImageAsBlob("https://files.catbox.moe/54e96l.png");
- const avatarPromise = new Promise(resolve => {
- avatar.onload = () => resolve();
- avatar.src = URL.createObjectURL(avatarBlob);
- });
+ const avatar = new Image();
+ const fade = new Image();
- const fadePromise = new Promise(resolve => {
- fade.onload = () => resolve();
- fade.src = URL.createObjectURL(fadeBlob);
- });
+ const avatarPromise = new Promise(resolve => {
+ avatar.onload = () => resolve();
+ avatar.src = URL.createObjectURL(avatarBlob);
+ });
- await Promise.all([avatarPromise, fadePromise]);
+ const fadePromise = new Promise(resolve => {
+ fade.onload = () => resolve();
+ fade.src = URL.createObjectURL(fadeBlob);
+ });
- if (grayScale) {
- ctx.drawImage(avatar, 0, 0, cardHeight, cardHeight);
- ctx.globalCompositeOperation = "saturation";
- ctx.fillStyle = "#fff";
- ctx.fillRect(0, 0, cardWidth, cardHeight);
- ctx.globalCompositeOperation = "source-over";
- } else {
- ctx.drawImage(avatar, 0, 0, cardHeight, cardHeight);
- }
- ctx.drawImage(fade, cardHeight - 400, 0, 400, cardHeight);
+ await Promise.all([avatarPromise, fadePromise]);
- ctx.fillStyle = "#fff";
- ctx.font = "italic 20px Georgia";
- const quoteWidth = cardWidth / 2 - 50;
- const quoteX = ((cardWidth - cardHeight));
- const quoteY = cardHeight / 2 - 10;
- wrapText(ctx, quote, quoteX, quoteY, quoteWidth, 20);
+ ctx.drawImage(avatar, 0, 0, cardHeight, cardHeight);
- const wrappedTextHeight = lines.length * 25;
-
- ctx.font = "bold 16px Georgia";
- const authorNameX = (cardHeight * 1.5) - (ctx.measureText(`- ${name}`).width / 2) - 30;
- const authorNameY = quoteY + wrappedTextHeight + 30;
-
- ctx.fillText(`- ${name}`, authorNameX, authorNameY);
- preparingSentence.length = 0;
- lines.length = 0;
- return new Promise(resolve => {
- canvas.toBlob(blob => {
- if (blob) {
- resolve(blob);
- } else {
- throw new Error("Failed to create Blob");
+ if (grayScale) {
+ ctx.globalCompositeOperation = "saturation";
+ ctx.fillStyle = "#fff";
+ ctx.fillRect(0, 0, cardWidth, cardHeight);
+ ctx.globalCompositeOperation = "source-over";
}
- }, "image/png");
- });
- function wrapText(
- context: CanvasRenderingContext2D,
- text: string,
- x: number,
- y: number,
- maxWidth: number,
- lineHeight: number
- ) {
- const words = text.split(" ");
- for (let i = 0; i < words.length; i++) {
- const workSentence = preparingSentence.join(" ") + " " + words[i];
+ ctx.drawImage(fade, cardHeight - 400, 0, 400, cardHeight);
- if (context.measureText(workSentence).width > maxWidth) {
- lines.push(preparingSentence.join(" "));
- preparingSentence = [words[i]];
- } else {
- preparingSentence.push(words[i]);
- }
- }
+ ctx.fillStyle = "#fff";
+ ctx.font = "italic 20px Georgia";
+ const quoteWidth = cardWidth / 2 - 50;
+ const quoteX = ((cardWidth - cardHeight));
+ const quoteY = cardHeight / 2 - 10;
+ wrapText(ctx, `"${quote}"`, quoteX, quoteY, quoteWidth, 20, preparingSentence, lines);
- lines.push(preparingSentence.join(" "));
+ const wrappedTextHeight = lines.length * 25;
- lines.forEach(element => {
- const lineWidth = context.measureText(element).width;
- const xOffset = (maxWidth - lineWidth) / 2;
+ ctx.font = "bold 16px Georgia";
+ const authorNameX = (cardHeight * 1.5) - (ctx.measureText(`- ${name}`).width / 2) - 30;
+ const authorNameY = quoteY + wrappedTextHeight + 30;
- y += lineHeight;
- context.fillText(element, x + xOffset, y);
- });
+ ctx.fillText(`- ${name}`, authorNameX, authorNameY);
+ preparingSentence.length = 0;
+ lines.length = 0;
+ return await canvasToBlob(canvas);
}
-
- async function fetchImageAsBlob(url: string): Promise {
- const response = await fetch(url);
- const blob = await response.blob();
- return blob;
- }
-
- function removeCustomEmojis(quote) {
- const emojiRegex = //g;
- return quote.replace(emojiRegex, "");
- }
-
}
+function registerStyleChange(style) {
+ setStyle = style;
+ GeneratePreview();
+}
+
+async function setIsUserCustomCapable() {
+ const allowList: string[] = await fetch("https://raw.githubusercontent.com/Equicord/Ignore/main/quoterusers.json").then(e => e.json());
+ isUserCustomCapable = allowList.includes(UserStore.getCurrentUser().id);
+}
+
+
function QuoteModal(props: ModalProps) {
+ setIsUserCustomCapable();
const [gray, setGray] = useState(true);
useEffect(() => {
grayscale = gray;
GeneratePreview();
}, [gray]);
+
+ const safeTextContent = recentmessage && recentmessage.content ? recentmessage.content : "";
+ const safeAvatarContent = recentmessage && recentmessage.author.avatar ? recentmessage.author.avatar : "";
+ const safeUsernameContent = recentmessage && recentmessage.author.username ? recentmessage.author.username : "";
+
+ const [customText, setCustomText] = useState(safeTextContent);
+ const [customAvatar, setCustomAvatar] = useState(safeAvatarContent);
+ const [customUsername, setCustomUsername] = useState(safeUsernameContent);
+ useEffect(() => {
+ customMessage = customText;
+ customImage = customAvatar;
+ customName = customUsername;
+ GeneratePreview();
+ }, [customText]);
+
return (
@@ -197,7 +248,26 @@ function QuoteModal(props: ModalProps) {
+ {isUserCustomCapable &&
+ (
+ <>
+
+
+
+
+
+
+ >
+ )}
Grayscale
+
+
@@ -207,47 +277,25 @@ function QuoteModal(props: ModalProps) {
}
async function SendInChat(onClose) {
- const image = await createQuoteImage(sizeUpgrade(recentmessage.author.getAvatarURL()), recentmessage.author.username, recentmessage.content, grayscale);
- const preview = generateFileNamePreview(recentmessage.content);
- const imageName = `${preview} - ${recentmessage.author.username}`;
+ const image = await createQuoteImage(sizeUpgrade(recentmessage.author.getAvatarURL()), recentmessage.content, grayscale);
+ const imageName = `${new Date().toISOString()} - ${recentmessage.author.username}`;
const file = new File([image], `${imageName}.png`, { type: "image/png" });
UploadHandler.promptToUpload([file], getCurrentChannel(), 0);
onClose();
}
async function Export() {
- const image = await createQuoteImage(sizeUpgrade(recentmessage.author.getAvatarURL()), recentmessage.author.username, recentmessage.content, grayscale);
+ const image = await createQuoteImage(sizeUpgrade(recentmessage.author.getAvatarURL()), recentmessage.content, grayscale);
const link = document.createElement("a");
link.href = URL.createObjectURL(image);
- const preview = generateFileNamePreview(recentmessage.content);
- const imageName = `${preview} - ${recentmessage.author.username}`;
+ const imageName = `${new Date().toISOString()} - ${recentmessage.author.username}`;
link.download = `${imageName}.png`;
link.click();
link.remove();
}
async function GeneratePreview() {
- const image = await createQuoteImage(sizeUpgrade(recentmessage.author.getAvatarURL()), recentmessage.author.username, recentmessage.content, grayscale);
+ const image = await createQuoteImage(sizeUpgrade(recentmessage.author.getAvatarURL()), recentmessage.content, grayscale);
document.getElementById("quoterPreview")?.setAttribute("src", URL.createObjectURL(image));
}
-
-function generateFileNamePreview(message) {
- const words = message.split(" ");
- let preview;
- if (words.length >= 6) {
- preview = words.slice(0, 6).join(" ");
- } else {
- preview = words.slice(0, words.length).join(" ");
- }
- return preview;
-}
-
-export default definePlugin({
- name: "Quoter",
- description: "Adds the ability to create a quote image from a message",
- authors: [Devs.Samwich],
- contextMenus: {
- "message": messagePatch
- }
-});
diff --git a/src/equicordplugins/quoter/utils.tsx b/src/equicordplugins/quoter/utils.tsx
new file mode 100644
index 00000000..bceaba91
--- /dev/null
+++ b/src/equicordplugins/quoter/utils.tsx
@@ -0,0 +1,65 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { UserStore } from "@webpack/common";
+
+export function canvasToBlob(canvas: HTMLCanvasElement): Promise {
+ return new Promise(resolve => {
+ canvas.toBlob(blob => {
+ if (blob) {
+ resolve(blob);
+ } else {
+ throw new Error("Failed to create Blob");
+ }
+ }, "image/png");
+ });
+}
+
+export function wrapText(context: CanvasRenderingContext2D, text: string, x: number, y: number, maxWidth: number, lineHeight: number, preparingSentence: string[], lines: string[]) {
+ const words = text.split(" ");
+ for (let i = 0; i < words.length; i++) {
+ const workSentence = preparingSentence.join(" ") + " " + words[i];
+
+ if (context.measureText(workSentence).width > maxWidth) {
+ lines.push(preparingSentence.join(" "));
+ preparingSentence = [words[i]];
+ } else {
+ preparingSentence.push(words[i]);
+ }
+ }
+
+ lines.push(preparingSentence.join(" "));
+
+ lines.forEach(element => {
+ const lineWidth = context.measureText(element).width;
+ const xOffset = (maxWidth - lineWidth) / 2;
+
+ y += lineHeight;
+ context.fillText(element, x + xOffset, y);
+ });
+}
+
+export async function fetchImageAsBlob(url: string): Promise {
+ const response = await fetch(url);
+ const blob = await response.blob();
+ return blob;
+}
+
+export function FixUpQuote(quote) {
+ const emojiRegex = //g;
+ quote = quote.replace(emojiRegex, "");
+
+
+ const mentionRegex = /<@(.*)>/;
+ let result = quote;
+
+ mentionRegex.exec(quote)?.forEach(match => {
+ console.log(match);
+ result = result.replace(match, `@${UserStore.getUser(match.replace("<@", "").replace(">", "")).username}`);
+ });
+
+ return result;
+}