aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/api/ContextMenu.ts2
-rw-r--r--src/plugins/apiContextMenu.ts4
-rw-r--r--src/plugins/emoteCloner.tsx130
-rw-r--r--src/plugins/reverseImageSearch.tsx76
4 files changed, 108 insertions, 104 deletions
diff --git a/src/api/ContextMenu.ts b/src/api/ContextMenu.ts
index 6467117..9a8d7b6 100644
--- a/src/api/ContextMenu.ts
+++ b/src/api/ContextMenu.ts
@@ -90,7 +90,7 @@ export function removeGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallba
*/
export function findGroupChildrenByChildId(id: string, children: Array<React.ReactElement>, itemsArray?: Array<React.ReactElement>): Array<React.ReactElement> | null {
for (const child of children) {
- if (child === null) continue;
+ if (child == null) continue;
if (child.props?.id === id) return itemsArray ?? null;
diff --git a/src/plugins/apiContextMenu.ts b/src/plugins/apiContextMenu.ts
index 77afead..69dfde4 100644
--- a/src/plugins/apiContextMenu.ts
+++ b/src/plugins/apiContextMenu.ts
@@ -37,7 +37,7 @@ function listener(exports: any, id: number) {
all: true,
noWarn: true,
find: "navId:",
- replacement: {
+ replacement: [{
/** Regex explanation
* Use of https://blog.stevenlevithan.com/archives/mimic-atomic-groups to mimick atomic groups: (?=(...))\1
* Match ${id} and look behind it for the first match of `<variable name>=`: ${id}(?=(\i)=.+?)
@@ -45,7 +45,7 @@ function listener(exports: any, id: number) {
*/
match: RegExp(`(?=(${id}(?<=(\\i)=.+?).+?\\2\\.${key},{))\\1`, "g"),
replace: "$&contextMenuApiArguments:arguments,"
- }
+ }]
});
removeListener(listener);
diff --git a/src/plugins/emoteCloner.tsx b/src/plugins/emoteCloner.tsx
index e252fe5..7db5efd 100644
--- a/src/plugins/emoteCloner.tsx
+++ b/src/plugins/emoteCloner.tsx
@@ -16,12 +16,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import { migratePluginSettings, Settings } from "@api/settings";
+import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
+import { migratePluginSettings } from "@api/settings";
import { CheckedTextInput } from "@components/CheckedTextInput";
import { Devs } from "@utils/constants";
import Logger from "@utils/Logger";
import { Margins } from "@utils/margins";
-import { makeLazy } from "@utils/misc";
import { ModalContent, ModalHeader, ModalRoot, openModal } from "@utils/modal";
import definePlugin from "@utils/types";
import { findByCodeLazy, findByPropsLazy } from "@webpack";
@@ -176,72 +176,74 @@ function CloneModal({ id, name: emojiName, isAnimated }: { id: string; name: str
);
}
+const messageContextMenuPatch: NavContextMenuPatchCallback = (children, args) => {
+ if (!args?.[0]) return;
+ const { favoriteableId, emoteClonerDataAlt, itemHref, itemSrc, favoriteableType } = args[0];
+
+ if (!emoteClonerDataAlt || favoriteableType !== "emoji") return;
+
+ const name = emoteClonerDataAlt.match(/:(.*)(?:~\d+)?:/)?.[1];
+ if (!name || !favoriteableId) return;
+
+ const src = itemHref ?? itemSrc;
+ const isAnimated = new URL(src).pathname.endsWith(".gif");
+
+ const group = findGroupChildrenByChildId("save-image", children);
+ if (group && !group.some(child => child?.props?.id === "emote-cloner")) {
+ group.push((
+ <Menu.MenuItem
+ id="emote-cloner"
+ key="emote-cloner"
+ label="Clone"
+ action={() =>
+ openModal(modalProps => (
+ <ModalRoot {...modalProps}>
+ <ModalHeader>
+ <img
+ role="presentation"
+ aria-hidden
+ src={`${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/emojis/${favoriteableId}.${isAnimated ? "gif" : "png"}`}
+ alt=""
+ height={24}
+ width={24}
+ style={{ marginRight: "0.5em" }}
+ />
+ <Forms.FormText>Clone {name}</Forms.FormText>
+ </ModalHeader>
+ <ModalContent>
+ <CloneModal id={favoriteableId} name={name} isAnimated={isAnimated} />
+ </ModalContent>
+ </ModalRoot>
+ ))
+ }
+ >
+ </Menu.MenuItem>
+ ));
+ }
+};
+
migratePluginSettings("EmoteCloner", "EmoteYoink");
export default definePlugin({
name: "EmoteCloner",
description: "Adds a Clone context menu item to emotes to clone them your own server",
- authors: [Devs.Ven],
- dependencies: ["MenuItemDeobfuscatorAPI"],
-
- patches: [{
- // Literally copy pasted from ReverseImageSearch lol
- find: "open-native-link",
- replacement: {
- match: /id:"open-native-link".{0,200}\(\{href:(.{0,3}),.{0,200}\},"open-native-link"\)/,
- replace: "$&,$self.makeMenu(arguments[2])"
- },
-
- },
- // Also copy pasted from Reverse Image Search
- {
- // pass the target to the open link menu so we can grab its data
- find: "REMOVE_ALL_REACTIONS_CONFIRM_BODY,",
- predicate: makeLazy(() => !Settings.plugins.ReverseImageSearch.enabled),
- noWarn: true,
- replacement: {
- match: /(?<props>.).onHeightUpdate.{0,200}(.)=(.)=.\.url;.+?\(null!=\3\?\3:\2[^)]+/,
- replace: "$&,$<props>.target"
- }
- }],
-
- makeMenu(htmlElement: HTMLImageElement) {
- if (htmlElement?.dataset.type !== "emoji")
- return null;
-
- const { id } = htmlElement.dataset;
- const name = htmlElement.alt.match(/:(.*)(?:~\d+)?:/)?.[1];
-
- if (!name || !id)
- return null;
-
- const isAnimated = new URL(htmlElement.src).pathname.endsWith(".gif");
-
- return <Menu.MenuItem
- id="emote-cloner"
- key="emote-cloner"
- label="Clone"
- action={() =>
- openModal(modalProps => (
- <ModalRoot {...modalProps}>
- <ModalHeader>
- <img
- role="presentation"
- aria-hidden
- src={`${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/emojis/${id}.${isAnimated ? "gif" : "png"}`}
- alt=""
- height={24}
- width={24}
- style={{ marginRight: "0.5em" }}
- />
- <Forms.FormText>Clone {name}</Forms.FormText>
- </ModalHeader>
- <ModalContent>
- <CloneModal id={id} name={name} isAnimated={isAnimated} />
- </ModalContent>
- </ModalRoot>
- ))
+ authors: [Devs.Ven, Devs.Nuckyz],
+ dependencies: ["MenuItemDeobfuscatorAPI", "ContextMenuAPI"],
+
+ patches: [
+ {
+ find: ".Messages.MESSAGE_ACTIONS_MENU_LABEL",
+ replacement: {
+ match: /(?<=favoriteableType:\i,)(?<=(\i)\.getAttribute\("data-type"\).+?)/,
+ replace: (_, target) => `emoteClonerDataAlt:${target}.alt,`
}
- >
- </Menu.MenuItem>;
+ }
+ ],
+
+ start() {
+ addContextMenuPatch("message", messageContextMenuPatch);
},
+
+ stop() {
+ removeContextMenuPatch("message", messageContextMenuPatch);
+ }
});
diff --git a/src/plugins/reverseImageSearch.tsx b/src/plugins/reverseImageSearch.tsx
index 4d9f040..6335fbd 100644
--- a/src/plugins/reverseImageSearch.tsx
+++ b/src/plugins/reverseImageSearch.tsx
@@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { Menu } from "@webpack/common";
@@ -29,39 +30,21 @@ const Engines = {
ImgOps: "https://imgops.com/start?url="
};
-export default definePlugin({
- name: "ReverseImageSearch",
- description: "Adds ImageSearch to image context menus",
- authors: [Devs.Ven],
- dependencies: ["MenuItemDeobfuscatorAPI"],
- patches: [{
- find: "open-native-link",
- replacement: {
- match: /id:"open-native-link".{0,200}\(\{href:(.{0,3}),.{0,200}\},"open-native-link"\)/,
- replace: (m, src) =>
- `${m},Vencord.Plugins.plugins.ReverseImageSearch.makeMenu(${src}, arguments[2])`
- }
- }, {
- // pass the target to the open link menu so we can check if it's an image
- find: ".Messages.MESSAGE_ACTIONS_MENU_LABEL",
- replacement: [
- {
- match: /ariaLabel:\i\.Z\.Messages\.MESSAGE_ACTIONS_MENU_LABEL/,
- replace: "$&,_vencordTarget:arguments[0].target"
- },
- {
- // var f = props.itemHref, .... MakeNativeMenu(null != f ? f : blah)
- match: /(\i)=\i\.itemHref,.+?\(null!=\1\?\1:.{1,10}(?=\))/,
- replace: "$&,arguments[0]._vencordTarget"
- }
- ]
- }],
+function search(src: string, engine: string) {
+ open(engine + encodeURIComponent(src), "_blank");
+}
+
+const imageContextMenuPatch: NavContextMenuPatchCallback = (children, args) => {
+ if (!args?.[0]) return;
+ const { reverseImageSearchType, itemHref, itemSrc } = args[0];
- makeMenu(src: string, target: HTMLElement) {
- if (target && !(target instanceof HTMLImageElement) && target.attributes["data-role"]?.value !== "img")
- return null;
+ if (!reverseImageSearchType || reverseImageSearchType !== "img") return;
- return (
+ const src = itemHref ?? itemSrc;
+
+ const group = findGroupChildrenByChildId("save-image", children);
+ if (group && !group.some(child => child?.props?.id === "search-image")) {
+ group.push((
<Menu.MenuItem
label="Search Image"
key="search-image"
@@ -74,7 +57,7 @@ export default definePlugin({
key={key}
id={key}
label={engine}
- action={() => this.search(src, Engines[engine])}
+ action={() => search(src, Engines[engine])}
/>
);
})}
@@ -82,14 +65,33 @@ export default definePlugin({
key="search-image-all"
id="search-image-all"
label="All"
- action={() => Object.values(Engines).forEach(e => this.search(src, e))}
+ action={() => Object.values(Engines).forEach(e => search(src, e))}
/>
</Menu.MenuItem>
- );
+ ));
+ }
+};
+
+export default definePlugin({
+ name: "ReverseImageSearch",
+ description: "Adds ImageSearch to image context menus",
+ authors: [Devs.Ven, Devs.Nuckyz],
+ dependencies: ["MenuItemDeobfuscatorAPI", "ContextMenuAPI"],
+ patches: [
+ {
+ find: ".Messages.MESSAGE_ACTIONS_MENU_LABEL",
+ replacement: {
+ match: /(?<=favoriteableType:\i,)(?<=(\i)\.getAttribute\("data-type"\).+?)/,
+ replace: (_, target) => `reverseImageSearchType:${target}.getAttribute("data-role"),`
+ }
+ }
+ ],
+
+ start() {
+ addContextMenuPatch("message", imageContextMenuPatch);
},
- // openUrl is a mangled export, so just match it in the module and pass it
- search(src: string, engine: string) {
- open(engine + encodeURIComponent(src), "_blank");
+ stop() {
+ removeContextMenuPatch("message", imageContextMenuPatch);
}
});