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; +}