From 981f77b9c0bcc34061f090fe084a24526bbe12b1 Mon Sep 17 00:00:00 2001 From: _Lighty_ Date: Tue, 19 May 2020 17:27:02 +0200 Subject: [PATCH] STR v2.1.0 --- Plugins/SaveToRedux/CHANGELOG.md | 4 + Plugins/SaveToRedux/SaveToRedux.plugin.js | 838 +++++++++++++--------- 2 files changed, 495 insertions(+), 347 deletions(-) diff --git a/Plugins/SaveToRedux/CHANGELOG.md b/Plugins/SaveToRedux/CHANGELOG.md index ec7abda..7c3247d 100644 --- a/Plugins/SaveToRedux/CHANGELOG.md +++ b/Plugins/SaveToRedux/CHANGELOG.md @@ -1,4 +1,8 @@ # [SaveToRedux](https://1lighty.github.io/BetterDiscordStuff/?plugin=SaveToRedux "SaveToRedux") Changelog +### 2.1.0 +- Fixed plugin after the new context menu changes. If you notice any bugs, [join my support server](https://discord.gg/NYvWdN5) +- Fixed incorrect slashes used on Windows. + ### 2.0.14 - Fixed plugin being broken. diff --git a/Plugins/SaveToRedux/SaveToRedux.plugin.js b/Plugins/SaveToRedux/SaveToRedux.plugin.js index 130c1e7..5cb0002 100644 --- a/Plugins/SaveToRedux/SaveToRedux.plugin.js +++ b/Plugins/SaveToRedux/SaveToRedux.plugin.js @@ -41,7 +41,7 @@ var SaveToRedux = (() => { twitter_username: '' } ], - version: '2.0.14', + version: '2.1.0', description: 'Allows you to save images, videos, profile icons, server icons, reactions, emotes and custom status emotes to any folder quickly, as well as install plugins from direct links.', github: 'https://github.com/1Lighty', github_raw: 'https://raw.githubusercontent.com/1Lighty/BetterDiscordPlugins/master/Plugins/SaveToRedux/SaveToRedux.plugin.js' @@ -50,7 +50,7 @@ var SaveToRedux = (() => { { title: 'fixed', type: 'fixed', - items: ['Fixed plugin being broken.'] + items: ['Fixed plugin after the new context menu changes. If you notice any bugs, [join my support server](https://discord.gg/NYvWdN5)', 'Fixed incorrect slashes used on Windows.'] } ], defaultConfig: [ @@ -103,8 +103,10 @@ var SaveToRedux = (() => { /* Build */ const buildPlugin = ([Plugin, Api]) => { - const { Settings, Modals, Utilities, WebpackModules, DiscordModules, DiscordClasses, ReactComponents, DiscordAPI, Logger, Patcher, PluginUpdater, PluginUtilities } = Api; - const { React, ContextMenuActions, GuildStore, DiscordConstants, Dispatcher, SwitchRow, EmojiUtils, RadioGroup, EmojiInfo, ModalStack } = DiscordModules; + const { Settings, Modals, Utilities, WebpackModules, DiscordModules, DiscordClasses, ReactComponents, DiscordAPI, Logger, PluginUpdater, PluginUtilities, ReactTools } = Api; + const { React, ContextMenuActions, GuildStore, DiscordConstants, Dispatcher, SwitchRow, EmojiUtils, EmojiStore, RadioGroup, EmojiInfo, ModalStack } = DiscordModules; + const Patcher = XenoLib.createSmartPatcher(Api.Patcher); + const TextComponent = WebpackModules.getByDisplayName('Text'); const getEmojiURL = Utilities.getNestedProp(WebpackModules.getByProps('getEmojiURL'), 'getEmojiURL'); const showAlertModal = Utilities.getNestedProp( @@ -249,12 +251,7 @@ var SaveToRedux = (() => { if ('string' != typeof r) throw new Error('Input must be string'); var l = Buffer.byteLength.bind(Buffer), a = o.bind(null, l), - p = r - .replace(t, n) - .replace(u, n) - .replace(i, n) - .replace(f, n) - .replace(c, n); + p = r.replace(t, n).replace(u, n).replace(i, n).replace(f, n).replace(c, n); return a(p, e.extLength); } var t = /[\/\?<>\\:\*\|"]/g, @@ -283,10 +280,16 @@ var SaveToRedux = (() => { } } + const isImage = e => /\.{0,1}(png|jpe?g|webp|gif|svg)$/i.test(e); + const isVideo = e => /\.{0,1}(mp4|webm|mov)$/i.test(e); + const isAudio = e => /\.{0,1}(mp3|ogg|wav|flac|m4a)$/i.test(e); + + const useIdealExtensions = url => (url.indexOf('/a_') !== -1 ? url.replace('.webp', '.gif').replace('.png', '.gif') : url.replace('.webp', '.png')); + return class SaveToRedux extends Plugin { constructor() { super(); - XenoLib.DiscordUtils.bindAll(this, ['handleContextMenu', 'formatFilename']); + XenoLib.DiscordUtils.bindAll(this, ['formatFilename']); XenoLib.changeName(__filename, 'SaveToRedux'); const oOnStart = this.onStart.bind(this); this.onStart = () => { @@ -335,7 +338,6 @@ var SaveToRedux = (() => { onStop() { this.promises.state.cancelled = true; Patcher.unpatchAll(); - XenoLib.unpatchContext(this.handleContextMenu); PluginUtilities.removeStyle(this.short + '-CSS'); } @@ -366,8 +368,47 @@ var SaveToRedux = (() => { /* PATCHES */ patchAll() { - XenoLib.patchContext(this.handleContextMenu); + Utilities.suppressErrors(this.patchEmojiPicker.bind(this), 'EmojiPicker patch')(this.promises.state); Utilities.suppressErrors(this.patchReactions.bind(this), 'Reaction patch')(this.promises.state); + this.patchContextMenus(); + } + + patchEmojiPicker() { + return; + const EmojiPickerListRow = WebpackModules.getModule(m => m.default && m.default.displayName == 'EmojiPickerListRow'); + Patcher.after(EmojiPickerListRow, 'default', (_, __, returnValue) => { + for (const emoji of returnValue.props.children) { + const emojiObj = emoji.props.children.props.emoji; + const url = emojiObj.id ? getEmojiURL({ id: emojiObj.id, animated: emojiObj.animated }) : 'https://discord.com' + EmojiInfo.getURL(emojiObj.surrogates); + const oOnContextMenu = emoji.props.onContextMenu; + emoji.props.onContextMenu = e => { + if (oOnContextMenu) { + let timeout = 0; + const unpatch = Patcher.before(ContextMenuActions, 'openContextMenu', (_, args) => { + const old = args[1]; + args[1] = e => { + try { + const _ret = old(e); + const ret = _ret.type(_ret.props); + const children = Utilities.getNestedProp(ret, 'props.children.0.props.children'); + if (Array.isArray(children)) children.push(this.constructMenu(url.split('?')[0], 'Emoji', emojiObj.uniqueName)); + return ret; + } catch (err) { + Logger.error('Some error happened in emoji picker context menu', err); + return null; + } + }; + unpatch(); + clearTimeout(timeout); + }); + timeout = setTimeout(unpatch, 1000); + return oOnContextMenu(e); + } else { + ContextMenuActions.openContextMenu(e, _ => React.createElement('div', { className: DiscordClasses.ContextMenu.contextMenu }, XenoLib.createContextMenuGroup([this.constructMenu(url.split('?')[0], 'Emoji', emojiObj.uniqueName)]))); + } + }; + } + }); } async patchReactions(promiseState) { @@ -378,8 +419,8 @@ var SaveToRedux = (() => { ret.props.children = e => { try { const oChRet = oChildren(e); - const url = _this.props.emoji.id ? getEmojiURL({ id: _this.props.emoji.id, animated: _this.props.emoji.animated }) : EmojiInfo.getURL(_this.props.emoji.name); - XenoLib.createSharedContext(oChRet, 'MESSAGE_REACTIONS', () => XenoLib.createContextMenuGroup([this.constructMenu(url.split('?')[0], 'Reaction', _this.props.emoji.name)])); + const url = _this.props.emoji.id ? getEmojiURL({ id: _this.props.emoji.id, animated: _this.props.emoji.animated }) : 'https://discord.com' + EmojiInfo.getURL(_this.props.emoji.name); + XenoLib.createSharedContext(oChRet, () => XenoLib.createContextMenuGroup([this.constructMenu(url.split('?')[0], 'Reaction', _this.props.emoji.name)])); return oChRet; } catch (e) { Logger.stacktrace('Error in Reaction patch', e); @@ -391,6 +432,224 @@ var SaveToRedux = (() => { Reaction.forceUpdateAll(); } + patchContextMenus() { + this.patchUserContextMenus(); + this.patchImageContextMenus(); + this.patchGuildContextMenu(); + } + + patchUserContextMenus() { + const CTXs = WebpackModules.findAll(({ default: { displayName } }) => displayName && (displayName.endsWith('UserContextMenu') || displayName === 'GroupDMContextMenu')); + for (const CTX of CTXs) { + Patcher.after(CTX, 'default', (_, [props], ret) => { + const menu = Utilities.getNestedProp( + Utilities.findInReactTree(ret, e => e && e.type && e.type.displayName === 'Menu'), + 'props.children' + ); + if (!Array.isArray(menu)) return; + let saveType; + let url; + let customName; + if (props.user && props.user.getAvatarURL) { + saveType = 'Avatar'; + url = props.user.getAvatarURL(); + if (this.settings.saveOptions.saveByName) customName = props.user.username; + } else if (props.channel && props.channel.type === 3 /* group DM */) { + url = AvatarModule.getChannelIconURL(props.channel); + saveType = 'Icon'; + } else return Logger.warn('Uknonwn context menu') /* hurr durr? */; + if (!url.indexOf('/assets/')) url = 'https://discordapp.com' + url; + url = useIdealExtensions(url); + try { + const submenu = this.constructMenu(url.split('?')[0], saveType, customName); + const group = XenoLib.createContextMenuGroup([submenu]); + if (this.settings.misc.contextMenuOnBottom) menu.push(group); + else menu.unshift(group); + } catch (e) { + Logger.warn('Failed to parse URL...', url, e); + } + }); + } + } + + patchImageContextMenus() { + const patchHandler = (props, ret, isImageMenu) => { + const menu = Utilities.getNestedProp( + Utilities.findInReactTree(ret, e => e && e.type && e.type.displayName === 'Menu'), + 'props.children' + ); + if (!Array.isArray(menu)) return; + const [state, setState] = React.useState({}); + let src; + let saveType = 'File'; + let url = ''; + let proxiedUrl = ''; + let customName = ''; + if (isImageMenu && (Utilities.getNestedProp(props, 'target.parentNode.className') || '').indexOf(XenoLib.getSingleClass('clickable imageWrapper')) !== -1) { + const inst = ReactTools.getOwnerInstance(Utilities.getNestedProp(props, 'target.parentNode.parentNode')); + proxiedUrl = props.src; + if (inst) src = inst.props.original; + if (typeof proxiedUrl === 'string') proxiedUrl = proxiedUrl.split('?')[0]; + /* if src does not have an extension but the proxied URL does, use the proxied URL instead */ + if (typeof src !== 'string' || src.indexOf('discordapp.com/channels') !== -1 || (!(isImage(src) || isVideo(src) || isAudio(src)) && (isImage(proxiedUrl) || isVideo(proxiedUrl) || isAudio(proxiedUrl)))) { + src = proxiedUrl; + proxiedUrl = ''; + } + } + if (!src) src = Utilities.getNestedProp(props, 'attachment.href') || Utilities.getNestedProp(props, 'attachment.url'); + /* is that enough specific cases? */ + if (typeof src === 'string') { + src = src.split('?')[0]; + if (src.indexOf('//giphy.com/gifs/') !== -1) src = `https://i.giphy.com/media/${src.match(/-([^-]+)$/)[1]}/giphy.gif`; + else if (src.indexOf('//tenor.com/view/') !== -1) { + src = props.src; + saveType = 'Video'; + } else if (src.indexOf('//preview.redd.it/') !== -1) { + src = src.replace('preview', 'i'); + } else if (src.indexOf('twimg.com/') !== -1) saveType = 'Image'; + } + if (!src) { + let C = props.target; + let proxiedsauce; + let sauce; + while (null != C) { + if (C instanceof HTMLImageElement && null != C.src) proxiedsauce = C.src; + if (C instanceof HTMLVideoElement && null != C.src) proxiedsauce = C.src; + if (C instanceof HTMLAnchorElement && null != C.href) sauce = C.href; + C = C.parentNode; + } + if (!proxiedsauce && !sauce) return; + if (proxiedsauce) proxiedsauce = proxiedsauce.split('?')[0]; + if (sauce) sauce = sauce.split('?')[0]; + // Logger.info('sauce', sauce, 'proxiedsauce', proxiedsauce); + /* do not check if proxiedsauce is an image video or audio, it will always be video or image! + an anchor element however is just a link which could be anything! so best we check it + special handler for github links, discord attachments and plugins + */ + if (sauce && sauce.indexOf('//github.com/') !== -1 && (sauce.indexOf('.plugin.js') === sauce.length - 10 || sauce.indexOf('.theme.css') === sauce.length - 10)) { + const split = sauce.slice(sauce.indexOf('//github.com/') + 13).split('/'); + split.splice(2, 1); + sauce = 'https://raw.githubusercontent.com/' + split.join('/'); + } + if (!proxiedsauce && (!sauce || !(isImage(sauce) || isVideo(sauce) || isAudio(sauce) || sauce.indexOf('//cdn.discordapp.com/attachments/') !== -1 || sauce.indexOf('//raw.githubusercontent.com/') !== -1))) return; + src = sauce; + proxiedUrl = proxiedsauce; + /* if src does not have an extension but the proxied URL does, use the proxied URL instead */ + if (!src || (!(isImage(sauce) || isVideo(sauce) || isAudio(sauce)) && (isImage(proxiedsauce) || isVideo(proxiedsauce) || isAudio(proxiedsauce)))) { + src = proxiedsauce; + proxiedUrl = ''; + } + if (!src) return; + } + url = src; + if (!url) return; + if (isImage(url) || url.indexOf('//steamuserimages') !== -1) saveType = 'Image'; + else if (isVideo(url)) saveType = 'Video'; + else if (isAudio(url)) saveType = 'Audio'; + if (url.indexOf('app.com/emojis/') !== -1) { + saveType = 'Emoji'; + const emojiId = url.split('emojis/')[1].split('.')[0]; + const emoji = EmojiUtils.getDisambiguatedEmojiContext().getById(emojiId); + if (!emoji) { + if (!DiscordAPI.currentChannel || !this.channelMessages[DiscordAPI.currentChannel.id]) return; + const message = this.channelMessages[DiscordAPI.currentChannel.id]._array.find(m => m.content.indexOf(emojiId) !== -1); + if (message && message.content) { + const group = message.content.match(new RegExp(`]*):${emojiId}>`)); + if (group && group[1]) customName = group[1]; + } + if (!customName) { + const alt = props.target.alt; + if (alt) customName = alt.split(':')[1] || alt; + } + } else customName = emoji.name; + } else if (state.__STR_extension) { + if (isImage(state.__STR_extension)) saveType = 'Image'; + else if (isVideo(state.__STR_extension)) saveType = 'Video'; + else if (isAudio(state.__STR_extension)) saveType = 'Audio'; + } else if (url.indexOf('//discordapp.com/assets/') !== -1 && props.target && props.target.className.indexOf('emoji') !== -1) { + const alt = props.target.alt; + if (alt) { + customName = alt.split(':')[1] || alt; + const name = EmojiStore.convertSurrogateToName(customName); + if (name) { + const match = name.match(EmojiStore.EMOJI_NAME_RE); + if (match) customName = EmojiStore.getByName(match[1]).uniqueName; + } + } + saveType = 'Emoji'; + } else if (url.indexOf('.plugin.js') === url.length - 10) saveType = 'Plugin'; + else if (url.indexOf('.theme.css') === url.length - 10) saveType = 'Theme'; + try { + const submenu = this.constructMenu( + url.split('?')[0], + saveType, + customName, + targetUrl => { + if (state.__STR_requesting || state.__STR_requested) return; + if (!isTrustedDomain(targetUrl)) return; + state.__STR_requesting = true; + RequestModule.head(targetUrl, (err, res) => { + if (err) return setState({ __STR_requesting: false, __STR_requested: true }); + const extension = MimeTypesModule.extension(res.headers['content-type']); + setState({ __STR_requesting: false, __STR_requested: true, __STR_extension: extension }); + }); + targetUrl; + }, + state.__STR_extension, + proxiedUrl + ); + const group = XenoLib.createContextMenuGroup([submenu]); + if (this.settings.misc.contextMenuOnBottom) menu.push(group); + else menu.unshift(group); + } catch (e) { + Logger.warn('Failed to parse URL...', url, e); + } + }; + + Patcher.after( + WebpackModules.find(({ default: { displayName } }) => displayName === 'NativeImageContextMenu'), + 'default', + (_, [props], ret) => patchHandler(props, ret, true) + ); + Patcher.after( + WebpackModules.find(({ default: { displayName } }) => displayName === 'MessageContextMenu'), + 'default', + (_, [props], ret) => patchHandler(props, ret) + ); + Patcher.after( + WebpackModules.find(({ default: { displayName } }) => displayName === 'MessageSearchResultContextMenu'), + 'default', + (_, [props], ret) => patchHandler(props, ret) + ); + } + + patchGuildContextMenu() { + Patcher.after( + WebpackModules.find(({ default: { displayName } }) => displayName === 'GuildContextMenu'), + 'default', + (_, [props], ret) => { + const menu = Utilities.getNestedProp( + Utilities.findInReactTree(ret, e => e && e.type && e.type.displayName === 'Menu'), + 'props.children' + ); + if (!Array.isArray(menu)) return; + let url = props.guild.getIconURL(); + if (!url) return; + let customName; + if (this.settings.saveOptions.saveByName) customName = props.guild.name; + url = useIdealExtensions(url); + try { + const submenu = this.constructMenu(url.split('?')[0], 'Icon', customName); + const group = XenoLib.createContextMenuGroup([submenu]); + if (this.settings.misc.contextMenuOnBottom) menu.push(group); + else menu.unshift(group); + } catch (e) { + Logger.warn('Failed to parse URL...', url, e); + } + } + ); + } + /* PATCHES */ rand() { @@ -420,10 +679,7 @@ var SaveToRedux = (() => { ret = name; break; case 1: // date - ret = `${date - .toLocaleDateString() - .split('/') - .join('-')} ${date.getHours()}-${date.getMinutes()}-${date.getSeconds()}`; + ret = `${date.toLocaleDateString().split('/').join('-')} ${date.getHours()}-${date.getMinutes()}-${date.getSeconds()}`; break; case 2: // random ret = rand; @@ -436,10 +692,7 @@ var SaveToRedux = (() => { ret = Utilities.formatTString(this.settings.saveOptions.customFileName, { rand, file: name, - date: date - .toLocaleDateString() - .split('/') - .join('-'), + date: date.toLocaleDateString().split('/').join('-'), time: `${date.getMinutes()}-${date.getSeconds()}-${date.getMilliseconds()}`, day: date.getDate(), // note to self: getDate gives you the day of month month: date.getMonth() + 1, // getMonth gives 0-11 @@ -464,11 +717,7 @@ var SaveToRedux = (() => { formatURL(url, requiresSize, customName, fallbackExtension, proxiedUrl, failNum = 0, forceKeepOriginal = false) { // url = url.replace(/\/$/, ''); if (requiresSize) url += '?size=2048'; - else if (url.indexOf('twimg.com/') !== -1) - url = url - .replace(':small', ':orig') - .replace(':medium', ':orig') - .replace(':large', ':orig'); + else if (url.indexOf('twimg.com/') !== -1) url = url.replace(':small', ':orig').replace(':medium', ':orig').replace(':large', ':orig'); else if (url.indexOf('.e621.net/') !== -1 || url.indexOf('.e926.net/') !== -1) { if (failNum <= 1) url = url.replace('preview/', '').replace('sample/', ''); if (failNum === 1) url = url.replace(/.jpe?g/, '.png'); @@ -495,14 +744,6 @@ var SaveToRedux = (() => { } constructMenu(url, type, customName, onNoExtension = () => {}, fallbackExtension, proxiedUrl) { - const createSubMenu = (name, items, callback) => - XenoLib.createContextMenuSubMenu(name, items, { - action: () => { - if (!callback) return; - ContextMenuActions.closeContextMenu(); - callback(); - } - }); const subItems = []; const folderSubMenus = []; const formattedurl = this.formatURL(url, type === 'Icon' || type === 'Avatar', customName, fallbackExtension, proxiedUrl, 0, type === 'Theme' || type === 'Plugin'); @@ -544,7 +785,7 @@ var SaveToRedux = (() => { .pipe(FsModule.createWriteStream(path)) .on('finish', () => { if (openOnSave) openItem(path); - BdApi.showToast(`Saved to '${path}'`, { type: 'success' }); + BdApi.showToast(`Saved to '${PathModule.resolve(path)}'`, { type: 'success' }); }) .on('error', e => { BdApi.showToast(`Failed to save! ${e}`, { type: 'error', timeout: 10000 }); @@ -581,7 +822,7 @@ var SaveToRedux = (() => { confirmText: Messages.MASKED_LINK_CONFIRM, minorText: Messages.MASKED_LINK_TRUST_THIS_DOMAIN, onConfirm: onOk, - onConfirmSecondary: function() { + onConfirmSecondary: function () { WebpackModules.getByProps('trustDomain').trustDomain(formattedurl.url); onOk(); } @@ -767,132 +1008,162 @@ var SaveToRedux = (() => { } }; - const folderSubMenu = folder => { - return createSubMenu( + const folderSubMenu = (folder, idx) => { + return XenoLib.createContextMenuSubMenu( folder.name, [ - XenoLib.createContextMenuItem('Remove Folder', () => { - const index = this.folders.findIndex(m => m === folder); - if (index === -1) return BdApi.showToast("Fatal error! Attempted to remove a folder that doesn't exist!", { type: 'error', timeout: 5000 }); - this.folders.splice(index, 1); - this.saveFolders(); - BdApi.showToast('Removed!', { type: 'success' }); - }), - XenoLib.createContextMenuItem('Open Folder', () => { - openItem(folder.path); - }), - XenoLib.createContextMenuItem('Save', () => { + XenoLib.createContextMenuItem( + 'Remove Folder', + () => { + this.folders.splice(idx, 1); + this.saveFolders(); + BdApi.showToast('Removed!', { type: 'success' }); + }, + 'remove-folder' + ), + XenoLib.createContextMenuItem( + 'Open Folder', + () => { + openItem(folder.path); + }, + 'open-folder' + ), + XenoLib.createContextMenuItem( + 'Save', + () => { + this.lastUsedFolder = idx; + const path = folder.path + `/${formattedurl.fileName}`; + saveFile(path, folder.path); + }, + 'save' + ), + XenoLib.createContextMenuItem('Save As...', () => saveAs(folder), 'savetoredux-save-as'), + XenoLib.createContextMenuItem( + 'Save And Open', + () => { + this.lastUsedFolder = idx; + const path = folder.path + `/${formattedurl.fileName}`; + saveFile(path, folder.path, true); + }, + 'save-and-open' + ), + XenoLib.createContextMenuItem( + 'Edit', + () => { + let __name = folder.name.slice(0); + let __path = folder.path.slice(0); + const saveFolder = () => { + if (!__path || !__path.length) return BdApi.showToast('Invalid path', { type: 'error' }); + folder.name = __name; + folder.path = __path; + this.saveFolders(); + }; + Modals.showModal( + 'Edit folder', + React.createElement(FolderEditor, { + name: __name, + path: __path, + onNameChange: e => (__name = e), + onPathChange: e => (__path = e) + }), + { + confirmText: 'Create', + onConfirm: saveFolder, + size: XenoLib.joinClassNames(Modals.ModalSizes.MEDIUM, 'ST-modal') + } + ); + }, + 'edit' + ) + ], + idx, + { + action: () => { this.lastUsedFolder = this.folders.findIndex(m => m === folder); const path = folder.path + `/${formattedurl.fileName}`; saveFile(path, folder.path); - }), - XenoLib.createContextMenuItem('Save As...', () => saveAs(folder)), - XenoLib.createContextMenuItem('Save And Open', () => { - this.lastUsedFolder = this.folders.findIndex(m => m === folder); - const path = folder.path + `/${formattedurl.fileName}`; - saveFile(path, folder.path, true); - }), - XenoLib.createContextMenuItem('Edit', () => { - let __name = folder.name.slice(0); - let __path = folder.path.slice(0); - const saveFolder = () => { - if (!__path || !__path.length) return BdApi.showToast('Invalid path', { type: 'error' }); - folder.name = __name; - folder.path = __path; - this.saveFolders(); - }; - Modals.showModal( - 'Edit folder', - React.createElement(FolderEditor, { - name: __name, - path: __path, - onNameChange: e => (__name = e), - onPathChange: e => (__path = e) - }), - { - confirmText: 'Create', - onConfirm: saveFolder, - size: XenoLib.joinClassNames(Modals.ModalSizes.MEDIUM, 'ST-modal') - } - ); - }) - ], - () => { - this.lastUsedFolder = this.folders.findIndex(m => m === folder); - const path = folder.path + `/${formattedurl.fileName}`; - saveFile(path, folder.path); + } } ); }; - for (const folder of this.folders) folderSubMenus.push(folderSubMenu(folder)); + for (const folderIDX in this.folders) folderSubMenus.push(folderSubMenu(this.folders[folderIDX], folderIDX)); subItems.push( ...folderSubMenus, - XenoLib.createContextMenuItem('Add Folder', () => { - dialog - .showOpenDialog({ - title: 'Add folder', - properties: ['openDirectory', 'createDirectory'] - }) - .then(({ filePaths: [path] }) => { - if (!path) return BdApi.showToast('Maybe next time.'); - let idx; - if ((idx = this.folders.findIndex(m => m.path === path)) !== -1) return BdApi.showToast(`Folder already exists as ${this.folders[idx].name}!`, { type: 'error', timeout: 5000 }); - const folderName = PathModule.basename(path); - let __name = folderName; - let __path = path.slice(0); - const saveFolder = () => { - if (!__path || !__path.length) return BdApi.showToast('Invalid path', { type: 'error' }); - this.folders.push({ - path: __path, - name: __name || 'Unnamed' - }); - this.saveFolders(); - BdApi.showToast('Added!', { type: 'success' }); - }; - Modals.showModal( - 'Create New Folder', - React.createElement(FolderEditor, { - name: folderName, - path: path, - onNameChange: e => (__name = e), - onPathChange: e => (__path = e) - }), - { - confirmText: 'Create', - onConfirm: saveFolder, - size: XenoLib.joinClassNames(Modals.ModalSizes.MEDIUM, 'ST-modal') - } - ); - }); - }), - XenoLib.createContextMenuItem('Save As...', () => { - dialog - .showSaveDialog({ - defaultPath: formattedurl.fileName, - filters: formattedurl.extension - ? [ - { - name: /\.{0,1}(png|jpe?g|webp|gif|svg)$/i.test(formattedurl.extension) ? 'Images' : /\.{0,1}(mp4|webm|mov)$/i.test(formattedurl.extension) ? 'Videos' : /\.{0,1}(mp3|ogg|wav|flac)$/i.test(formattedurl.extension) ? 'Audio' : 'Files', - extensions: [formattedurl.extension] - }, - { - name: 'All Files', - extensions: ['*'] - } - ] - : undefined - }) - .then(({ filePath: path }) => { - if (!path) return BdApi.showToast('Maybe next time.'); - saveFile(path, undefined, false, true); - }); - }), + XenoLib.createContextMenuItem( + 'Add Folder', + () => { + dialog + .showOpenDialog({ + title: 'Add folder', + properties: ['openDirectory', 'createDirectory'] + }) + .then(({ filePaths: [path] }) => { + if (!path) return BdApi.showToast('Maybe next time.'); + let idx; + if ((idx = this.folders.findIndex(m => m.path === path)) !== -1) return BdApi.showToast(`Folder already exists as ${this.folders[idx].name}!`, { type: 'error', timeout: 5000 }); + const folderName = PathModule.basename(path); + let __name = folderName; + let __path = path.slice(0); + const saveFolder = () => { + if (!__path || !__path.length) return BdApi.showToast('Invalid path', { type: 'error' }); + this.folders.push({ + path: __path, + name: __name || 'Unnamed' + }); + this.saveFolders(); + BdApi.showToast('Added!', { type: 'success' }); + }; + Modals.showModal( + 'Create New Folder', + React.createElement(FolderEditor, { + name: folderName, + path: path, + onNameChange: e => (__name = e), + onPathChange: e => (__path = e) + }), + { + confirmText: 'Create', + onConfirm: saveFolder, + size: XenoLib.joinClassNames(Modals.ModalSizes.MEDIUM, 'ST-modal') + } + ); + }); + }, + 'add-folder' + ), + XenoLib.createContextMenuItem( + 'Save As...', + () => { + dialog + .showSaveDialog({ + defaultPath: formattedurl.fileName, + filters: formattedurl.extension + ? [ + { + name: /\.{0,1}(png|jpe?g|webp|gif|svg)$/i.test(formattedurl.extension) ? 'Images' : /\.{0,1}(mp4|webm|mov)$/i.test(formattedurl.extension) ? 'Videos' : /\.{0,1}(mp3|ogg|wav|flac)$/i.test(formattedurl.extension) ? 'Audio' : 'Files', + extensions: [formattedurl.extension] + }, + { + name: 'All Files', + extensions: ['*'] + } + ] + : undefined + }) + .then(({ filePath: path }) => { + if (!path) return BdApi.showToast('Maybe next time.'); + saveFile(path, undefined, false, true); + }); + }, + 'save-as' + ), type === 'Plugin' ? XenoLib.createContextMenuItem( `Install Plugin`, () => { saveFile(BdApi.Plugins.folder + `/${formattedurl.fileName}`, undefined, false, true); }, + 'install-plugin', { /* onContextMenu: () => console.log('wee!'), tooltip: 'Right click to install and enable' */ tooltip: 'No overwrite warning' @@ -905,6 +1176,7 @@ var SaveToRedux = (() => { () => { saveFile(BdApi.Themes.folder + `/${formattedurl.fileName}`, undefined, false, true); }, + 'install-theme', { /* onContextMenu: () => console.log('wee!'), tooltip: 'Right click to install and enable' */ tooltip: 'No overwrite warning' @@ -912,170 +1184,17 @@ var SaveToRedux = (() => { ) : null ); - return createSubMenu(`Save ${type} To`, subItems, () => { - if (this.lastUsedFolder === -1) return BdApi.showToast('No folder has been used yet', { type: 'error' }); - const folder = this.folders[this.lastUsedFolder]; - if (!folder) return BdApi.showToast('Folder no longer exists', { type: 'error' }); - const path = folder.path + `/${formattedurl.fileName}`; - saveFile(path, folder.path); + return XenoLib.createContextMenuSubMenu(`Save ${type} To`, subItems, 'str', { + action: () => { + if (this.lastUsedFolder === -1) return BdApi.showToast('No folder has been used yet', { type: 'error' }); + const folder = this.folders[this.lastUsedFolder]; + if (!folder) return BdApi.showToast('Folder no longer exists', { type: 'error' }); + const path = folder.path + `/${formattedurl.fileName}`; + saveFile(path, folder.path); + } }); } - handleContextMenu(_this, ret) { - if (!ret) return ret; - // Logger.info(_this, ret); - const type = _this.props.type; - let saveType = 'File'; - let url = ''; - let proxiedUrl = ''; - let customName = ''; - const isImage = e => /\.{0,1}(png|jpe?g|webp|gif|svg)$/i.test(e); - const isVideo = e => /\.{0,1}(mp4|webm|mov)$/i.test(e); - const isAudio = e => /\.{0,1}(mp3|ogg|wav|flac)$/i.test(e); - const useCorrectShit = () => { - if (url.indexOf('/a_') !== -1) url = url.replace('.webp', '.gif').replace('.png', '.gif'); - else url = url.replace('.webp', '.png'); - }; - if (type === 'NATIVE_IMAGE' || type === 'MESSAGE_MAIN' || type === 'MESSAGE_SEARCH_RESULT') { - let src; - if (type === 'NATIVE_IMAGE') { - src = Utilities.getNestedProp(ret, 'props.children.props.href') || Utilities.getNestedProp(_this, 'props.href'); - proxiedUrl = Utilities.getNestedProp(ret, 'props.children.props.src') || Utilities.getNestedProp(_this, 'props.src'); - if (typeof proxiedUrl === 'string') proxiedUrl = proxiedUrl.split('?')[0]; - /* if src does not have an extension but the proxied URL does, use the proxied URL instead */ - if (typeof src !== 'string' || src.indexOf('discordapp.com/channels') !== -1 || (!(isImage(src) || isVideo(src) || isAudio(src)) && (isImage(proxiedUrl) || isVideo(proxiedUrl) || isAudio(proxiedUrl)))) { - src = proxiedUrl; - proxiedUrl = ''; - } - } - // Logger.info('src', src, 'proxiedUrl', proxiedUrl, _this, ret); - if (!src) src = Utilities.getNestedProp(_this, 'props.attachment.href') || Utilities.getNestedProp(_this, 'props.attachment.url'); - /* is that enough specific cases? */ - if (typeof src === 'string') { - src = src.split('?')[0]; - if (src.indexOf('//giphy.com/gifs/') !== -1) src = `https://i.giphy.com/media/${src.match(/-([^-]+)$/)[1]}/giphy.gif`; - else if (src.indexOf('//tenor.com/view/') !== -1) { - src = _this.props.src; - saveType = 'Video'; - } else if (src.indexOf('//preview.redd.it/') !== -1) { - src = src.replace('preview', 'i'); - } else if (src.indexOf('twimg.com/') !== -1) saveType = 'Image'; - } - if (!src) { - let C = _this.props.target; - let proxiedsauce; - let sauce; - while (null != C) { - if (C instanceof HTMLImageElement && null != C.src) proxiedsauce = C.src; - if (C instanceof HTMLVideoElement && null != C.src) proxiedsauce = C.src; - if (C instanceof HTMLAnchorElement && null != C.href) sauce = C.href; - C = C.parentNode; - } - if (!proxiedsauce && !sauce) return; - if (proxiedsauce) proxiedsauce = proxiedsauce.split('?')[0]; - if (sauce) sauce = sauce.split('?')[0]; - // Logger.info('sauce', sauce, 'proxiedsauce', proxiedsauce); - /* do not check if proxiedsauce is an image video or audio, it will always be video or image! - an anchor element however is just a link which could be anything! so best we check it - special handler for github links, discord attachments and plugins - */ - if (sauce && sauce.indexOf('//github.com/') !== -1 && (sauce.indexOf('.plugin.js') === sauce.length - 10 || sauce.indexOf('.theme.css') === sauce.length - 10)) { - const split = sauce.slice(sauce.indexOf('//github.com/') + 13).split('/'); - split.splice(2, 1); - sauce = 'https://raw.githubusercontent.com/' + split.join('/'); - } - if (!proxiedsauce && (!sauce || !(isImage(sauce) || isVideo(sauce) || isAudio(sauce) || sauce.indexOf('//cdn.discordapp.com/attachments/') !== -1 || sauce.indexOf('//raw.githubusercontent.com/') !== -1))) return; - src = sauce; - proxiedUrl = proxiedsauce; - /* if src does not have an extension but the proxied URL does, use the proxied URL instead */ - if (!src || (!(isImage(sauce) || isVideo(sauce) || isAudio(sauce)) && (isImage(proxiedsauce) || isVideo(proxiedsauce) || isAudio(proxiedsauce)))) { - src = proxiedsauce; - proxiedUrl = ''; - } - if (!src) return; - } - url = src; - if (!url) return; - if (isImage(url) || url.indexOf('//steamuserimages') !== -1) saveType = 'Image'; - else if (isVideo(url)) saveType = 'Video'; - else if (isAudio(url)) saveType = 'Audio'; - if (url.indexOf('app.com/emojis/') !== -1) { - saveType = 'Emoji'; - const emojiId = url.split('emojis/')[1].split('.')[0]; - const emoji = EmojiUtils.getDisambiguatedEmojiContext().getById(emojiId); - if (!emoji) { - if (!DiscordAPI.currentChannel || !this.channelMessages[DiscordAPI.currentChannel.id]) return; - const message = this.channelMessages[DiscordAPI.currentChannel.id]._array.find(m => m.content.indexOf(emojiId) !== -1); - if (message && message.content) { - const group = message.content.match(new RegExp(`]*):${emojiId}>`)); - if (group && group[1]) customName = group[1]; - } - if (!customName) { - const alt = _this.props.target.alt; - if (alt) customName = alt.split(':')[1] || alt; - } - } else customName = emoji.name; - } else if (_this.state.__STR_extension) { - if (isImage(_this.state.__STR_extension)) saveType = 'Image'; - else if (isVideo(_this.state.__STR_extension)) saveType = 'Video'; - else if (isAudio(_this.state.__STR_extension)) saveType = 'Audio'; - } else if (url.indexOf('//discordapp.com/assets/') !== -1 && _this.props.target && _this.props.target.className.indexOf('emoji') !== -1) { - const alt = _this.props.target.alt; - if (alt) customName = alt.split(':')[1] || alt; - } else if (url.indexOf('.plugin.js') === url.length - 10) saveType = 'Plugin'; - else if (url.indexOf('.theme.css') === url.length - 10) saveType = 'Theme'; - if (!Array.isArray(ret.props.children)) ret.props.children = [ret.props.children]; - } else if (type === 'GUILD_ICON_BAR') { - saveType = 'Icon'; - url = _this.props.guild.getIconURL(); - if (!url) return; - if (this.settings.saveOptions.saveByName) customName = _this.props.guild.name; - useCorrectShit(); - } else { - if (_this.props.user && _this.props.user.getAvatarURL) { - saveType = 'Avatar'; - url = _this.props.user.getAvatarURL(); - if (this.settings.saveOptions.saveByName) customName = _this.props.user.username; - } else if (_this.props.channel && _this.props.channel.type === 3 /* group DM */) { - url = AvatarModule.getChannelIconURL(_this.props.channel); - saveType = 'Icon'; - } else return /* hurr durr? */; - useCorrectShit(); - if (url.startsWith('/assets/')) url = 'https://discordapp.com' + url; - } - try { - const submenu = this.constructMenu( - url.split('?')[0], - saveType, - customName, - targetUrl => { - if (_this.state.__STR_requesting || _this.state.__STR_requested) return; - if (!isTrustedDomain(targetUrl)) return; - _this.state.__STR_requesting = true; - RequestModule.head(targetUrl, (err, res) => { - if (err) return _this.setState({ __STR_requesting: false, __STR_requested: true }); - const extension = MimeTypesModule.extension(res.headers['content-type']); - _this.setState({ - __STR_requesting: false, - __STR_requested: true, - __STR_extension: extension - }); - }); - targetUrl; - }, - _this.state.__STR_extension, - proxiedUrl - ); - const group = React.createElement(XenoLib.ReactComponents.ErrorBoundary, { label: 'savetoredux submenu', onError: () => XenoLib.Notifications.error(`[**${this.name}**] An issue has occured and the submenus had to be removed to avoid crashes. Sorry for the inconvenience. More info in console (CTRL + SHIFT + I, click console).`, { timeout: 10000 }) }, XenoLib.createContextMenuGroup([submenu])); - const targetGroup = ret.props.children; - - if (this.settings.misc.contextMenuOnBottom) targetGroup.push(group); - else targetGroup.unshift(group); - } catch (e) { - Logger.warn('Failed to parse URL...', url, e); - } - } - showChangelog(footer) { XenoLib.showChangelog(`${this.name} has been updated!`, this.version, this._config.changelog); } @@ -1122,7 +1241,7 @@ var SaveToRedux = (() => { n = (n, e) => n && n._config && n._config.info && n._config.info.version && i(n._config.info.version, e), e = BdApi.getPlugin('ZeresPluginLibrary'), o = BdApi.getPlugin('XenoLib'); - n(e, '1.2.14') && (ZeresPluginLibraryOutdated = !0), n(o, '1.3.17') && (XenoLibOutdated = !0); + n(e, '1.2.17') && (ZeresPluginLibraryOutdated = !0), n(o, '1.3.20') && (XenoLibOutdated = !0); } } catch (i) { console.error('Error checking if libraries are out of date', i); @@ -1164,8 +1283,8 @@ var SaveToRedux = (() => { g = BdApi.findModuleByProps('push', 'update', 'pop', 'popWithKey'), h = BdApi.findModuleByDisplayName('Text'), i = BdApi.findModule(a => a.defaultProps && a.key && 'confirm-modal' === a.key()), - j = () => BdApi.alert(e, BdApi.React.createElement('span', {}, BdApi.React.createElement('div', {}, f), `Due to a slight mishap however, you'll have to download the libraries yourself.`, c || ZeresPluginLibraryOutdated ? BdApi.React.createElement('div', {}, BdApi.React.createElement('a', { href: 'https://betterdiscord.net/ghdl?id=2252', target: '_blank' }, 'Click here to download ZeresPluginLibrary')) : null, b || XenoLibOutdated ? BdApi.React.createElement('div', {}, BdApi.React.createElement('a', { href: 'https://betterdiscord.net/ghdl?id=3169', target: '_blank' }, 'Click here to download XenoLib')) : null)); - if (!g || !i || !h) return j(); + j = () => BdApi.alert(e, BdApi.React.createElement('span', {}, BdApi.React.createElement('div', {}, f), `Due to a slight mishap however, you'll have to download the libraries yourself. This is not intentional, something went wrong, errors are in console.`, c || ZeresPluginLibraryOutdated ? BdApi.React.createElement('div', {}, BdApi.React.createElement('a', { href: 'https://betterdiscord.net/ghdl?id=2252', target: '_blank' }, 'Click here to download ZeresPluginLibrary')) : null, b || XenoLibOutdated ? BdApi.React.createElement('div', {}, BdApi.React.createElement('a', { href: 'https://betterdiscord.net/ghdl?id=3169', target: '_blank' }, 'Click here to download XenoLib')) : null)); + if (!g || !i || !h) return console.error(`Missing components:${(g ? '' : ' ModalStack') + (i ? '' : ' ConfirmationModalComponent') + (h ? '' : 'TextElement')}`), j(); class k extends BdApi.React.PureComponent { constructor(a) { super(a), (this.state = { hasError: !1 }); @@ -1182,42 +1301,67 @@ var SaveToRedux = (() => { this.props.onConfirm(); } } - let m = !1; - const n = g.push( - a => - BdApi.React.createElement( - k, - { - label: 'missing dependency modal', - onError: () => { - g.popWithKey(n), j(); - } - }, - BdApi.React.createElement( - l, - Object.assign( - { - header: e, - children: [BdApi.React.createElement(h, { size: h.Sizes.SIZE_16, children: [`${f} Please click Download Now to download ${d ? 'them' : 'it'}.`] })], - red: !1, - confirmText: 'Download Now', - cancelText: 'Cancel', - onConfirm: () => { - if (m) return; - m = !0; - const a = require('request'), - b = require('fs'), - c = require('path'), - d = () => { - (global.XenoLib && !XenoLibOutdated) || a('https://raw.githubusercontent.com/1Lighty/BetterDiscordPlugins/master/Plugins/1XenoLib.plugin.js', (a, d, e) => (a || 200 !== d.statusCode ? (g.popWithKey(n), j()) : void b.writeFile(c.join(BdApi.Plugins.folder, '1XenoLib.plugin.js'), e, () => {}))); - }; - !global.ZeresPluginLibrary || ZeresPluginLibraryOutdated ? a('https://raw.githubusercontent.com/rauenzi/BDPluginLibrary/master/release/0PluginLibrary.plugin.js', (a, e, f) => (a || 200 !== e.statusCode ? (g.popWithKey(n), j()) : void (b.writeFile(c.join(BdApi.Plugins.folder, '0PluginLibrary.plugin.js'), f, () => {}), d()))) : d(); - } - }, - a + let m = !1, + n = !1; + const o = g.push( + a => { + if (n) return null; + try { + return BdApi.React.createElement( + k, + { + label: 'missing dependency modal', + onError: () => { + g.popWithKey(o), j(); + } + }, + BdApi.React.createElement( + l, + Object.assign( + { + header: e, + children: [BdApi.React.createElement(h, { size: h.Sizes.SIZE_16, children: [`${f} Please click Download Now to download ${d ? 'them' : 'it'}.`] })], + red: !1, + confirmText: 'Download Now', + cancelText: 'Cancel', + onConfirm: () => { + if (m) return; + m = !0; + const a = require('request'), + b = require('fs'), + c = require('path'), + d = BdApi.Plugins && BdApi.Plugins.folder ? BdApi.Plugins.folder : window.ContentManager.pluginsFolder, + e = () => { + (global.XenoLib && !XenoLibOutdated) || + a('https://raw.githubusercontent.com/1Lighty/BetterDiscordPlugins/master/Plugins/1XenoLib.plugin.js', (a, e, f) => { + try { + if (a || 200 !== e.statusCode) return g.popWithKey(o), j(); + b.writeFile(c.join(d, '1XenoLib.plugin.js'), f, () => {}); + } catch (a) { + console.error('Fatal error downloading XenoLib', a), g.popWithKey(o), j(); + } + }); + }; + !global.ZeresPluginLibrary || ZeresPluginLibraryOutdated + ? a('https://raw.githubusercontent.com/rauenzi/BDPluginLibrary/master/release/0PluginLibrary.plugin.js', (a, f, h) => { + try { + if (a || 200 !== f.statusCode) return g.popWithKey(o), j(); + b.writeFile(c.join(d, '0PluginLibrary.plugin.js'), h, () => {}), e(); + } catch (a) { + console.error('Fatal error downloading ZeresPluginLibrary', a), g.popWithKey(o), j(); + } + }) + : e(); + } + }, + a + ) ) - ) - ), + ); + } catch (a) { + return console.error('There has been an error constructing the modal', a), (n = !0), g.popWithKey(o), j(), null; + } + }, void 0, `${this.name}_DEP_MODAL` );