/* * Vencord, a modification for Discord's desktop app * Copyright (c) 2023 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 . */ import { NavContextMenuPatchCallback } from "@api/ContextMenu"; import { definePluginSettings } from "@api/Settings"; import { disableStyle, enableStyle } from "@api/Styles"; import { makeRange } from "@components/PluginSettings/components"; import { debounce } from "@shared/debounce"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; import { Menu, React, ReactDOM } from "@webpack/common"; import type { Root } from "react-dom/client"; import { Magnifier, MagnifierProps } from "./components/Magnifier"; import { ELEMENT_ID } from "./constants"; import styles from "./styles.css?managed"; export const settings = definePluginSettings({ saveZoomValues: { type: OptionType.BOOLEAN, description: "Whether to save zoom and lens size values", default: true, }, invertScroll: { type: OptionType.BOOLEAN, description: "Invert scroll", default: true, }, nearestNeighbour: { type: OptionType.BOOLEAN, description: "Use Nearest Neighbour Interpolation when scaling images", default: false, }, square: { type: OptionType.BOOLEAN, description: "Make the lens square", default: false, }, zoom: { description: "Zoom of the lens", type: OptionType.SLIDER, markers: makeRange(1, 50, 4), default: 2, stickToMarkers: false, }, size: { description: "Radius / Size of the lens", type: OptionType.SLIDER, markers: makeRange(50, 1000, 50), default: 100, stickToMarkers: false, }, zoomSpeed: { description: "How fast the zoom / lens size changes", type: OptionType.SLIDER, markers: makeRange(0.1, 5, 0.2), default: 0.5, stickToMarkers: false, }, }); const imageContextMenuPatch: NavContextMenuPatchCallback = children => { const { square, nearestNeighbour } = settings.use(["square", "nearestNeighbour"]); children.push( { settings.store.square = !square; }} /> { settings.store.nearestNeighbour = !nearestNeighbour; }} /> ( settings.store.zoom = value, 100)} /> )} /> ( settings.store.size = value, 100)} /> )} /> ( settings.store.zoomSpeed = value, 100)} renderValue={(value: number) => `${value.toFixed(3)}x`} /> )} /> ); }; export default definePlugin({ name: "ImageZoom", description: "Lets you zoom in to images and gifs. Use scroll wheel to zoom in and shift + scroll wheel to increase lens radius / size", authors: [Devs.Aria], tags: ["ImageUtilities"], patches: [ { find: "Messages.OPEN_IN_BROWSER", replacement: { // there are 2 image thingies. one for carosuel and one for the single image. // so thats why i added global flag. // also idk if this patch is good, should it be more specific? // https://regex101.com/r/xfvNvV/1 match: /return.{1,200}\.wrapper.{1,200}src:\i,/g, replace: `$&id: '${ELEMENT_ID}',` } }, { find: "handleImageLoad=", replacement: [ { match: /placeholderVersion:\i,/, replace: "...$self.makeProps(this),$&" }, { match: /componentDidMount\(\){/, replace: "$&$self.renderMagnifier(this);", }, { match: /componentWillUnmount\(\){/, replace: "$&$self.unMountMagnifier();" } ] }, { find: ".carouselModal", replacement: { match: /(?<=\.carouselModal.{0,100}onClick:)\i,/, replace: "()=>{}," } } ], settings, contextMenus: { "image-context": imageContextMenuPatch }, // to stop from rendering twice /shrug currentMagnifierElement: null as React.FunctionComponentElement | null, element: null as HTMLDivElement | null, Magnifier, root: null as Root | null, makeProps(instance) { return { onMouseOver: () => this.onMouseOver(instance), onMouseOut: () => this.onMouseOut(instance), onMouseDown: (e: React.MouseEvent) => this.onMouseDown(e, instance), onMouseUp: () => this.onMouseUp(instance), id: instance.props.id, }; }, renderMagnifier(instance) { if (instance.props.id === ELEMENT_ID) { if (!this.currentMagnifierElement) { this.currentMagnifierElement = ; this.root = ReactDOM.createRoot(this.element!); this.root.render(this.currentMagnifierElement); } } }, unMountMagnifier() { this.root?.unmount(); this.currentMagnifierElement = null; this.root = null; }, onMouseOver(instance) { instance.setState((state: any) => ({ ...state, mouseOver: true })); }, onMouseOut(instance) { instance.setState((state: any) => ({ ...state, mouseOver: false })); }, onMouseDown(e: React.MouseEvent, instance) { if (e.button === 0 /* left */) instance.setState((state: any) => ({ ...state, mouseDown: true })); }, onMouseUp(instance) { instance.setState((state: any) => ({ ...state, mouseDown: false })); }, start() { enableStyle(styles); this.element = document.createElement("div"); this.element.classList.add("MagnifierContainer"); document.body.appendChild(this.element); }, stop() { disableStyle(styles); // so componenetWillUnMount gets called if Magnifier component is still alive this.root && this.root.unmount(); this.element?.remove(); } });