diff options
-rw-r--r-- | src/api/ContextMenu.ts | 2 | ||||
-rw-r--r-- | src/plugins/apiContextMenu.ts | 4 | ||||
-rw-r--r-- | src/plugins/emoteCloner.tsx | 130 | ||||
-rw-r--r-- | src/plugins/reverseImageSearch.tsx | 76 |
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); } }); |