- // ==UserScript==
- // @name Discord Context Menu Extender
- // @namespace http://tampermonkey.net/
- // @version 1.0.2
- // @description Adding Discord desktop action buttons in context menu in discord web version
- // @author DoctorDeathDDracula & Sticky
- // @supportURL https://discord.gg/sHj5UauJZ4
- // @license MIT
- // @match *://discord.com/*
- // @icon https://www.google.com/s2/favicons?sz=64&domain=discord.com
- // @grant unsafeWindow
- // @grant GM_log
- // @grant GM_download
- // @grant GM_xmlhttpRequest
- // @grant GM_setClipboard
- // @connect discord.com
- // @connect cdn.discordapp.com
- // @connect media.discordapp.net
- // @connect images-ext-1.discordapp.net
- // @connect images-ext-2.discordapp.net
- // @run-at document-start
- // ==/UserScript==
-
-
- (function() {
- 'use strict';
-
- const console = {
- log: GM_log
- };
-
- class DiscordContextMenuExtender {
-
- constructor() {
- this.compatibilityItems = ['ClipboardItem', 'MutationObserver'];
- this.currentElement = null;
- this.style = `.my-focused{background-color:var(--brand-experiment-560);color:#fff;} .my-focused > *{color:#fff !important;}`;
- }
-
- cc(tag, options = {}, parent = false, init = undefined) {
- const children = options.children || [];
- delete options.children;
- const element = Object.assign(document.createElement(tag), options);
- for (const child of children) element.appendChild(child);
- if (typeof init == 'function') init(element);
- return parent && parent.nodeType === Node.ELEMENT_NODE ? parent.appendChild(element) : element;
- }
-
- ss(selector, searchIn = document, all = false) {
- return all ? searchIn.querySelectorAll(selector) : searchIn.querySelector(selector);
- }
-
- checkForСompatibility() {
- for (let item of this.compatibilityItems) {
- if (!(item in unsafeWindow)) return false;
- }
- return true;
- }
-
- init() {
- if (!this.checkForСompatibility()) return alert('Sorry, your browser does not support this feature');
- document.addEventListener('DOMContentLoaded', this.onDOMContentLoaded.bind(this));
- unsafeWindow.addEventListener('pointerdown', this.onPointerDown.bind(this));
- this.shutDownConsole();
- this.setTriggerOnElement('#message', 'addedNodes', this.onContextMenu.bind(this));
- }
-
- onDOMContentLoaded() {
- this.addStyles();
- }
-
- addStyles() {
- this.cc('style', {
- textContent: this.style
- }, document.head);
- }
-
- shutDownConsole() {
- for (const key in Object.getOwnPropertyDescriptors(unsafeWindow.console)) unsafeWindow.console[key] = () => {};
- }
-
- onPointerDown(e) {
- if (e.which == 3) this.currentElement = e.target;
- }
-
- onContextMenu(menu) {
- const currentMessageBlock = this.currentElement.closest('[id*="chat-messages"]');
- const media = this.currentElement.closest('[class*=embedMedia]') || this.currentElement.closest('[class*="messageAttachment"]');
- const group = this.ss('[role="group"]', menu, true)[1];
-
- if (media !== null) {
- let mediaType;
- if (mediaType = this.ss('img', media)) {
- group.after(this.cc('div', {
- role: "group",
- className: this.ss('[role="group"]', menu, true).className,
- children: [
- this.generateSplitLine(),
- this.generateContextButton(mediaType, menu, group, 'Copy Image', async() => {
- await this.copyImage(mediaType);
- menu.remove();
- }),
- this.generateContextButton(mediaType, menu, group, 'Save Image', async() => {
- await this.downloadImage(mediaType);
- menu.remove();
- }),
- this.generateSplitLine(),
- this.generateContextButton(mediaType, menu, group, 'Copy Link', () => {
- GM_setClipboard(this.getFullOfDiscordImage(mediaType.src).split('?')[0]);
- menu.remove();
- }),
- this.generateContextButton(mediaType, menu, group, 'Open Link', async() => {
- window.open(this.getFullOfDiscordImage(mediaType.src).split('?')[0], '_blank');
- menu.remove();
- })
- ]
- }));
- this.fixMenuPosition(menu);
- } else if ((mediaType = this.ss('video:not([aria-label="GIF"])', media))) {
- group.after(this.cc('div', {
- role: "group",
- className: this.ss('[role="group"]', menu, true).className,
- children: [
- this.generateSplitLine(),
- this.generateContextButton(mediaType, menu, group, 'Copy Link', () => {
- GM_setClipboard(this.getFullOfDiscordImage(mediaType.src).split('?')[0]);
- menu.remove();
- }),
- this.generateContextButton(mediaType, menu, group, 'Open Link', async() => {
- window.open(this.getFullOfDiscordImage(mediaType.src).split('?')[0], '_blank');
- menu.remove();
- })
- ]
- }));
- this.fixMenuPosition(menu);
- } else if ((mediaType = this.ss('video[aria-label="GIF"]', media))) {
- group.after(this.cc('div', {
- role: "group",
- className: this.ss('[role="group"]', menu, true).className,
- children: [
- this.generateSplitLine(),
- this.generateContextButton(mediaType, menu, group, 'Copy Link', () => {
- GM_setClipboard(this.getFullOfDiscordImage(mediaType.src).split('?')[0]);
- menu.remove();
- }),
- this.generateContextButton(mediaType, menu, group, 'Open Link', async() => {
- window.open(this.getFullOfDiscordImage(mediaType.src).split('?')[0], '_blank');
- menu.remove();
- })
- ]
- }));
- this.fixMenuPosition(menu);
- } else {
- console.log('unknown type');
- }
- } else {
- console.log('null');
- }
- }
-
- fixMenuPosition(menu) {
- const rectM = menu.getBoundingClientRect();
- if (rectM.y + rectM.height > window.innerHeight) {
- const def = rectM.y + rectM.height - window.innerHeight;
- const top = menu.parentNode.style.top.replace('px', '');
- menu.parentNode.style.top = top - def + 'px';
- }
- }
-
- generateSplitLine() {
- return this.cc('div', {
- role: 'separator',
- style: 'box-sizing:border-box;margin:4px;border-bottom:1px solid var(--background-modifier-accent);'
- });
- }
-
- generateContextButton(mediaType, menu, group, text, onclick) {
- return this.cc('div', {
- role: "group",
- children: [
- this.cc('div', {
- className: group.children[1].className,
- children: [
- this.cc('div', {
- textContent: text,
- style: group.children[1].firstChild.className,
- })
- ],
- onclick: onclick,
- onmouseenter: function() {
- const focused = menu.querySelector('[class*="focused"]');
- if (focused) focused.classList.remove(Array.from(focused.classList).find(_class => _class.startsWith('focused')));
- this.classList.add('my-focused');
- },
- onmouseleave: function() {
- this.classList.remove('my-focused');
- }
- })
- ]
- })
- }
-
- setTriggerOnElement(selector, action, callback, once, searchIn) {
- const observer = new MutationObserver(function(mutations) {
- for (const mutation of mutations) {
- const nodes = mutation[action] || [];
- for (const node of nodes) {
- const element = node.matches && node.matches(selector) ? node : (node.querySelector ? node.querySelector(selector) : null);
- if (element) {
- if (once) {
- observer.disconnect();
- return callback(element);
- } else {
- callback(element);
- }
- }
- }
- }
- });
-
- observer.observe(searchIn || document, {
- attributes: false,
- childList: true,
- subtree: true
- });
-
- return observer;
- }
-
- async downloadImage(image) {
- const blob = await this.requestImage(this.getFullOfDiscordImage(image.src), 'blob');
- const dataURL = await this.blobToBase64(blob);
- this.cc('a', {
- href: dataURL,
- download: image.src.split('/').pop().split('?')[0]
- }).click();
- }
-
- async imageToPNGBlob(blob) {
- const dataURL = await this.blobToBase64(blob);
- console.log(dataURL);
- }
-
- blobToBase64(blob) {
- return new Promise((resolve) => {
- const reader = new FileReader();
- reader.onloadend = () => resolve(reader.result);
- reader.readAsDataURL(blob);
- });
- }
-
- async copyImage(image) {
- await (this.getExtension(image.src) != 'png' ? this.copyNonPNGImage(image) : this.copyPNGImage(image));
- }
-
- async copyNonPNGImage(image) {
- const blob = await this.requestImage(image.src, 'blob');
- const dataURL = await this.blobToBase64(blob);
- const pngBlob = await this.imageDataURLToPNGBlob(dataURL);
- this.copyImageFromBlob(pngBlob);
- }
-
- imageDataURLToPNGBlob(dataURL) {
- return new Promise(resolve => {
- const img = this.cc('img', {
- src: dataURL,
- onload: () => {
- const canvas = this.cc('canvas', {
- style: 'position:fixed;top:0px;left:0px;z-index:1000;',
- width: img.width,
- height: img.height
- });
- const context = canvas.getContext('2d');
- context.drawImage(img, 0, 0, img.width, img.height);
- canvas.toBlob(blob => resolve(blob));
- }
- });
- });
- }
-
- async copyPNGImage(image) {
- const imageBlob = await this.requestImage(this.getFullOfDiscordImage(image.src), 'blob');
- this.copyImageFromBlob(imageBlob);
- }
-
- copyImageFromBlob(blob) {
- const item = new unsafeWindow.ClipboardItem({
- "image/png": blob
- });
- navigator.clipboard.write([item]);
- }
-
- requestImage(imageURL, responseType) {
- return new Promise(function(resolve, reject) {
- GM_xmlhttpRequest({
- methods: "GET",
- responseType: responseType,
- url: imageURL,
- onload: function(data) {
- resolve(data.response);
- },
- onerror: function(e) {
- reject(e);
- }
- });
- });
- }
-
- getFullOfDiscordImage(imageURL) {
- return imageURL.replace('media.discordapp.net', 'cdn.discordapp.com');
- }
-
- getNoneDefaultStyle(node) {
- const styles = [];
- const supportElement = this.cc(node.tagName, {
- visible: false
- }, document.body);
- const elementStyles = window.getComputedStyle(node);
- const defaultStyles = window.getComputedStyle(supportElement);
- for (const key of elementStyles) {
- if (elementStyles[key] !== defaultStyles[key] && defaultStyles[key] !== '') styles.push([key, elementStyles[key]]);
- }
- supportElement.remove();
- return styles;
- }
-
- packInlineStyle(style) {
- return style.reduce(function(pre, cur) {
- return pre + ";" + cur[0] + ":" + cur[1];
- }, "") + ";";
- }
-
- getExtension(filename) {
- return filename.split('.').pop().split('?')[0];
- }
- }
-
- const DCME = new DiscordContextMenuExtender();
- DCME.init();
-
- })();