2022-11-17 00:30:23 +00:00
/ *
* Vencord , a modification for Discord ' s desktop app
* Copyright ( c ) 2022 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 / > .
* /
2023-05-05 23:36:00 +00:00
import { definePluginSettings } from "@api/Settings" ;
2022-11-28 12:37:55 +00:00
import { Devs } from "@utils/constants" ;
2023-04-08 01:51:37 +00:00
import definePlugin , { OptionType } from "@utils/types" ;
import { saveFile } from "@utils/web" ;
import { findByProps , findLazy } from "@webpack" ;
import { Clipboard } from "@webpack/common" ;
async function fetchImage ( url : string ) {
const res = await fetch ( url ) ;
if ( res . status !== 200 ) return ;
return await res . blob ( ) ;
}
const MiniDispatcher = findLazy ( m = > m . emitter ? . _events ? . INSERT_TEXT ) ;
const settings = definePluginSettings ( {
// This needs to be all in one setting because to enable any of these, we need to make Discord use their desktop context
// menu handler instead of the web one, which breaks the other menus that aren't enabled
addBack : {
type : OptionType . BOOLEAN ,
description : "Add back the Discord context menus for images, links and the chat input bar" ,
// Web slate menu has proper spellcheck suggestions and image context menu is also pretty good,
2023-08-15 23:55:56 +00:00
// so disable this by default. Vesktop just doesn't, so enable by default
default : IS_VESKTOP ,
2023-04-08 01:51:37 +00:00
restartNeeded : true
}
} ) ;
2022-11-17 00:30:23 +00:00
export default definePlugin ( {
name : "WebContextMenus" ,
2023-06-25 16:22:13 +00:00
description : "Re-adds context menus missing in the web version of Discord: Links & Images (Copy/Open Link/Image), Text Area (Copy, Cut, Paste, SpellCheck)" ,
2022-11-17 00:30:23 +00:00
authors : [ Devs . Ven ] ,
2023-03-11 13:18:32 +00:00
enabledByDefault : true ,
2023-08-15 23:55:56 +00:00
required : IS_VESKTOP ,
2022-11-17 00:30:23 +00:00
2023-04-08 01:51:37 +00:00
settings ,
start() {
if ( settings . store . addBack ) {
const ctxMenuCallbacks = findByProps ( "contextMenuCallbackNative" ) ;
window . removeEventListener ( "contextmenu" , ctxMenuCallbacks . contextMenuCallbackWeb ) ;
window . addEventListener ( "contextmenu" , ctxMenuCallbacks . contextMenuCallbackNative ) ;
this . changedListeners = true ;
}
} ,
stop() {
if ( this . changedListeners ) {
const ctxMenuCallbacks = findByProps ( "contextMenuCallbackNative" ) ;
window . removeEventListener ( "contextmenu" , ctxMenuCallbacks . contextMenuCallbackNative ) ;
window . addEventListener ( "contextmenu" , ctxMenuCallbacks . contextMenuCallbackWeb ) ;
}
} ,
patches : [
// Add back Copy & Open Link
{
// There is literally no reason for Discord to make this Desktop only.
// The only thing broken is copy, but they already have a different copy function
// with web support????
find : "open-native-link" ,
replacement : [
{
// if (IS_DESKTOP || null == ...)
match : /if\(!\i\.\i\|\|null==/ ,
replace : "if(null=="
} ,
// Fix silly Discord calling the non web support copy
{
2023-08-12 03:27:59 +00:00
match : /\i\.\i\.copy/ ,
2023-04-08 01:51:37 +00:00
replace : "Vencord.Webpack.Common.Clipboard.copy"
}
]
} ,
// Add back Copy & Save Image
{
find : 'id:"copy-image"' ,
replacement : [
{
// if (!IS_WEB || null ==
match : /if\(!\i\.\i\|\|null==/ ,
replace : "if(null=="
} ,
{
2023-08-12 03:27:59 +00:00
match : /return\s*?\[\i\.\i\.canCopyImage\(\)/ ,
2023-04-08 01:51:37 +00:00
replace : "return [true"
} ,
{
match : /(?<=COPY_IMAGE_MENU_ITEM,)action:/ ,
replace : "action:()=>$self.copyImage(arguments[0]),oldAction:"
} ,
{
match : /(?<=SAVE_IMAGE_MENU_ITEM,)action:/ ,
replace : "action:()=>$self.saveImage(arguments[0]),oldAction:"
} ,
]
} ,
// Add back image context menu
{
find : 'navId:"image-context"' ,
predicate : ( ) = > settings . store . addBack ,
replacement : {
// return IS_DESKTOP ? React.createElement(Menu, ...)
2023-04-28 17:32:29 +00:00
match : /return \i\.\i\?/ ,
2023-04-08 01:51:37 +00:00
replace : "return true?"
}
} ,
// Add back link context menu
{
find : '"interactionUsernameProfile"' ,
predicate : ( ) = > settings . store . addBack ,
replacement : {
match : /if\("A"===\i\.tagName&&""!==\i\.textContent\)/ ,
replace : "if(false)"
}
} ,
// Add back slate / text input context menu
{
find : '"slate-toolbar"' ,
predicate : ( ) = > settings . store . addBack ,
replacement : {
match : /(?<=\.handleContextMenu=.+?"bottom";)\i\.\i\?/ ,
replace : "true?"
2022-11-17 00:30:23 +00:00
}
2023-04-08 01:51:37 +00:00
} ,
{
2023-06-25 16:22:13 +00:00
find : 'navId:"textarea-context"' ,
all : true ,
2023-04-08 01:51:37 +00:00
predicate : ( ) = > settings . store . addBack ,
replacement : [
{
2023-05-11 00:31:07 +00:00
// if (!IS_DESKTOP) return null;
match : /if\(!\i\.\i\)return null;/ ,
replace : ""
2023-04-08 01:51:37 +00:00
} ,
{
// Change calls to DiscordNative.clipboard to us instead
2023-08-12 03:27:59 +00:00
match : /\b\i\.\i\.(copy|cut|paste)/g ,
2023-04-08 01:51:37 +00:00
replace : "$self.$1"
}
]
2023-06-25 16:22:13 +00:00
} ,
{
find : '"add-to-dictionary"' ,
predicate : ( ) = > settings . store . addBack ,
replacement : {
match : /var \i=\i\.text,/ ,
replace : "return [null,null];$&"
}
2023-04-08 01:51:37 +00:00
}
] ,
async copyImage ( url : string ) {
2023-04-08 21:05:38 +00:00
// Clipboard only supports image/png, jpeg and similar won't work. Thus, we need to convert it to png
// via canvas first
const img = new Image ( ) ;
img . onload = ( ) = > {
const canvas = document . createElement ( "canvas" ) ;
canvas . width = img . naturalWidth ;
canvas . height = img . naturalHeight ;
canvas . getContext ( "2d" ) ! . drawImage ( img , 0 , 0 ) ;
canvas . toBlob ( data = > {
navigator . clipboard . write ( [
new ClipboardItem ( {
"image/png" : data !
} )
] ) ;
} , "image/png" ) ;
} ;
img . crossOrigin = "anonymous" ;
img . src = url ;
2023-04-08 01:51:37 +00:00
} ,
async saveImage ( url : string ) {
const data = await fetchImage ( url ) ;
if ( ! data ) return ;
2023-04-08 20:57:34 +00:00
const name = new URL ( url ) . pathname . split ( "/" ) . pop ( ) ! ;
2023-04-08 01:51:37 +00:00
const file = new File ( [ data ] , name , { type : data . type } ) ;
saveFile ( file ) ;
} ,
copy() {
const selection = document . getSelection ( ) ;
if ( ! selection ) return ;
Clipboard . copy ( selection . toString ( ) ) ;
} ,
cut() {
this . copy ( ) ;
MiniDispatcher . dispatch ( "INSERT_TEXT" , { rawText : "" } ) ;
} ,
async paste() {
2023-09-27 02:13:40 +00:00
const clip = ( await navigator . clipboard . read ( ) ) [ 0 ] ;
if ( ! clip ) return ;
2023-04-08 01:51:37 +00:00
const data = new DataTransfer ( ) ;
2023-09-27 02:13:40 +00:00
for ( const type of clip . types ) {
if ( type === "image/png" ) {
const file = new File ( [ await clip . getType ( type ) ] , "unknown.png" , { type } ) ;
data . items . add ( file ) ;
} else if ( type === "text/plain" ) {
const blob = await clip . getType ( type ) ;
data . setData ( type , await blob . text ( ) ) ;
}
}
2023-04-08 01:51:37 +00:00
document . dispatchEvent (
new ClipboardEvent ( "paste" , {
clipboardData : data
} )
) ;
}
2022-11-17 00:30:23 +00:00
} ) ;