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" ;
2023-12-06 04:37:42 +00:00
import { findByPropsLazy } from "@webpack" ;
2023-11-23 05:18:34 +00:00
import { Clipboard , ComponentDispatch } from "@webpack/common" ;
2023-04-08 01:51:37 +00:00
2023-12-06 04:37:42 +00:00
const ctxMenuCallbacks = findByPropsLazy ( "contextMenuCallbackNative" ) ;
2023-04-08 01:51:37 +00:00
async function fetchImage ( url : string ) {
const res = await fetch ( url ) ;
if ( res . status !== 200 ) return ;
return await res . blob ( ) ;
}
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
2024-01-17 23:39:34 +00:00
const MEDIA_PROXY_URL = "https://media.discordapp.net" ;
2024-02-15 08:20:27 +00:00
const CDN_URL = "cdn.discordapp.com" ;
2024-01-17 23:39:34 +00:00
2024-02-15 08:20:27 +00:00
function fixImageUrl ( urlString : string ) {
2024-01-17 23:39:34 +00:00
const url = new URL ( urlString ) ;
2024-02-15 08:20:27 +00:00
if ( url . host === CDN_URL ) return urlString ;
2024-01-17 23:39:34 +00:00
url . searchParams . delete ( "width" ) ;
url . searchParams . delete ( "height" ) ;
2024-02-15 08:20:27 +00:00
if ( url . origin === MEDIA_PROXY_URL ) {
url . host = CDN_URL ;
url . searchParams . delete ( "size" ) ;
url . searchParams . delete ( "quality" ) ;
url . searchParams . delete ( "format" ) ;
} else {
url . searchParams . set ( "quality" , "lossless" ) ;
}
2024-01-17 23:58:40 +00:00
2024-01-17 23:39:34 +00:00
return url . toString ( ) ;
}
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 ) {
window . removeEventListener ( "contextmenu" , ctxMenuCallbacks . contextMenuCallbackWeb ) ;
window . addEventListener ( "contextmenu" , ctxMenuCallbacks . contextMenuCallbackNative ) ;
this . changedListeners = true ;
}
} ,
stop() {
if ( this . changedListeners ) {
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 ==
2023-10-25 16:07:12 +00:00
match : /!\i\.isPlatformEmbedded/ ,
replace : "false"
2023-04-08 01:51:37 +00:00
} ,
{
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"' ,
2023-11-25 01:43:29 +00:00
all : true ,
2023-04-08 01:51:37 +00:00
predicate : ( ) = > settings . store . addBack ,
replacement : {
2023-11-25 01:43:29 +00:00
// return IS_DESKTOP ? React.createElement(Menu, ...)
match : /return \i\.\i(?=\?|&&)/ ,
2023-11-24 19:49:19 +00:00
replace : "return true"
2023-04-08 01:51:37 +00:00
}
} ,
// Add back link context menu
{
find : '"interactionUsernameProfile"' ,
predicate : ( ) = > settings . store . addBack ,
replacement : {
2023-10-25 16:07:12 +00:00
match : /if\((?="A"===\i\.tagName&&""!==\i\.textContent)/ ,
replace : "if(false&&"
2023-04-08 01:51:37 +00:00
}
} ,
// Add back slate / text input context menu
{
2023-10-25 16:07:12 +00:00
find : 'getElementById("slate-toolbar"' ,
2023-04-08 01:51:37 +00:00
predicate : ( ) = > settings . store . addBack ,
replacement : {
2023-10-25 16:07:12 +00:00
match : /(?<=handleContextMenu\(\i\)\{.{0,200}isPlatformEmbedded)\?/ ,
replace : "||true?"
2022-11-17 00:30:23 +00:00
}
2023-04-08 01:51:37 +00:00
} ,
{
2023-10-26 18:36:05 +00:00
find : ".SLASH_COMMAND_SUGGESTIONS_TOGGLED,{" ,
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 : {
2023-10-25 16:07:12 +00:00
match : /let\{text:\i=""/ ,
2023-06-25 16:22:13 +00:00
replace : "return [null,null];$&"
}
2024-01-05 21:12:13 +00:00
} ,
// Add back "Show My Camera" context menu
{
2024-03-27 13:29:58 +00:00
find : '"MediaEngineWebRTC");' ,
2024-01-05 21:12:13 +00:00
replacement : {
match : /supports\(\i\)\{switch\(\i\)\{case (\i).Features/ ,
replace : "$&.DISABLE_VIDEO:return true;case $1.Features"
}
2023-04-08 01:51:37 +00:00
}
] ,
async copyImage ( url : string ) {
2024-02-15 08:20:27 +00:00
url = fixImageUrl ( url ) ;
2024-01-17 23:39:34 +00:00
let imageData = await fetch ( url ) . then ( r = > r . blob ( ) ) ;
if ( imageData . type !== "image/png" ) {
const bitmap = await createImageBitmap ( imageData ) ;
const canvas = document . createElement ( "canvas" ) ;
canvas . width = bitmap . width ;
canvas . height = bitmap . height ;
canvas . getContext ( "2d" ) ! . drawImage ( bitmap , 0 , 0 ) ;
await new Promise < void > ( done = > {
canvas . toBlob ( data = > {
imageData = data ! ;
done ( ) ;
} , "image/png" ) ;
} ) ;
}
2024-01-13 18:05:01 +00:00
if ( IS_VESKTOP && VesktopNative . clipboard ) {
2024-01-17 23:39:34 +00:00
VesktopNative . clipboard . copyImage ( await imageData . arrayBuffer ( ) , url ) ;
2024-01-13 18:05:01 +00:00
return ;
2024-01-17 23:39:34 +00:00
} else {
navigator . clipboard . write ( [
new ClipboardItem ( {
"image/png" : imageData
} )
] ) ;
2024-01-13 18:05:01 +00:00
}
2023-04-08 01:51:37 +00:00
} ,
async saveImage ( url : string ) {
2024-02-15 08:20:27 +00:00
url = fixImageUrl ( url ) ;
2024-01-17 23:39:34 +00:00
2023-04-08 01:51:37 +00:00
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 ( ) ;
2023-11-23 05:18:34 +00:00
ComponentDispatch . dispatch ( "INSERT_TEXT" , { rawText : "" } ) ;
2023-04-08 01:51:37 +00:00
} ,
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
} ) ;