263 lines
8.6 KiB
TypeScript
263 lines
8.6 KiB
TypeScript
/*
|
|
* 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 <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
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(
|
|
<Menu.MenuGroup id="image-zoom">
|
|
<Menu.MenuCheckboxItem
|
|
id="vc-square"
|
|
label="Square Lens"
|
|
checked={square}
|
|
action={() => {
|
|
settings.store.square = !square;
|
|
}}
|
|
/>
|
|
<Menu.MenuCheckboxItem
|
|
id="vc-nearest-neighbour"
|
|
label="Nearest Neighbour"
|
|
checked={nearestNeighbour}
|
|
action={() => {
|
|
settings.store.nearestNeighbour = !nearestNeighbour;
|
|
}}
|
|
/>
|
|
<Menu.MenuControlItem
|
|
id="vc-zoom"
|
|
label="Zoom"
|
|
control={(props, ref) => (
|
|
<Menu.MenuSliderControl
|
|
ref={ref}
|
|
{...props}
|
|
minValue={1}
|
|
maxValue={50}
|
|
value={settings.store.zoom}
|
|
onChange={debounce((value: number) => settings.store.zoom = value, 100)}
|
|
/>
|
|
)}
|
|
/>
|
|
<Menu.MenuControlItem
|
|
id="vc-size"
|
|
label="Lens Size"
|
|
control={(props, ref) => (
|
|
<Menu.MenuSliderControl
|
|
ref={ref}
|
|
{...props}
|
|
minValue={50}
|
|
maxValue={1000}
|
|
value={settings.store.size}
|
|
onChange={debounce((value: number) => settings.store.size = value, 100)}
|
|
/>
|
|
)}
|
|
/>
|
|
<Menu.MenuControlItem
|
|
id="vc-zoom-speed"
|
|
label="Zoom Speed"
|
|
control={(props, ref) => (
|
|
<Menu.MenuSliderControl
|
|
ref={ref}
|
|
{...props}
|
|
minValue={0.1}
|
|
maxValue={5}
|
|
value={settings.store.zoomSpeed}
|
|
onChange={debounce((value: number) => settings.store.zoomSpeed = value, 100)}
|
|
renderValue={(value: number) => `${value.toFixed(3)}x`}
|
|
/>
|
|
)}
|
|
/>
|
|
</Menu.MenuGroup>
|
|
);
|
|
};
|
|
|
|
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<MagnifierProps & JSX.IntrinsicAttributes> | 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 = <Magnifier size={settings.store.size} zoom={settings.store.zoom} instance={instance} />;
|
|
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();
|
|
}
|
|
});
|