diff --git a/README.md b/README.md index 521fd1e3..08ba6179 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,6 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch - GlobalBadges by HypedDomi & Hosted by Wolfie - GoogleThat by Samwich - HideChatButtons by iamme -- HideMessage by Hanzy - HideServers by bepvte - HolyNotes by Wolfie - HomeTyping by Samwich @@ -115,6 +114,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch - QuestCompleter by Amia - QuestionMarkReplacement by nyx - Quoter by Samwich +- Remix by MrDiamond - RemixMe by kvba - RepeatMessage by Tolgchu - ReplyPingControl by ant0n & MrDiamond diff --git a/src/equicordplugins/gifCollections/index.tsx b/src/equicordplugins/gifCollections/index.tsx index 271261cd..33fff6ac 100644 --- a/src/equicordplugins/gifCollections/index.tsx +++ b/src/equicordplugins/gifCollections/index.tsx @@ -6,7 +6,7 @@ import "./style.css"; -import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; import { DataStore } from "@api/index"; import { definePluginSettings } from "@api/Settings"; import { Flex } from "@components/Flex"; @@ -317,15 +317,14 @@ export default definePlugin({ }, ], settings, + contextMenus: { + "message": addCollectionContextMenuPatch + }, start() { refreshCacheCollection(); - addContextMenuPatch("message", addCollectionContextMenuPatch); GIF_COLLECTION_PREFIX = settings.store.collectionPrefix; GIF_ITEM_PREFIX = settings.store.itemPrefix; }, - stop() { - removeContextMenuPatch("message", addCollectionContextMenuPatch); - }, get collections() { refreshCacheCollection(); return this.sortedCollections(); diff --git a/src/equicordplugins/hideMessage/EyeIcon.tsx b/src/equicordplugins/hideMessage/EyeIcon.tsx deleted file mode 100644 index eae2323b..00000000 --- a/src/equicordplugins/hideMessage/EyeIcon.tsx +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -export function EyeIcon({ height = 24, width = 24, className }: { height?: number; width?: number; className?: string; }) { - return ( - - - - - ); -} diff --git a/src/equicordplugins/hideMessage/HideIcon.tsx b/src/equicordplugins/hideMessage/HideIcon.tsx deleted file mode 100644 index fb80d194..00000000 --- a/src/equicordplugins/hideMessage/HideIcon.tsx +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -export function HideIcon({ height = 24, width = 24, className }: { height?: number; width?: number; className?: string; }) { - return ( - - - - - ); -} diff --git a/src/equicordplugins/hideMessage/HideMessageAccessory.tsx b/src/equicordplugins/hideMessage/HideMessageAccessory.tsx deleted file mode 100644 index 0048d05e..00000000 --- a/src/equicordplugins/hideMessage/HideMessageAccessory.tsx +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { cl, revealMessage } from "./"; -import { HideIcon } from "./HideIcon"; - -export const HideMessageAccessory = ({ id }: { id: string; }) => { - return ( - - - This message is hidden •{" "} - - - ); -}; diff --git a/src/equicordplugins/hideMessage/index.tsx b/src/equicordplugins/hideMessage/index.tsx deleted file mode 100644 index 21265f17..00000000 --- a/src/equicordplugins/hideMessage/index.tsx +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import "./styles.css"; - -import { NavContextMenuPatchCallback } from "@api/ContextMenu"; -import { get, set } from "@api/DataStore"; -import { addMessageAccessory, removeMessageAccessory } from "@api/MessageAccessories"; -import { definePluginSettings } from "@api/Settings"; -import { classNameFactory } from "@api/Styles"; -import { EquicordDevs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; -import { Menu } from "@webpack/common"; - -import { EyeIcon } from "./EyeIcon"; -import { HideIcon } from "./HideIcon"; -import { HideMessageAccessory } from "./HideMessageAccessory"; - -let style: HTMLStyleElement; - -const KEY = "HideMessage_hiddenMessages"; - -let hiddenMessages = new Map(); - -const patchMessageContextMenu: NavContextMenuPatchCallback = (children, { message }) => { - const { deleted, id, channel_id } = message; - if (deleted || message.state !== "SENT") return; - - const isHidden = hiddenMessages?.has(id) || false; - if (isHidden) { - return children.push( - revealMessage(id)} - /> - ); - } - - children.push( { - hiddenMessages.set(id, { id, channel_id }); - if (settings.store.saveHiddenMessages) set(KEY, hiddenMessages); - - buildCss(); - }} - />); -}; - -const buildCss = () => { - const elements = [...hiddenMessages.values()].map(m => `#chat-messages-${m.channel_id}-${m.id}`).join(","); - - style.textContent = settings.store.showNotice ? ` - :is(${elements}):not(.messagelogger-deleted) > div { - position: relative; - background: var(--brand-experiment-05a); - } - :is(${elements}):not(.messagelogger-deleted) > div:hover { - background: var(--brand-experiment-10a); - } - :is(${elements}):not(.messagelogger-deleted) > div:before { - background: var(--brand-500); - content: ""; - position: absolute; - display: block; - top: 0; - left: 0; - bottom: 0; - pointer-events: none; - width: 2px; - } - :is(${elements}) [id^='message-accessories'] > *:not(.vc-hide-message-accessory), - :is(${elements}) [id^='message-content']:not([class^='repliedTextContent']) > * { - display: none !important; - } - :is(${elements}) [id^='message-content']:empty { - display: block !important; - } - :is(${elements}) [class^='contents'] [id^='message-content']:after { - content: "Hidden content"; - } - ` : ` - :is(${elements}) { - display: none !important; - } - `; -}; - -export const revealMessage = (id: string) => { - const isHidden = hiddenMessages?.has(id) || false; - if (isHidden) { - hiddenMessages.delete(id); - buildCss(); - - if (settings.store.saveHiddenMessages) set(KEY, hiddenMessages); - } -}; - -export const cl = classNameFactory("vc-hide-message-"); - -export const settings = definePluginSettings({ - showNotice: { - type: OptionType.BOOLEAN, - description: "Shows a notice when a message is hidden", - default: true, - onChange: buildCss - }, - saveHiddenMessages: { - type: OptionType.BOOLEAN, - description: "Persist restarts", - default: false, - onChange: async (value: boolean) => { - if (value) set(KEY, hiddenMessages); - else (hiddenMessages = await get(KEY) || hiddenMessages); - } - }, -}); - -export default definePlugin({ - name: "HideMessage", - description: "Adds a context menu option to hide messages", - authors: [EquicordDevs.Hanzy], - dependencies: ["MessageAccessoriesAPI", "MessagePopoverAPI"], - settings, - - contextMenus: { - "message": patchMessageContextMenu - }, - - async start() { - style = document.createElement("style"); - style.id = "VencordHideMessage"; - document.head.appendChild(style); - - if (settings.store.saveHiddenMessages) { - hiddenMessages = await get(KEY) || hiddenMessages; - buildCss(); - } - - addMessageAccessory("vc-hide-message", ({ message }) => { - const isHidden = hiddenMessages?.has(message.id) || false; - if (isHidden && settings.store.showNotice) return ; - return null; - }); - }, - - async stop() { - for (const id of hiddenMessages.keys()) revealMessage(id); - - removeMessageAccessory("vc-hide-message"); - - style.remove(); - hiddenMessages.clear(); - }, -}); diff --git a/src/equicordplugins/hideMessage/styles.css b/src/equicordplugins/hideMessage/styles.css deleted file mode 100644 index 7d59b8fb..00000000 --- a/src/equicordplugins/hideMessage/styles.css +++ /dev/null @@ -1,21 +0,0 @@ -.vc-hide-message-accessory { - margin-top: 4px; - font-size: 12px; - font-weight: 400; - color: var(--text-muted); -} - -.vc-hide-message-accessory svg { - margin-right: 4px; - vertical-align: text-bottom; -} - -.vc-hide-message-reveal { - all: unset; - cursor: pointer; - color: var(--text-link); -} - -.vc-hide-message-reveal:is(:hover, :focus) { - text-decoration: underline; -} diff --git a/src/equicordplugins/remix/RemixModal.tsx b/src/equicordplugins/remix/RemixModal.tsx new file mode 100644 index 00000000..d6228eaf --- /dev/null +++ b/src/equicordplugins/remix/RemixModal.tsx @@ -0,0 +1,54 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal"; +import { Button, Text } from "@webpack/common"; + +import { sendRemix } from "."; +import { brushCanvas, canvas, cropCanvas, ctx, exportImg, shapeCanvas } from "./editor/components/Canvas"; +import { Editor } from "./editor/Editor"; +import { resetBounds } from "./editor/tools/crop"; +import { SendIcon } from "./icons/SendIcon"; + +type Props = { + modalProps: ModalProps; + close: () => void; + url?: string; +}; + +function reset() { + resetBounds(); + + if (!ctx || !canvas) return; + ctx.clearRect(0, 0, canvas.width, canvas.height); + brushCanvas.clearRect(0, 0, canvas.width, canvas.height); + shapeCanvas.clearRect(0, 0, canvas.width, canvas.height); + cropCanvas.clearRect(0, 0, canvas.width, canvas.height); +} + +async function closeModal(closeFunc: () => void, save?: boolean) { + if (save) sendRemix(await exportImg()); + reset(); + closeFunc(); +} + +export default function RemixModal({ modalProps, close, url }: Props) { + return ( + + + Remix + closeModal(close)} /> + + + + + + + + + + ); +} diff --git a/src/equicordplugins/remix/editor/Editor.tsx b/src/equicordplugins/remix/editor/Editor.tsx new file mode 100644 index 00000000..a96fd9c1 --- /dev/null +++ b/src/equicordplugins/remix/editor/Editor.tsx @@ -0,0 +1,44 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { findComponentByCodeLazy } from "@webpack"; +import { useEffect, useState } from "@webpack/common"; + +import { Canvas } from "./components/Canvas"; +import { Toolbar } from "./components/Toolbar"; +import { imageToBlob, urlToImage } from "./utils/canvas"; + +const FileUpload = findComponentByCodeLazy("fileUploadInput,"); + +export const Editor = (props: { url?: string; }) => { + const [file, setFile] = useState(undefined); + + useEffect(() => { + if (!props.url) return; + + urlToImage(props.url).then(img => { + imageToBlob(img).then(blob => { + setFile(new File([blob], "remix.png")); + }); + }); + }, []); + + return ( +
+ {!file && setFile(file)} + />} + {file && (<> + + + )} +
+ ); +}; diff --git a/src/equicordplugins/remix/editor/components/Canvas.tsx b/src/equicordplugins/remix/editor/components/Canvas.tsx new file mode 100644 index 00000000..39e1bb42 --- /dev/null +++ b/src/equicordplugins/remix/editor/components/Canvas.tsx @@ -0,0 +1,82 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { useEffect, useRef } from "@webpack/common"; + +import { initInput } from "../input"; +import { bounds } from "../tools/crop"; +import { heightFromBounds, widthFromBounds } from "../utils/canvas"; + +export let canvas: HTMLCanvasElement | null = null; +export let ctx: CanvasRenderingContext2D | null = null; + +export const brushCanvas = document.createElement("canvas")!.getContext("2d")!; +export const shapeCanvas = document.createElement("canvas")!.getContext("2d")!; +export const cropCanvas = document.createElement("canvas")!.getContext("2d")!; + +export let image: HTMLImageElement; + +export function exportImg(): Promise { + return new Promise(resolve => { + if (!canvas || !ctx) return; + + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.drawImage(image, 0, 0); + ctx.drawImage(brushCanvas.canvas, 0, 0); + + if (bounds.right === -1) bounds.right = canvas.width; + if (bounds.bottom === -1) bounds.bottom = canvas.height; + + const renderCanvas = document.createElement("canvas"); + renderCanvas.width = widthFromBounds(bounds); + renderCanvas.height = heightFromBounds(bounds); + + const renderCtx = renderCanvas.getContext("2d")!; + renderCtx.drawImage(canvas, -bounds.left, -bounds.top); + renderCanvas.toBlob(blob => resolve(blob!)); + + render(); + }); +} + +export const Canvas = ({ file }: { file: File; }) => { + const canvasRef = useRef(null); + + useEffect(() => { + image = new Image(); + image.src = URL.createObjectURL(file); + image.onload = () => { + canvas = canvasRef.current; + + if (!canvas) return; + + canvas.width = image.width; + canvas.height = image.height; + brushCanvas.canvas.width = image.width; + brushCanvas.canvas.height = image.height; + shapeCanvas.canvas.width = image.width; + shapeCanvas.canvas.height = image.height; + cropCanvas.canvas.width = image.width; + cropCanvas.canvas.height = image.height; + + ctx = canvas.getContext("2d")!; + ctx.drawImage(image, 0, 0); + + initInput(); + }; + }); + + return (); +}; + +export function render() { + if (!ctx || !canvas) return; + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.drawImage(image, 0, 0); + ctx.drawImage(brushCanvas.canvas, 0, 0); + ctx.drawImage(shapeCanvas.canvas, 0, 0); + ctx.drawImage(cropCanvas.canvas, 0, 0); +} diff --git a/src/equicordplugins/remix/editor/components/SettingColorComponent.tsx b/src/equicordplugins/remix/editor/components/SettingColorComponent.tsx new file mode 100644 index 00000000..fbda3e41 --- /dev/null +++ b/src/equicordplugins/remix/editor/components/SettingColorComponent.tsx @@ -0,0 +1,52 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +// brutally ripped out of usercss +// (remove when usercss is merged) + +import "./colorStyles.css"; + +import { classNameFactory } from "@api/Styles"; +import { findComponentByCodeLazy } from "@webpack"; +import { Forms } from "@webpack/common"; + +interface ColorPickerProps { + color: number | null; + showEyeDropper?: boolean; + onChange(value: number | null): void; +} + +const ColorPicker = findComponentByCodeLazy(".BACKGROUND_PRIMARY).hex"); + +const cl = classNameFactory("vc-remix-settings-color-"); + +interface Props { + name: string; + color: number; + onChange(value: string): void; +} + +function hexToColorString(color: number): string { + return `#${color.toString(16).padStart(6, "0")}`; +} + +export function SettingColorComponent({ name, onChange, color }: Props) { + function handleChange(newColor: number) { + onChange(hexToColorString(newColor)); + } + + return ( + +
+ +
+
+ ); +} diff --git a/src/equicordplugins/remix/editor/components/Toolbar.tsx b/src/equicordplugins/remix/editor/components/Toolbar.tsx new file mode 100644 index 00000000..dac61b82 --- /dev/null +++ b/src/equicordplugins/remix/editor/components/Toolbar.tsx @@ -0,0 +1,141 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Switch } from "@components/Switch"; +import { Button, Forms, Select, Slider, useEffect, useState } from "@webpack/common"; + +import { BrushTool } from "../tools/brush"; +import { CropTool, resetBounds } from "../tools/crop"; +import { EraseTool } from "../tools/eraser"; +import { currentShape, setShape, setShapeFill, Shape, ShapeTool } from "../tools/shape"; +import { brushCanvas, canvas, cropCanvas, render, shapeCanvas } from "./Canvas"; +import { SettingColorComponent } from "./SettingColorComponent"; + +export type Tool = "none" | "brush" | "erase" | "crop" | "shape"; + +export type ToolDefinition = { + selected: () => void; + unselected: () => void; + [key: string]: any; +}; + +export const tools: Record = { + none: undefined, + brush: BrushTool, + erase: EraseTool, + crop: CropTool, + shape: ShapeTool, +}; + +export let currentTool: Tool = "none"; +export let currentColor = "#ff0000"; +export let currentSize = 20; +export let currentFill = false; + +function colorStringToHex(color: string): number { + return parseInt(color.replace("#", ""), 16); +} + +export const Toolbar = () => { + const [tool, setTool] = useState(currentTool); + const [color, setColor] = useState(currentColor); + const [size, setSize] = useState(currentSize); + const [fill, setFill] = useState(currentFill); + + function changeTool(newTool: Tool) { + const oldTool = tool; + + setTool(newTool); + onChangeTool(oldTool, newTool); + } + + function onChangeTool(old: Tool, newTool: Tool) { + tools[old]?.unselected(); + tools[newTool]?.selected(); + } + + useEffect(() => { + currentTool = tool; + currentColor = color; + currentSize = size; + currentFill = fill; + + brushCanvas.fillStyle = color; + shapeCanvas.fillStyle = color; + + brushCanvas.strokeStyle = color; + shapeCanvas.strokeStyle = color; + + brushCanvas.lineWidth = size; + shapeCanvas.lineWidth = size; + + brushCanvas.lineCap = "round"; + brushCanvas.lineJoin = "round"; + + setShapeFill(currentFill); + }, [tool, color, size, fill]); + + function clear() { + if (!canvas) return; + + brushCanvas.clearRect(0, 0, canvas.width, canvas.height); + shapeCanvas.clearRect(0, 0, canvas.width, canvas.height); + resetBounds(); + if (tool !== "crop") cropCanvas.clearRect(0, 0, canvas.width, canvas.height); + render(); + } + + return ( +
+
+ + + + +
+
+
+ {(tool === "brush" || tool === "shape") && + + } + + {(tool === "brush" || tool === "erase" || tool === "shape") && + + } +
+ {(tool === "crop") && } +
+ {(tool === "shape") && (<> +