From af8fbe66e4200dc4cd7b7b08120f274ea11c30c9 Mon Sep 17 00:00:00 2001 From: Dollar3795 Date: Fri, 5 Mar 2021 08:14:37 +0000 Subject: [PATCH] Upload files to 'Plugins' --- Plugins/1XenoLib.plugin.js | 2079 +++++++++++++++++++ Plugins/BetterImageViewer.plugin.js | 2207 +++++++++++++++++++++ Plugins/BetterTypingUsers.plugin.js | 510 +++++ Plugins/BetterUnavailableGuilds.plugin.js | 519 +++++ Plugins/CrashRecovery.plugin.js | 527 +++++ 5 files changed, 5842 insertions(+) create mode 100644 Plugins/1XenoLib.plugin.js create mode 100644 Plugins/BetterImageViewer.plugin.js create mode 100644 Plugins/BetterTypingUsers.plugin.js create mode 100644 Plugins/BetterUnavailableGuilds.plugin.js create mode 100644 Plugins/CrashRecovery.plugin.js diff --git a/Plugins/1XenoLib.plugin.js b/Plugins/1XenoLib.plugin.js new file mode 100644 index 0000000..8744a75 --- /dev/null +++ b/Plugins/1XenoLib.plugin.js @@ -0,0 +1,2079 @@ +//META{"name":"XenoLib","source":"https://github.com/1Lighty/BetterDiscordPlugins/blob/master/Plugins/1XenoLib.plugin.js/","authorId":"239513071272329217","invite":"NYvWdN5","donate":"https://paypal.me/lighty13"}*// +/*@cc_on +@if (@_jscript) + + // Offer to self-install for clueless users that try to run this directly. + var shell = WScript.CreateObject('WScript.Shell'); + var fs = new ActiveXObject('Scripting.FileSystemObject'); + var pathPlugins = shell.ExpandEnvironmentStrings('%APPDATA%\\BetterDiscord\\plugins'); + var pathSelf = WScript.ScriptFullName; + // Put the user at ease by addressing them in the first person + shell.Popup('It looks like you\'ve mistakenly tried to run me directly. \n(Don\'t do that!)', 0, 'I\'m a plugin for BetterDiscord', 0x30); + if (fs.GetParentFolderName(pathSelf) === fs.GetAbsolutePathName(pathPlugins)) { + shell.Popup('I\'m in the correct folder already.', 0, 'I\'m already installed', 0x40); + } else if (!fs.FolderExists(pathPlugins)) { + shell.Popup('I can\'t find the BetterDiscord plugins folder.\nAre you sure it\'s even installed?', 0, 'Can\'t install myself', 0x10); + } else if (shell.Popup('Should I copy myself to BetterDiscord\'s plugins folder for you?', 0, 'Do you need some help?', 0x34) === 6) { + fs.CopyFile(pathSelf, fs.BuildPath(pathPlugins, fs.GetFileName(pathSelf)), true); + // Show the user where to put plugins in the future + shell.Exec('explorer ' + pathPlugins); + shell.Popup('I\'m installed!', 0, 'Successfully installed', 0x40); + } + WScript.Quit(); + +@else@*/ +/* + * Copyright © 2019-2021, _Lighty_ + * All rights reserved. + * Code may not be redistributed, modified or otherwise taken without explicit permission. + */ +module.exports = (() => { + const canUseUntitledNotifAPI = !!(global.Untitled && Untitled.n11s && Untitled.n11s.n11sApi) + /* Setup */ + const config = { + main: 'index.js', + info: { + name: 'XenoLib', + authors: [ + { + name: 'Lighty', + discord_id: '239513071272329217', + github_username: 'LightyPon', + twitter_username: '' + } + ], + version: '1.3.35', + description: 'Simple library to complement plugins with shared code without lowering performance. Also adds needed buttons to some plugins.', + github: 'https://github.com/1Lighty', + github_raw: 'https://raw.githubusercontent.com/1Lighty/BetterDiscordPlugins/master/Plugins/1XenoLib.plugin.js' + }, + changelog: [ + { + title: 'RIP BBD on Canary', + type: 'fixed', + items: ['Implemented fixes that allow patches to work properly on canary using Powercord.'] + } + ], + defaultConfig: [ + canUseUntitledNotifAPI ? {} : { + type: 'category', + id: 'notifications', + name: 'Notification settings', + collapsible: true, + shown: true, + settings: [ + { + name: 'Notification position', + id: 'position', + type: 'position', + value: 'topRight' + }, + { + name: 'Notifications use backdrop-filter', + id: 'backdrop', + type: 'switch', + value: true + }, + { + name: 'Background color', + id: 'backdropColor', + type: 'color', + value: '#3e4346', + options: { + defaultColor: '#3e4346' + } + }, + { + name: 'Timeout resets to 0 when hovered', + id: 'timeoutReset', + type: 'switch', + value: true + } + ] + }, + { + type: 'category', + id: 'addons', + name: 'AddonCard settings', + collapsible: true, + shown: false, + settings: [ + { + name: 'Add extra buttons to specific plugins', + note: 'Disabling this will move the buttons to the bottom of plugin settings (if available)', + id: 'extra', + type: 'switch', + value: true + } + ] + } + ] + }; + + /* Build */ + const buildPlugin = ([Plugin, Api]) => { + const { ContextMenu, EmulatedTooltip, Toasts, Settings, Popouts, Modals, Utilities, WebpackModules, Filters, DiscordModules, ColorConverter, DOMTools, DiscordClasses, DiscordSelectors, ReactTools, ReactComponents, DiscordAPI, Logger, PluginUpdater, PluginUtilities, DiscordClassModules, Structs } = Api; + const { React, ModalStack, ContextMenuActions, ContextMenuItem, ContextMenuItemsGroup, ReactDOM, ChannelStore, GuildStore, UserStore, DiscordConstants, Dispatcher, GuildMemberStore, GuildActions, PrivateChannelActions, LayerManager, InviteActions, FlexChild, Titles, Changelog: ChangelogModal } = DiscordModules; + + PluginUpdater.checkForUpdate(config.info.name, config.info.version, config.info.github_raw); + + let CancelledAsync = false; + const DefaultLibrarySettings = {}; + + for (let s = 0; s < config.defaultConfig.length; s++) { + const current = config.defaultConfig[s]; + if (current.type != 'category') { + DefaultLibrarySettings[current.id] = current.value; + } else { + DefaultLibrarySettings[current.id] = {}; + for (let s = 0; s < current.settings.length; s++) { + const subCurrent = current.settings[s]; + DefaultLibrarySettings[current.id][subCurrent.id] = subCurrent.value; + } + } + } + + if (global.XenoLib) { + try { + global.XenoLib.shutdown(); + } catch (e) { } + } + const XenoLib = {}; + XenoLib.shutdown = () => { + try { + Patcher.unpatchAll(); + } catch (e) { + Logger.stacktrace('Failed to unpatch all', e); + } + CancelledAsync = true; + PluginUtilities.removeStyle('XenoLib-CSS'); + try { + const notifWrapper = document.querySelector('.xenoLib-notifications'); + if (notifWrapper) { + ReactDOM.unmountComponentAtNode(notifWrapper); + notifWrapper.remove(); + } + } catch (e) { + Logger.stacktrace('Failed to unmount Notifications component', e); + } + }; + + XenoLib._ = XenoLib.DiscordUtils = WebpackModules.getByProps('bindAll', 'debounce'); + + XenoLib.loadData = (name, key, defaultData, returnNull) => { + try { + return XenoLib._.mergeWith(defaultData ? Utilities.deepclone(defaultData) : {}, BdApi.getData(name, key), (_, b) => { + if (XenoLib._.isArray(b)) return b; + }); + } catch (err) { + Logger.err(name, 'Unable to load data: ', err); + if (returnNull) return null; + return Utilities.deepclone(defaultData); + } + }; + + XenoLib.getClass = (arg, thrw) => { + try { + const args = arg.split(' '); + return WebpackModules.getByProps(...args)[args[args.length - 1]]; + } catch (e) { + if (thrw) throw e; + if (!XenoLib.getClass.__warns[arg] || Date.now() - XenoLib.getClass.__warns[arg] > 1000 * 60) { + Logger.stacktrace(`Failed to get class with props ${arg}`, e); + XenoLib.getClass.__warns[arg] = Date.now(); + } + return ''; + } + }; + XenoLib.getSingleClass = (arg, thrw) => { + try { + return XenoLib.getClass(arg, thrw).split(' ')[0]; + } catch (e) { + if (thrw) throw e; + if (!XenoLib.getSingleClass.__warns[arg] || Date.now() - XenoLib.getSingleClass.__warns[arg] > 1000 * 60) { + Logger.stacktrace(`Failed to get class with props ${arg}`, e); + XenoLib.getSingleClass.__warns[arg] = Date.now(); + } + return ''; + } + }; + XenoLib.getClass.__warns = {}; + XenoLib.getSingleClass.__warns = {}; + + const rendererFunctionClass = (() => { + try { + const topContext = require('electron').webFrame.top.context; + if (topContext === window) return null; + return topContext.Function + } catch { + return null; + } + })(); + const originalFunctionClass = Function; + XenoLib.createSmartPatcher = patcher => { + const createPatcher = patcher => { + return (moduleToPatch, functionName, callback, options = {}) => { + try { + var origDef = moduleToPatch[functionName]; + } catch (_) { + return Logger.error(`Failed to patch ${functionName}`); + } + if (rendererFunctionClass && origDef && !(origDef instanceof originalFunctionClass) && origDef instanceof rendererFunctionClass) window.Function = rendererFunctionClass; + const unpatches = []; + try { + unpatches.push(patcher(moduleToPatch, functionName, callback, options) || DiscordConstants.NOOP); + } catch (err) { + throw err; + } finally { + if (rendererFunctionClass) window.Function = originalFunctionClass; + } + try { + if (origDef && origDef.__isBDFDBpatched && moduleToPatch.BDFDBpatch && typeof moduleToPatch.BDFDBpatch[functionName].originalMethod === 'function') { + /* do NOT patch a patch by ZLIb, that'd be bad and cause double items in context menus */ + if ((Utilities.getNestedProp(ZeresPluginLibrary, 'Patcher.patches') || []).findIndex(e => e.module === moduleToPatch) !== -1 && moduleToPatch.BDFDBpatch[functionName].originalMethod.__originalFunction) return; + unpatches.push(patcher(moduleToPatch.BDFDBpatch[functionName], 'originalMethod', callback, options)); + } + } catch (err) { + Logger.stacktrace('Failed to patch BDFDB patches', err); + } + return function unpatch() { + unpatches.forEach(e => e()); + }; + }; + }; + return Object.assign({}, patcher, { + before: createPatcher(patcher.before), + instead: createPatcher(patcher.instead), + after: createPatcher(patcher.after) + }); + }; + + const Patcher = XenoLib.createSmartPatcher(Api.Patcher); + + const LibrarySettings = XenoLib.loadData(config.info.name, 'settings', DefaultLibrarySettings); + + PluginUtilities.addStyle( + 'XenoLib-CSS', + ` + .xenoLib-color-picker .xenoLib-button { + width: 34px; + min-height: 38px; + } + .xenoLib-color-picker .xenoLib-button:hover { + width: 128px; + } + .xenoLib-color-picker .xenoLib-button .${XenoLib.getSingleClass('recording text')} { + opacity: 0; + transform: translate3d(200%,0,0); + } + .xenoLib-color-picker .xenoLib-button:hover .${XenoLib.getSingleClass('recording text')} { + opacity: 1; + transform: translateZ(0); + } + .xenoLib-button-icon { + left: 50%; + top: 50%; + position: absolute; + margin-left: -12px; + margin-top: -8px; + width: 24px; + height: 24px; + opacity: 1; + transform: translateZ(0); + transition: opacity .2s ease-in-out,transform .2s ease-in-out,-webkit-transform .2s ease-in-out; + } + .xenoLib-button-icon.xenoLib-revert > svg { + width: 24px; + height: 24px; + } + .xenoLib-button-icon.xenoLib-revert { + margin-top: -12px; + } + .xenoLib-button:hover .xenoLib-button-icon { + opacity: 0; + transform: translate3d(-200%,0,0); + } + .xenoLib-notifications { + position: absolute; + color: white; + width: 100%; + min-height: 100%; + display: flex; + flex-direction: column; + z-index: 1000; + pointer-events: none; + font-size: 14px; + } + .xenoLib-notification { + min-width: 200px; + overflow: hidden; + } + .xenoLib-notification-content-wrapper { + padding: 22px 20px 0 20px; + } + .xenoLib-centering-bottomLeft .xenoLib-notification-content-wrapper:first-of-type, .xenoLib-centering-bottomMiddle .xenoLib-notification-content-wrapper:first-of-type, .xenoLib-centering-bottomRight .xenoLib-notification-content-wrapper:first-of-type { + padding: 0 20px 20px 20px; + } + .xenoLib-notification-content { + padding: 12px; + overflow: hidden; + background: #474747; + pointer-events: all; + position: relative; + width: 20vw; + white-space: break-spaces; + min-width: 330px; + } + .xenoLib-notification-loadbar { + position: absolute; + bottom: 0; + left: 0px; + width: auto; + background-image: linear-gradient(130deg,var(--grad-one),var(--grad-two)); + height: 5px; + } + .xenoLib-notification-loadbar-user { + animation: fade-loadbar-animation 1.5s ease-in-out infinite; + } + @keyframes fade-loadbar-animation { + 0% { + filter: brightness(75%) + } + 50% { + filter: brightness(100%) + } + to { + filter: brightness(75%) + } + } + .xenoLib-notification-loadbar-striped:before { + content: ""; + position: absolute; + width: 100%; + height: 100%; + border-radius: 5px; + background: linear-gradient( + -20deg, + transparent 35%, + var(--bar-color) 35%, + var(--bar-color) 70%, + transparent 70% + ); + animation: shift 1s linear infinite; + background-size: 60px 100%; + box-shadow: inset 0 0px 1px rgba(0, 0, 0, 0.2), + inset 0 -2px 1px rgba(0, 0, 0, 0.2); + } + @keyframes shift { + to { + background-position: 60px 100%; + } + } + .xenoLib-notification-close { + float: right; + padding: 0; + height: unset; + opacity: .7; + } + .xenLib-notification-counter { + float: right; + margin-top: 2px; + } + .topMiddle-xenoLib { + top: 0; + left: 0; + right: 0; + margin-left: auto; + margin-right: auto; + } + .bottomMiddle-xenoLib { + bottom: 0; + left: 0; + right: 0; + margin-left: auto; + margin-right: auto; + } + .xenoLib-centering-topLeft, .xenoLib-centering-bottomLeft { + align-items: flex-start; + } + .xenoLib-centering-topMiddle, .xenoLib-centering-bottomMiddle { + align-items: center; + } + .xenoLib-centering-topRight, .xenoLib-centering-bottomRight { + align-items: flex-end; + } + .xenoLib-centering-bottomLeft, .xenoLib-centering-bottomMiddle, .xenoLib-centering-bottomRight { + flex-direction: column-reverse; + bottom: 0; + } + .XL-chl-p img{ + width: unset !important; + } + .xenoLib-error-text { + padding-top: 5px; + } + ` + ); + + XenoLib.joinClassNames = WebpackModules.getModule(e => e.default && e.default.default); + XenoLib.authorId = '239513071272329217'; + XenoLib.supportServerId = '389049952732446731'; + + try { + const getUserAsync = WebpackModules.getByProps('getUser', 'acceptAgreements').getUser; + const requestUser = () => + getUserAsync(XenoLib.authorId) + .then(user => (XenoLib.author = user)) + .catch(() => setTimeout(requestUser, 1 * 60 * 1000)); + if (UserStore.getUser(XenoLib.authorId)) XenoLib.author = UserStore.getUser(XenoLib.authorId); + else requestUser(); + } catch (e) { + Logger.stacktrace('Failed to grab author object', e); + } + + XenoLib.ReactComponents = {}; + + XenoLib.ReactComponents.ErrorBoundary = class XLErrorBoundary extends React.PureComponent { + constructor(props) { + super(props); + this.state = { hasError: false }; + } + componentDidCatch(err, inf) { + Logger.err(`Error in ${this.props.label}, screenshot or copy paste the error above to Lighty for help.`); + this.setState({ hasError: true }); + if (typeof this.props.onError === 'function') this.props.onError(err); + } + render() { + if (this.state.hasError) return null; + return this.props.children; + } + }; + + const deprecateFunction = (name, advice, ret = undefined) => () => (Logger.warn(`XenoLib.${name} is deprecated! ${advice}`), ret); + + XenoLib.patchContext = deprecateFunction('patchContext', 'Do manual patching of context menus instead.'); + + const CTXMenu = WebpackModules.getByProps('default', 'MenuStyle'); + + class ContextMenuWrapper extends React.PureComponent { + constructor(props) { + super(props); + this.handleOnClose = this.handleOnClose.bind(this); + } + handleOnClose() { + ContextMenuActions.closeContextMenu(); + if (this.props.target instanceof HTMLElement) this.props.target.focus(); + } + render() { + return React.createElement(CTXMenu.default, { onClose: this.handleOnClose, id: 'xenolib-context' }, this.props.menu); + } + } + XenoLib.createSharedContext = (element, menuCreation) => { + if (element.__XenoLib_ContextMenus) { + element.__XenoLib_ContextMenus.push(menuCreation); + } else { + element.__XenoLib_ContextMenus = [menuCreation]; + const oOnContextMenu = element.props.onContextMenu; + element.props.onContextMenu = e => (typeof oOnContextMenu === 'function' && oOnContextMenu(e), ContextMenuActions.openContextMenu(e, _ => React.createElement(XenoLib.ReactComponents.ErrorBoundary, { label: 'CTX Menu' }, React.createElement(ContextMenuWrapper, { menu: element.__XenoLib_ContextMenus.map(m => m()), ..._ })))); + } + }; + + const contextMenuItems = WebpackModules.find(m => m.MenuRadioItem && !m.default); + XenoLib.unpatchContext = deprecateFunction('unpatchContext', 'Manual patching needs manual unpatching'); + XenoLib.createContextMenuItem = (label, action, id, options = {}) => (!contextMenuItems ? null : React.createElement(contextMenuItems.MenuItem, { label, id, action: () => (!options.noClose && ContextMenuActions.closeContextMenu(), action()), ...options })); + XenoLib.createContextMenuSubMenu = (label, children, id, options = {}) => (!contextMenuItems ? null : React.createElement(contextMenuItems.MenuItem, { label, children, id, ...options })); + XenoLib.createContextMenuGroup = (children, options) => (!contextMenuItems ? null : React.createElement(contextMenuItems.MenuGroup, { children, ...options })); + + try { + XenoLib.ReactComponents.ButtonOptions = WebpackModules.getByProps('ButtonLink'); + XenoLib.ReactComponents.Button = XenoLib.ReactComponents.ButtonOptions.default; + } catch (e) { + Logger.stacktrace('Error getting Button component', e); + } + + const path = require('path'); + const isBBDBeta = typeof window.BDModules !== 'undefined' && !window.require && typeof window.BetterDiscordConfig !== 'undefined' && path.normalize(__dirname).replace(/[\\\/]/g, '/').toLowerCase().indexOf('rd_bd/plugins') !== -1; + // why zere? + if (isBBDBeta) Object.assign(window, require('timers')); + + function patchAddonCardAnyway(manualPatch) { + try { + if (patchAddonCardAnyway.patched) return; + patchAddonCardAnyway.patched = true; + const LinkClassname = XenoLib.joinClassNames(XenoLib.getClass('anchorUnderlineOnHover anchor'), XenoLib.getClass('anchor anchorUnderlineOnHover'), 'bda-author'); + const handlePatch = (_this, _, ret) => { + if (!_this.props.addon || !_this.props.addon.plugin || typeof _this.props.addon.plugin.getAuthor().indexOf('Lighty') === -1) return; + const settingsProps = Utilities.findInReactTree(ret, e => e && e.className === 'plugin-settings'); + if (settingsProps) delete settingsProps.id; + const author = Utilities.findInReactTree(ret, e => e && e.props && typeof e.props.className === 'string' && e.props.className.indexOf('bda-author') !== -1); + if (!author || typeof author.props.children !== 'string' || author.props.children.indexOf('Lighty') === -1) return; + const onClick = () => { + if (DiscordAPI.currentUser.id === XenoLib.authorId) return; + PrivateChannelActions.ensurePrivateChannel(DiscordAPI.currentUser.id, XenoLib.authorId).then(() => { + PrivateChannelActions.openPrivateChannel(DiscordAPI.currentUser.id, XenoLib.authorId); + LayerManager.popLayer(); + }); + }; + if (author.props.children === 'Lighty') { + author.type = 'a'; + author.props.className = LinkClassname; + author.props.onClick = onClick; + } else { + const idx = author.props.children.indexOf('Lighty'); + const pre = author.props.children.slice(0, idx); + const post = author.props.children.slice(idx + 6); + author.props.children = [ + pre, + React.createElement( + 'a', + { + className: LinkClassname, + onClick + }, + 'Lighty' + ), + post + ]; + delete author.props.onClick; + author.props.className = 'bda-author'; + author.type = 'span'; + } + let footerProps = Utilities.findInReactTree(ret, e => e && e.props && typeof e.props.className === 'string' && e.props.className.indexOf('bda-links') !== -1); + if (!footerProps) return; + footerProps = footerProps.props; + if (!Array.isArray(footerProps.children)) footerProps.children = [footerProps.children]; + const findLink = name => Utilities.findInReactTree(footerProps.children, e => e && e.props && e.props.children === name); + const websiteLink = findLink('Website'); + const sourceLink = findLink('Source'); + const supportServerLink = findLink('Support Server'); + footerProps.children = []; + if (websiteLink) { + const href = websiteLink.props.href; + delete websiteLink.props.href; + delete websiteLink.props.target; + websiteLink.props.onClick = () => window.open(href); + footerProps.children.push(websiteLink); + } + if (sourceLink) { + const href = sourceLink.props.href; + delete sourceLink.props.href; + delete sourceLink.props.target; + sourceLink.props.onClick = () => window.open(href); + footerProps.children.push(websiteLink ? ' | ' : null, sourceLink); + } + footerProps.children.push(websiteLink || sourceLink ? ' | ' : null, React.createElement('a', { className: 'bda-link bda-link-website', onClick: e => ContextMenuActions.openContextMenu(e, e => React.createElement(XenoLib.ReactComponents.ErrorBoundary, { label: 'Donate button CTX menu' }, React.createElement(ContextMenuWrapper, { menu: XenoLib.createContextMenuGroup([XenoLib.createContextMenuItem('Paypal', () => window.open('https://paypal.me/lighty13'), 'paypal'), XenoLib.createContextMenuItem('Ko-fi', () => window.open('https://ko-fi.com/lighty_'), 'kofi'), XenoLib.createContextMenuItem('Patreon', () => window.open('https://www.patreon.com/lightyp'), 'patreon')]), ...e }))) }, 'Donate')); + footerProps.children.push(' | ', supportServerLink || React.createElement('a', { className: 'bda-link bda-link-website', onClick: () => (LayerManager.popLayer(), InviteActions.acceptInviteAndTransitionToInviteChannel('NYvWdN5')) }, 'Support Server')); + footerProps.children.push(' | ', React.createElement('a', { className: 'bda-link bda-link-website', onClick: () => (_this.props.addon.plugin.showChangelog ? _this.props.addon.plugin.showChangelog() : Modals.showChangelogModal(_this.props.addon.plugin.getName() + ' Changelog', _this.props.addon.plugin.getVersion(), _this.props.addon.plugin.getChanges())) }, 'Changelog')); + footerProps = null; + }; + async function patchRewriteCard() { + const component = [...ReactComponents.components.entries()].find(([_, e]) => e.component && e.component.prototype && e.component.prototype.reload && e.component.prototype.showSettings); + const AddonCard = component ? component[1] : await ReactComponents.getComponent('AddonCard', '.bda-slist > .ui-switch-item', e => e.prototype && e.prototype.reload && e.prototype.showSettings); + if (CancelledAsync) return; + const ContentColumn = await ReactComponents.getComponent('ContentColumn', '.content-column'); + class PatchedAddonCard extends AddonCard.component { + render() { + const ret = super.render(); + try { + /* did I mention I am Lighty? */ + handlePatch(this, undefined, ret); + } catch (err) { + Logger.stacktrace('AddonCard patch', err); + } + return ret; + } + } + let firstRender = true; + Patcher.after(ContentColumn.component.prototype, 'render', (_, __, ret) => { + if (!LibrarySettings.addons.extra) return; + const list = Utilities.findInReactTree(ret, e => e && typeof e.className === 'string' && e.className.indexOf('bd-addon-list') !== -1); + if (Utilities.getNestedProp(list, 'children.0.props.children.type') !== AddonCard.component) return; + for (const item of list.children) { + const card = Utilities.getNestedProp(item, 'props.children'); + if (!card) continue; + card.type = PatchedAddonCard; + } + if (!firstRender) return; + ret.key = DiscordModules.KeyGenerator(); + firstRender = false; + }); + if (manualPatch) return; + ContentColumn.forceUpdateAll(); + AddonCard.forceUpdateAll(); + } + patchRewriteCard(); + } catch (e) { + Logger.stacktrace('Failed to patch V2C_*Card or AddonCard (BBD rewrite)', e); + } + } + if (LibrarySettings.addons.extra) patchAddonCardAnyway(); + + try { + XenoLib.ReactComponents.PluginFooter = class XLPluginFooter extends React.PureComponent { + render() { + if (LibrarySettings.addons.extra) return null; + return React.createElement( + 'div', + { + style: { + display: 'flex' + } + }, + React.createElement( + XenoLib.ReactComponents.Button, + { + style: { + flex: '2 1 auto' + }, + onClick: this.props.showChangelog + }, + 'Changelog' + ), + React.createElement( + XenoLib.ReactComponents.Button, + { + style: { + flex: '2 1 auto' + }, + onClick: e => ContextMenuActions.openContextMenu(e, e => React.createElement(XenoLib.ReactComponents.ErrorBoundary, { label: 'Donate button CTX menu' }, React.createElement(ContextMenuWrapper, { menu: XenoLib.createContextMenuGroup([XenoLib.createContextMenuItem('Paypal', () => window.open('https://paypal.me/lighty13'), 'paypal'), XenoLib.createContextMenuItem('Ko-fi', () => window.open('https://ko-fi.com/lighty_'), 'kofi'), XenoLib.createContextMenuItem('Patreon', () => window.open('https://www.patreon.com/lightyp'), 'patreon')]), ...e }))) + }, + 'Donate' + ), + React.createElement( + XenoLib.ReactComponents.Button, + { + style: { + flex: '2 1 auto' + }, + onClick: () => (LayerManager.popLayer(), InviteActions.acceptInviteAndTransitionToInviteChannel('NYvWdN5')) + }, + 'Support server' + ) + ); + } + }; + } catch (err) { + Logger.stacktrace('Error creating plugin footer'); + XenoLib.ReactComponents.PluginFooter = DiscordConstants.NOOP_NULL; + } + + const TextElement = WebpackModules.getByDisplayName('Text'); + + /* shared between FilePicker and ColorPicker */ + const MultiInputClassname = XenoLib.joinClassNames(Utilities.getNestedProp(DiscordClasses, 'BasicInputs.input.value'), XenoLib.getClass('multiInput')); + const MultiInputFirstClassname = XenoLib.getClass('multiInputFirst'); + const MultiInputFieldClassname = XenoLib.getClass('multiInputField'); + const ErrorMessageClassname = XenoLib.joinClassNames('xenoLib-error-text', XenoLib.getClass('errorMessage'), Utilities.getNestedProp(TextElement, 'Colors.ERROR')); + let ErrorClassname = XenoLib.getClass('input error'); + + // sometimes we can't access it for some reason, works thru BBD beta's webpack tho /shrug + if (isBBDBeta) setImmediate(() => { + const inputClasses = require('webpack').getByProps('multiInputFirst'); + ErrorClassname = inputClasses.error; + }); + + try { + const DelayedCall = WebpackModules.getByProps('DelayedCall').DelayedCall; + const FsModule = require('fs'); + /** + * @interface + * @name module:FilePicker + * @property {string} path + * @property {string} placeholder + * @property {Function} onChange + * @property {object} properties + * @property {bool} nullOnInvalid + * @property {bool} saveOnEnter + */ + XenoLib.ReactComponents.FilePicker = class FilePicker extends React.PureComponent { + constructor(props) { + super(props); + this.state = { + multiInputFocused: false, + path: props.path, + error: null + }; + XenoLib._.bindAll(this, ['handleOnBrowse', 'handleChange', 'checkInvalidDir']); + this.handleKeyDown = XenoLib._.throttle(this.handleKeyDown.bind(this), 500); + this.delayedCallVerifyPath = new DelayedCall(500, () => this.checkInvalidDir()); + } + checkInvalidDir(doSave) { + FsModule.access(this.state.path, FsModule.constants.W_OK, error => { + const invalid = (error && error.message.match(/.*: (.*), access '/)[1]) || null; + this.setState({ error: invalid }); + if (this.props.saveOnEnter && !doSave) return; + if (invalid) this.props.onChange(this.props.nullOnInvalid ? null : ''); + else this.props.onChange(this.state.path); + }); + } + handleOnBrowse() { + DiscordNative.fileManager.showOpenDialog({ title: this.props.title, properties: this.props.properties }).then(({ filePaths: [path] }) => { + if (path) this.handleChange(path); + }); + } + handleChange(path) { + this.setState({ path }); + this.delayedCallVerifyPath.delay(); + } + handleKeyDown(e) { + if (!this.props.saveOnEnter || e.which !== DiscordConstants.KeyboardKeys.ENTER) return; + this.checkInvalidDir(true); + } + render() { + const n = {}; + n[DiscordClasses.BasicInputs.focused] = this.state.multiInputFocused; + n[ErrorClassname] = !!this.state.error; + return React.createElement( + 'div', + { className: DiscordClasses.BasicInputs.inputWrapper, style: { width: '100%' } }, + React.createElement( + 'div', + { className: XenoLib.joinClassNames(MultiInputClassname, n) }, + React.createElement(DiscordModules.Textbox, { + value: this.state.path, + placeholder: this.props.placeholder, + onChange: this.handleChange, + onFocus: () => this.setState({ multiInputFocused: true }), + onBlur: () => this.setState({ multiInputFocused: false }), + onKeyDown: this.handleKeyDown, + autoFocus: false, + className: MultiInputFirstClassname, + inputClassName: MultiInputFieldClassname + }), + React.createElement(XenoLib.ReactComponents.Button, { onClick: this.handleOnBrowse, color: (!!this.state.error && XenoLib.ReactComponents.ButtonOptions.ButtonColors.RED) || XenoLib.ReactComponents.ButtonOptions.ButtonColors.GREY, look: XenoLib.ReactComponents.ButtonOptions.ButtonLooks.GHOST, size: XenoLib.ReactComponents.Button.Sizes.MEDIUM }, 'Browse') + ), + !!this.state.error && React.createElement('div', { className: ErrorMessageClassname }, 'Error: ', this.state.error) + ); + } + }; + } catch (e) { + Logger.stacktrace('Failed to create FilePicker component', e); + } + + /** + * @param {string} name - name label of the setting + * @param {string} note - help/note to show underneath or above the setting + * @param {string} value - current hex color + * @param {callable} onChange - callback to perform on setting change, callback receives hex string + * @param {object} [options] - object of options to give to the setting + * @param {boolean} [options.disabled=false] - should the setting be disabled + * @param {Array} [options.colors=presetColors] - preset list of colors + * @author Zerebos, from his library ZLibrary + */ + const FormItem = WebpackModules.getByDisplayName('FormItem'); + const DeprecatedModal = WebpackModules.getByDisplayName('DeprecatedModal'); + + const ColorPickerComponent = (_ => { + try { + const GuildSettingsRoles = WebpackModules.getByDisplayName('FluxContainer(GuildSettingsRoles)'); + const RoleSettingsContainer = GuildSettingsRoles.prototype.render.call({ + memoizedGetStateFromStores: _ => { } + }).type.prototype.renderRoleSettings.call({ + props: { + guild: { + id: '', + isOwner: _ => false + } + }, + getSelectedRole: _ => ({ id: '' }), + renderHeader: _ => null + }); + const RoleSettings = Utilities.findInReactTree(RoleSettingsContainer, e => e && e.type && e.type.displayName === "GuildRoleSettings").type.prototype.renderColorPicker.call({ + props: { + role: {} + } + }); + return RoleSettings.props.children.type; + } catch (err) { + Logger.stacktrace('Failed to get lazy colorpicker, unsurprisingly', err); + return _ => null; + } + })() + + class ColorPickerModal extends React.PureComponent { + constructor(props) { + super(props); + this.state = { value: props.value }; + XenoLib._.bindAll(this, ['handleChange']); + } + handleChange(value) { + this.setState({ value }); + this.props.onChange(ColorConverter.int2hex(value)); + } + render() { + return React.createElement( + DeprecatedModal, + { tag: 'form', onSubmit: this.handleSubmit, size: '' }, + React.createElement( + DeprecatedModal.Content, + {}, + React.createElement( + FormItem, + { className: XenoLib.joinClassNames(DiscordClasses.Margins.marginTop20.value, DiscordClasses.Margins.marginBottom20.value) }, + React.createElement(ColorPickerComponent, { + defaultColor: this.props.defaultColor, + colors: [16711680, 16746496, 16763904, 13434624, 65314, 65484, 61183, 43775, 26367, 8913151, 16711918, 16711782, 11730944, 11755264, 11767552, 9417472, 45848, 45967, 42931, 30643, 18355, 6226099, 11731111, 11731015], + value: this.state.value, + onChange: this.handleChange + }) + ) + ) + ); + } + } + + const ExtraButtonClassname = XenoLib.joinClassNames('xenoLib-button', XenoLib.getClass('recording button')); + const TextClassname = XenoLib.getClass('recording text'); + const DropperIcon = React.createElement('svg', { width: 16, height: 16, viewBox: '0 0 16 16' }, React.createElement('path', { d: 'M14.994 1.006C13.858-.257 11.904-.3 10.72.89L8.637 2.975l-.696-.697-1.387 1.388 5.557 5.557 1.387-1.388-.697-.697 1.964-1.964c1.13-1.13 1.3-2.985.23-4.168zm-13.25 10.25c-.225.224-.408.48-.55.764L.02 14.37l1.39 1.39 2.35-1.174c.283-.14.54-.33.765-.55l4.808-4.808-2.776-2.776-4.813 4.803z', fill: 'currentColor' })); + const ClockReverseIcon = React.createElement('svg', { width: 16, height: 16, viewBox: '0 0 24 24' }, React.createElement('path', { d: 'M13,3 C8.03,3 4,7.03 4,12 L1,12 L4.89,15.89 L4.96,16.03 L9,12 L6,12 C6,8.13 9.13,5 13,5 C16.87,5 20,8.13 20,12 C20,15.87 16.87,19 13,19 C11.07,19 9.32,18.21 8.06,16.94 L6.64,18.36 C8.27,19.99 10.51,21 13,21 C17.97,21 22,16.97 22,12 C22,7.03 17.97,3 13,3 L13,3 Z M12,8 L12,13 L16.28,15.54 L17,14.33 L13.5,12.25 L13.5,8 L12,8 L12,8 Z', fill: 'currentColor' })); + class ColorPicker extends React.PureComponent { + constructor(props) { + super(props); + this.state = { + error: null, + value: props.value, + multiInputFocused: false + }; + XenoLib._.bindAll(this, ['handleChange', 'handleColorPicker', 'handleReset']); + } + handleChange(value) { + if (!value.length) { + this.state.error = 'You must input a hex string'; + } else if (!ColorConverter.isValidHex(value)) { + this.state.error = 'Invalid hex string'; + } else { + this.state.error = null; + } + this.setState({ value }); + this.props.onChange(!value.length || !ColorConverter.isValidHex(value) ? this.props.defaultColor : value); + } + handleColorPicker() { + const modalId = ModalStack.push(e => React.createElement(XenoLib.ReactComponents.ErrorBoundary, { label: 'color picker modal', onError: () => ModalStack.popWithKey(modalId) }, React.createElement(ColorPickerModal, { ...e, defaultColor: ColorConverter.hex2int(this.props.defaultColor), value: ColorConverter.hex2int(this.props.value), onChange: this.handleChange }))); + } + handleReset() { + this.handleChange(this.props.defaultColor); + } + render() { + const n = {}; + n[DiscordClasses.BasicInputs.focused] = this.state.multiInputFocused; + n[ErrorClassname] = !!this.state.error; + return React.createElement( + 'div', + { className: XenoLib.joinClassNames(DiscordClasses.BasicInputs.inputWrapper.value, 'xenoLib-color-picker'), style: { width: '100%' } }, + React.createElement( + 'div', + { className: XenoLib.joinClassNames(MultiInputClassname, n) }, + React.createElement('div', { + className: XenoLib.ReactComponents.Button.Sizes.SMALL, + style: { + backgroundColor: this.state.value, + height: 38 + } + }), + React.createElement(DiscordModules.Textbox, { + value: this.state.value, + placeholder: 'Hex color', + onChange: this.handleChange, + onFocus: () => this.setState({ multiInputFocused: true }), + onBlur: () => this.setState({ multiInputFocused: false }), + autoFocus: false, + className: MultiInputFirstClassname, + inputClassName: MultiInputFieldClassname + }), + React.createElement( + XenoLib.ReactComponents.Button, + { + onClick: this.handleColorPicker, + color: (!!this.state.error && XenoLib.ReactComponents.ButtonOptions.ButtonColors.RED) || XenoLib.ReactComponents.ButtonOptions.ButtonColors.GREY, + look: XenoLib.ReactComponents.ButtonOptions.ButtonLooks.GHOST, + size: XenoLib.ReactComponents.Button.Sizes.MIN, + className: ExtraButtonClassname + }, + React.createElement('span', { className: TextClassname }, 'Color picker'), + React.createElement( + 'span', + { + className: 'xenoLib-button-icon' + }, + DropperIcon + ) + ), + React.createElement( + XenoLib.ReactComponents.Button, + { + onClick: this.handleReset, + color: (!!this.state.error && XenoLib.ReactComponents.ButtonOptions.ButtonColors.RED) || XenoLib.ReactComponents.ButtonOptions.ButtonColors.GREY, + look: XenoLib.ReactComponents.ButtonOptions.ButtonLooks.GHOST, + size: XenoLib.ReactComponents.Button.Sizes.MIN, + className: ExtraButtonClassname + }, + React.createElement('span', { className: TextClassname }, 'Reset'), + React.createElement( + 'span', + { + className: 'xenoLib-button-icon xenoLib-revert' + }, + ClockReverseIcon + ) + ) + ), + !!this.state.error && React.createElement('div', { className: ErrorMessageClassname }, 'Error: ', this.state.error) + ); + } + } + XenoLib.Settings = {}; + XenoLib.Settings.FilePicker = class FilePickerSettingField extends Settings.SettingField { + constructor(name, note, value, onChange, options = { properties: ['openDirectory', 'createDirectory'], placeholder: 'Path to folder', defaultPath: '' }) { + super(name, note, onChange, XenoLib.ReactComponents.FilePicker || class b { }, { + onChange: reactElement => path => { + this.onChange(path ? path : options.defaultPath); + }, + path: value, + nullOnInvalid: true, + ...options + }); + } + }; + XenoLib.Settings.ColorPicker = class ColorPickerSettingField extends Settings.SettingField { + constructor(name, note, value, onChange, options = {}) { + super(name, note, onChange, ColorPicker, { + disabled: options.disabled ? true : false, + onChange: reactElement => color => { + this.onChange(color); + }, + defaultColor: typeof options.defaultColor !== 'undefined' ? options.defaultColor : ColorConverter.int2hex(DiscordConstants.DEFAULT_ROLE_COLOR), + value + }); + } + }; + + XenoLib.Settings.PluginFooter = class PluginFooterField extends Settings.SettingField { + constructor(showChangelog) { + super('', '', DiscordConstants.NOOP, XenoLib.ReactComponents.PluginFooter, { + showChangelog + }); + } + }; + + XenoLib.changeName = (currentName, newName) => { + try { + const path = require('path'); + const fs = require('fs'); + const pluginsFolder = path.dirname(currentName); + const pluginName = path.basename(currentName).match(/^[^\.]+/)[0]; + if (pluginName === newName) return true; + const wasEnabled = BdApi.Plugins && BdApi.Plugins.isEnabled ? BdApi.Plugins.isEnabled(pluginName) : global.pluginCookie && pluginCookie[pluginName]; + fs.accessSync(currentName, fs.constants.W_OK | fs.constants.R_OK); + const files = fs.readdirSync(pluginsFolder); + files.forEach(file => { + if (!file.startsWith(pluginName) || file.startsWith(newName) || file.indexOf('.plugin.js') !== -1) return; + fs.renameSync(path.resolve(pluginsFolder, file), path.resolve(pluginsFolder, `${newName}${file.match(new RegExp(`^${pluginName}(.*)`))[1]}`)); + }); + fs.renameSync(currentName, path.resolve(pluginsFolder, `${newName}.plugin.js`)); + XenoLib.Notifications.success(`[**XenoLib**] \`${pluginName}\` file has been renamed to \`${newName}\``); + if ((!BdApi.Plugins || !BdApi.Plugins.isEnabled || !BdApi.Plugins.enable) && (!global.pluginCookie || !global.pluginModule)) Modals.showAlertModal('Plugin has been renamed', 'Plugin has been renamed, but your client mod has a missing feature, as such, the plugin could not be enabled (if it even was enabled).'); + else { + if (!wasEnabled) return; + setTimeout(() => (BdApi.Plugins && BdApi.Plugins.enable ? BdApi.Plugins.enable(newName) : pluginModule.enablePlugin(newName)), 1000); /* /shrug */ + } + } catch (e) { + Logger.stacktrace('There has been an issue renaming a plugin', e); + } + }; + + const FancyParser = (() => { + const ParsersModule = WebpackModules.getByProps('astParserFor', 'parse'); + try { + const DeepClone = WebpackModules.find(m => m.default && m.default.toString().indexOf('/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(') !== -1 && !m.useVirtualizedAnchor).default; + const ReactParserRules = (WebpackModules.find(m => m.default && m.default.toString().search(/function\(\w\){return \w\({},\w,{link:\(0,\w\.default\)\(\w\)}\)}$/) !== -1) || WebpackModules.find(m => m.default && m.default.toString().search(/function\(\){return \w}$/) !== -1)).default; /* thanks Zere for not fixing the bug ._. */ + const FANCY_PANTS_PARSER_RULES = DeepClone([WebpackModules.getByProps('RULES').RULES, ReactParserRules()]); + const { defaultRules } = WebpackModules.getByProps('defaultParse'); + FANCY_PANTS_PARSER_RULES.image = defaultRules.image; + FANCY_PANTS_PARSER_RULES.link = defaultRules.link; + return ParsersModule.reactParserFor(FANCY_PANTS_PARSER_RULES); + } catch (e) { + Logger.stacktrace('Failed to create special parser', e); + try { + return ParsersModule.parse; + } catch (e) { + Logger.stacktrace('Failed to get even basic parser', e); + return e => e; + } + } + })(); + const AnchorClasses = WebpackModules.getByProps('anchor', 'anchorUnderlineOnHover') || {}; + const EmbedVideo = (() => { + try { + return WebpackModules.getByProps('EmbedVideo').EmbedVideo; + } catch (e) { + Logger.stacktrace('Failed to get EmbedVideo!', e); + return DiscordConstants.NOOP_NULL; + } + })(); + const VideoComponent = (() => { + try { + const ret = new (WebpackModules.getByDisplayName('MediaPlayer'))({}).render(); + const vc = Utilities.findInReactTree(ret, e => e && e.props && typeof e.props.className === 'string' && e.props.className.indexOf('video-8eMOth') !== -1); + return vc.type; + } catch (e) { + Logger.stacktrace('Failed to get the video component', e); + return DiscordConstants.NOOP_NULL; + } + })(); + const ComponentRenderers = WebpackModules.getByProps('renderVideoComponent') || {}; + const NewModalStack = WebpackModules.getByProps('openModal', 'hasModalOpen'); + /* MY CHANGELOG >:C */ + XenoLib.showChangelog = (title, version, changelog, footer) => { + const ChangelogClasses = DiscordClasses.Changelog; + const items = []; + let isFistType = true; + for (let i = 0; i < changelog.length; i++) { + const item = changelog[i]; + switch (item.type) { + case 'image': + items.push(React.createElement('img', { alt: '', src: item.src, width: item.width || 451, height: item.height || 254 })); + continue; + case 'video': + items.push(React.createElement(VideoComponent, { src: item.src, poster: item.thumbnail, width: item.width || 451, height: item.height || 254, loop: !0, muted: !0, autoPlay: !0, className: ChangelogClasses.video })); + continue; + case 'youtube': + items.push(React.createElement(EmbedVideo, { className: ChangelogClasses.video, allowFullScreen: !1, href: `https://youtu.be/${item.youtube_id}`, thumbnail: { url: `https://i.ytimg.com/vi/${item.youtube_id}/maxresdefault.jpg`, width: item.width || 451, height: item.height || 254 }, video: { url: `https://www.youtube.com/embed/${item.youtube_id}?vq=large&rel=0&controls=0&showinfo=0`, width: item.width || 451, height: item.height || 254 }, width: item.width || 451, height: item.height || 254, renderVideoComponent: ComponentRenderers.renderVideoComponent || DiscordConstants.NOOP_NULL, renderImageComponent: ComponentRenderers.renderImageComponent || DiscordConstants.NOOP_NULL, renderLinkComponent: ComponentRenderers.renderMaskedLinkComponent || DiscordConstants.NOOP_NULL })); + continue; + case 'description': + items.push(React.createElement('p', {}, FancyParser(item.content))); + continue; + default: + const logType = ChangelogClasses[item.type] || ChangelogClasses.added; + items.push(React.createElement('h1', { className: XenoLib.joinClassNames(logType.value, { [ChangelogClasses.marginTop.value]: item.marginTop || isFistType }) }, item.title)); + items.push( + React.createElement( + 'ul', + { className: 'XL-chl-p' }, + item.items.map(e => + React.createElement( + 'li', + {}, + React.createElement( + 'p', + {}, + Array.isArray(e) + ? e.map(e => + Array.isArray(e) + ? React.createElement( + 'ul', + {}, + e.map(e => React.createElement('li', {}, FancyParser(e))) + ) + : FancyParser(e) + ) + : FancyParser(e) + ) + ) + ) + ) + ); + isFistType = false; + } + } + const renderFooter = () => ['Need support? ', React.createElement('a', { className: XenoLib.joinClassNames(AnchorClasses.anchor, AnchorClasses.anchorUnderlineOnHover), onClick: () => (LayerManager.popLayer(), ModalStack.pop(), InviteActions.acceptInviteAndTransitionToInviteChannel('NYvWdN5')) }, 'Join my support server'), '! Or consider donating via ', React.createElement('a', { className: XenoLib.joinClassNames(AnchorClasses.anchor, AnchorClasses.anchorUnderlineOnHover), onClick: () => window.open('https://paypal.me/lighty13') }, 'Paypal'), ', ', React.createElement('a', { className: XenoLib.joinClassNames(AnchorClasses.anchor, AnchorClasses.anchorUnderlineOnHover), onClick: () => window.open('https://ko-fi.com/lighty_') }, 'Ko-fi'), ', ', React.createElement('a', { className: XenoLib.joinClassNames(AnchorClasses.anchor, AnchorClasses.anchorUnderlineOnHover), onClick: () => window.open('https://www.patreon.com/lightyp') }, 'Patreon'), '!']; + NewModalStack.openModal(props => React.createElement(XenoLib.ReactComponents.ErrorBoundary, { label: 'Changelog', onError: () => props.onClose() }, React.createElement(ChangelogModal, { className: ChangelogClasses.container, selectable: true, onScroll: _ => _, onClose: _ => _, renderHeader: () => React.createElement(FlexChild.Child, { grow: 1, shrink: 1 }, React.createElement(Titles.default, { tag: Titles.Tags.H4 }, title), React.createElement(TextElement, { size: TextElement.Sizes.SIZE_12, className: ChangelogClasses.date }, `Version ${version}`)), renderFooter: () => React.createElement(FlexChild.Child, { gro: 1, shrink: 1 }, React.createElement(TextElement, { size: TextElement.Sizes.SIZE_12 }, footer ? (typeof footer === 'string' ? FancyParser(footer) : footer) : renderFooter())), children: items, ...props }))); + }; + + /* https://github.com/react-spring/zustand + * MIT License + * + * Copyright (c) 2019 Paul Henschel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + XenoLib.zustand = createState => { + var state; + var listeners = new Set(); + const setState = partial => { + var partialState = typeof partial === 'function' ? partial(state) : partial; + if (partialState !== state) { + state = Object.assign({}, state, partialState); + listeners.forEach(function (listener) { + return listener(); + }); + } + }; + const getState = () => state; + const getSubscriber = (listener, selector, equalityFn) => { + if (selector === void 0) selector = getState; + if (equalityFn === void 0) equalityFn = Object.is; + return { currentSlice: selector(state), equalityFn: equalityFn, errored: false, listener: listener, selector: selector, unsubscribe: function unsubscribe() { } }; + }; + var subscribe = function subscribe(subscriber) { + function listener() { + // Selector or equality function could throw but we don't want to stop + // the listener from being called. + // https://github.com/react-spring/zustand/pull/37 + try { + var newStateSlice = subscriber.selector(state); + if (!subscriber.equalityFn(subscriber.currentSlice, newStateSlice)) subscriber.listener((subscriber.currentSlice = newStateSlice)); + } catch (error) { + subscriber.errored = true; + subscriber.listener(null, error); + } + } + + listeners.add(listener); + return () => listeners.delete(listener); + }; + const apiSubscribe = (listener, selector, equalityFn) => subscribe(getSubscriber(listener, selector, equalityFn)); + const destroy = () => listeners.clear(); + const useStore = (selector, equalityFn) => { + if (selector === void 0) selector = getState; + if (equalityFn === void 0) equalityFn = Object.is; + var forceUpdate = React.useReducer(c => c + 1, 0)[1]; + var subscriberRef = React.useRef(); + if (!subscriberRef.current) { + subscriberRef.current = getSubscriber(forceUpdate, selector, equalityFn); + subscriberRef.current.unsubscribe = subscribe(subscriberRef.current); + } + var subscriber = subscriberRef.current; + var newStateSlice; + var hasNewStateSlice = false; // The selector or equalityFn need to be called during the render phase if + // they change. We also want legitimate errors to be visible so we re-run + // them if they errored in the subscriber. + if (subscriber.selector !== selector || subscriber.equalityFn !== equalityFn || subscriber.errored) { + // Using local variables to avoid mutations in the render phase. + newStateSlice = selector(state); + hasNewStateSlice = !equalityFn(subscriber.currentSlice, newStateSlice); + } // Syncing changes in useEffect. + React.useLayoutEffect(function () { + if (hasNewStateSlice) subscriber.currentSlice = newStateSlice; + subscriber.selector = selector; + subscriber.equalityFn = equalityFn; + subscriber.errored = false; + }); + React.useLayoutEffect(() => subscriber.unsubscribe, []); + return hasNewStateSlice ? newStateSlice : subscriber.currentSlice; + }; + const api = { setState: setState, getState: getState, subscribe: apiSubscribe, destroy: destroy }; + state = createState(setState, getState, api); + return [useStore, api]; + }; + /* NOTIFICATIONS START */ + let UPDATEKEY = {}; + try { + if (canUseUntitledNotifAPI) { + const defaultOptions = { + loading: false, + progress: -1, + channelId: undefined, + timeout: 3500, + color: '#2196f3', + onLeave: DiscordConstants.NOOP + }; + const utils = { + success(content, options = {}) { + return this.show(content, Object.assign({ color: '#43b581' }, options)); + }, + info(content, options = {}) { + return this.show(content, Object.assign({ color: '#4a90e2' }, options)); + }, + warning(content, options = {}) { + return this.show(content, Object.assign({ color: '#ffa600' }, options)); + }, + danger(content, options = {}) { + return this.show(content, Object.assign({ color: '#f04747' }, options)); + }, + error(content, options = {}) { + return this.danger(content, options); + }, + /** + * @param {string|HTMLElement|React} content - Content to display. If it's a string, it'll be formatted with markdown, including URL support [like this](https://google.com/) + * @param {object} options + * @param {string} [options.channelId] Channel ID if content is a string which gets formatted, and you want to mention a role for example. + * @param {Number} [options.timeout] Set to 0 to keep it permanently until user closes it, or if you want a progress bar + * @param {Boolean} [options.loading] Makes the bar animate differently instead of fading in and out slowly + * @param {Number} [options.progress] 0-100, -1 sets it to 100%, setting it to 100% closes the notification automatically + * @param {string} [options.color] Bar color + * @param {string} [options.allowDuplicates] By default, notifications that are similar get grouped together, use true to disable that + * @param {function} [options.onLeave] Callback when notification is leaving + * @return {Number} - Notification ID. Store this if you plan on force closing it, changing its content or want to set the progress + */ + show(content, options = {}) { + const { timeout, loading, progress, color, allowDuplicates, onLeave, channelId, onClick, onContext } = Object.assign(Utilities.deepclone(defaultOptions), options); + return Untitled.n11s.show(content instanceof HTMLElement ? ReactTools.createWrappedElement(content) : content, { + timeout, + loading, + progress, + color, + allowDuplicates, + onClick, + onContext, + onClose: onLeave, + markdownOptions: { channelId } + }); + }, + remove(id) { + Untitled.n11s.remove(id); + }, + /** + * @param {Number} id Notification ID + * @param {object} options + * @param {string} [options.channelId] Channel ID if content is a string which gets formatted, and you want to mention a role for example. + * @param {Boolean} [options.loading] Makes the bar animate differently instead of fading in and out slowly + * @param {Number} [options.progress] 0-100, -1 sets it to 100%, setting it to 100% closes the notification automatically + * @param {string} [options.color] Bar color + * @param {function} [options.onLeave] Callback when notification is leaving + */ + update(id, options) { + const obj = {}; + for (const key in ['loading', 'progress', 'color', 'onClick', 'onContext']) if (typeof options[key] !== 'undefined') obj[key] = options[key]; + if (options.onLeave) obj.onClose = options.onLeave; + if (options.channelId) obj.markdownOptions = { channelId: options.channelId }; + Untitled.n11s.update(id, obj); + }, + exists(id) { + return Untitled.n11s.exists(id); + } + }; + XenoLib.Notifications = utils; + } else { + const DeepEqualityCheck = (content1, content2) => { + if (typeof content1 !== typeof content2) return false; + const isCNT1HTML = content1 instanceof HTMLElement; + const isCNT2HTML = content2 instanceof HTMLElement; + if (isCNT1HTML !== isCNT2HTML) return false; + else if (isCNT1HTML) return content1.isEqualNode(content2); + if (content1 !== content2) { + if (Array.isArray(content1)) { + if (content1.length !== content2.length) return false; + for (const [index, item] of content1.entries()) { + if (!DeepEqualityCheck(item, content2[index])) return false; + } + } else if (typeof content1 === 'object') { + if (content1.type) { + if (typeof content1.type !== typeof content2.type) return false; + if (content1.type !== content2.type) return false; + } + if (typeof content1.props !== typeof content2.props) return false; + if (content1.props) { + if (Object.keys(content1.props).length !== Object.keys(content2.props).length) return false; + for (const prop in content1.props) { + if (!DeepEqualityCheck(content1.props[prop], content2.props[prop])) return false; + } + } + } else return false; + } + return true; + }; + const [useStore, api] = XenoLib.zustand(e => ({ data: [] })); + const defaultOptions = { + loading: false, + progress: -1, + channelId: undefined, + timeout: 3500, + color: '#2196f3', + onLeave: DiscordConstants.NOOP + }; + const utils = { + success(content, options = {}) { + return this.show(content, Object.assign({ color: '#43b581' }, options)); + }, + info(content, options = {}) { + return this.show(content, Object.assign({ color: '#4a90e2' }, options)); + }, + warning(content, options = {}) { + return this.show(content, Object.assign({ color: '#ffa600' }, options)); + }, + danger(content, options = {}) { + return this.show(content, Object.assign({ color: '#f04747' }, options)); + }, + error(content, options = {}) { + return this.danger(content, options); + }, + /** + * @param {string|HTMLElement|React} content - Content to display. If it's a string, it'll be formatted with markdown, including URL support [like this](https://google.com/) + * @param {object} options + * @param {string} [options.channelId] Channel ID if content is a string which gets formatted, and you want to mention a role for example. + * @param {Number} [options.timeout] Set to 0 to keep it permanently until user closes it, or if you want a progress bar + * @param {Boolean} [options.loading] Makes the bar animate differently instead of fading in and out slowly + * @param {Number} [options.progress] 0-100, -1 sets it to 100%, setting it to 100% closes the notification automatically + * @param {string} [options.color] Bar color + * @param {string} [options.allowDuplicates] By default, notifications that are similar get grouped together, use true to disable that + * @param {function} [options.onLeave] Callback when notification is leaving + * @return {Number} - Notification ID. Store this if you plan on force closing it, changing its content or want to set the progress + */ + show(content, options = {}) { + let id = null; + options = Object.assign(Utilities.deepclone(defaultOptions), options); + api.setState(state => { + if (!options.allowDuplicates) { + const notif = state.data.find(n => DeepEqualityCheck(n.content, content) && n.timeout === options.timeout && !n.leaving); + if (notif) { + id = notif.id; + Dispatcher.dirtyDispatch({ type: 'XL_NOTIFS_DUPLICATE', id: notif.id }); + return state; + } + } + if (state.data.length >= 100) return state; + do { + id = Math.floor(4294967296 * Math.random()); + } while (state.data.findIndex(n => n.id === id) !== -1); + return { data: [].concat(state.data, [{ content, ...options, id }]) }; + }); + return id; + }, + remove(id) { + Dispatcher.dirtyDispatch({ type: 'XL_NOTIFS_REMOVE', id }); + }, + /** + * @param {Number} id Notification ID + * @param {object} options + * @param {string} [options.channelId] Channel ID if content is a string which gets formatted, and you want to mention a role for example. + * @param {Boolean} [options.loading] Makes the bar animate differently instead of fading in and out slowly + * @param {Number} [options.progress] 0-100, -1 sets it to 100%, setting it to 100% closes the notification automatically + * @param {string} [options.color] Bar color + * @param {function} [options.onLeave] Callback when notification is leaving + */ + update(id, options) { + delete options.id; + api.setState(state => { + const idx = state.data.findIndex(n => n.id === id); + if (idx === -1) return state; + state.data[idx] = Object.assign(state.data[idx], options); + return state; + }); + Dispatcher.dirtyDispatch({ type: 'XL_NOTIFS_UPDATE', id, ...options }); + }, + exists(id) { + return api.getState().data.findIndex(e => e.id === id && !e.leaving) !== -1; + } + }; + XenoLib.Notifications = utils; + const ReactSpring = WebpackModules.getByProps('useTransition'); + const BadgesModule = WebpackModules.getByProps('NumberBadge'); + const CloseButton = React.createElement('svg', { width: 16, height: 16, viewBox: '0 0 24 24' }, React.createElement('path', { d: 'M18.4 4L12 10.4L5.6 4L4 5.6L10.4 12L4 18.4L5.6 20L12 13.6L18.4 20L20 18.4L13.6 12L20 5.6L18.4 4Z', fill: 'currentColor' })); + class Notification extends React.PureComponent { + constructor(props) { + super(props); + this.state = { + closeFast: false /* close button pressed, XL_NOTIFS_REMOVE dispatch */, + offscreen: false /* don't do anything special if offscreen, not timeout */, + counter: 1 /* how many times this notification was shown */, + resetBar: false /* reset bar to 0 in the event counter goes up */, + hovered: false, + leaving: true /* prevent hover events from messing up things */, + loading: props.loading /* loading animation, enable progress */, + progress: props.progress /* -1 means undetermined */, + content: props.content, + contentParsed: this.parseContent(props.content, props.channelId), + color: props.color + }; + this._contentRef = null; + this._ref = null; + this._animationCancel = DiscordConstants.NOOP; + this._oldOffsetHeight = 0; + this._initialProgress = !this.props.timeout ? (this.state.loading && this.state.progress !== -1 ? this.state.progress : 100) : 0; + XenoLib._.bindAll(this, ['closeNow', 'handleDispatch', '_setContentRef']); + this.handleResizeEvent = XenoLib._.throttle(this.handleResizeEvent.bind(this), 100); + this.resizeObserver = new ResizeObserver(this.handleResizeEvent); + this._timeout = props.timeout; + } + componentDidMount() { + this._unsubscribe = api.subscribe(_ => this.checkOffScreen()); + window.addEventListener('resize', this.handleResizeEvent); + Dispatcher.subscribe('XL_NOTIFS_DUPLICATE', this.handleDispatch); + Dispatcher.subscribe('XL_NOTIFS_REMOVE', this.handleDispatch); + Dispatcher.subscribe('XL_NOTIFS_UPDATE', this.handleDispatch); + Dispatcher.subscribe('XL_NOTIFS_ANIMATED', this.handleDispatch); + Dispatcher.subscribe('XL_NOTIFS_SETTINGS_UPDATE', this.handleDispatch); + } + componentWillUnmount() { + this._unsubscribe(); + window.window.removeEventListener('resize', this.handleResizeEvent); + Dispatcher.unsubscribe('XL_NOTIFS_DUPLICATE', this.handleDispatch); + Dispatcher.unsubscribe('XL_NOTIFS_REMOVE', this.handleDispatch); + Dispatcher.unsubscribe('XL_NOTIFS_UPDATE', this.handleDispatch); + Dispatcher.unsubscribe('XL_NOTIFS_ANIMATED', this.handleDispatch); + Dispatcher.unsubscribe('XL_NOTIFS_SETTINGS_UPDATE', this.handleDispatch); + this.resizeObserver.disconnect(); + this.resizeObserver = null; /* no mem leaks plz */ + this._ref = null; + this._contentRef = null; + } + handleDispatch(e) { + if (this.state.leaving || this.state.closeFast) return; + if (e.type === 'XL_NOTIFS_SETTINGS_UPDATE') { + if (e.key !== UPDATEKEY) return; + this._animationCancel(); + this.forceUpdate(); + return; + } + if (e.type === 'XL_NOTIFS_ANIMATED') this.checkOffScreen(); + if (e.id !== this.props.id) return; + const { content, channelId, loading, progress, color } = e; + const { content: curContent, channelId: curChannelId, loading: curLoading, progress: curProgress, color: curColor } = this.state; + switch (e.type) { + case 'XL_NOTIFS_REMOVE': + this.closeNow(); + break; + case 'XL_NOTIFS_DUPLICATE': + this._animationCancel(); + this.setState({ counter: this.state.counter + 1, resetBar: !!this.props.timeout, closeFast: false }); + break; + case 'XL_NOTIFS_UPDATE': + if (!this.state.initialAnimDone) { + this.state.content = content || curContent; + this.state.channelId = channelId || curChannelId; + this.state.contentParsed = this.parseContent(content || curContent, channelId || curChannelId); + if (typeof loading !== 'undefined') this.state.loading = loading; + if (typeof progress !== 'undefined') this.state.progress = progress; + this.state.color = color || curColor; + return; + } + this._animationCancel(); + this.setState({ + content: content || curContent, + channelId: channelId || curChannelId, + contentParsed: this.parseContent(content || curContent, channelId || curChannelId), + loading: typeof loading !== 'undefined' ? loading : curLoading, + progress: typeof progress !== 'undefined' ? progress : curProgress, + color: color || curColor + }); + break; + } + } + parseContent(content, channelId) { + if (typeof content === 'string') return FancyParser(content, true, { channelId }); + else if (content instanceof Element) return ReactTools.createWrappedElement(content); + else return content; + } + checkOffScreen() { + if (this.state.leaving || !this._contentRef) return; + const bcr = this._contentRef.getBoundingClientRect(); + if (Math.floor(bcr.bottom) - 1 > Structs.Screen.height || Math.ceil(bcr.top) + 1 < 0) { + if (!this.state.offscreen) { + this._animationCancel(); + this.setState({ offscreen: true }); + } + } else if (this.state.offscreen) { + this._animationCancel(); + this.setState({ offscreen: false }); + } + } + closeNow() { + if (this.state.closeFast) return; + this.resizeObserver.disconnect(); + this._animationCancel(); + api.setState(state => { + const dt = state.data.find(m => m.id === this.props.id); + if (dt) dt.leaving = true; + return { data: state.data }; + }); + this.setState({ closeFast: true }); + } + handleResizeEvent() { + if (this._oldOffsetHeight !== this._contentRef.offsetHeight) { + this._animationCancel(); + this.forceUpdate(); + } + } + _setContentRef(ref) { + if (!ref) return; + this._contentRef = ref; + this.resizeObserver.observe(ref); + } + render() { + const config = { duration: 200 }; + if (this._contentRef) this._oldOffsetHeight = this._contentRef.offsetHeight; + return React.createElement( + ReactSpring.Spring, + { + native: true, + from: { opacity: 0, height: 0, progress: this._initialProgress, loadbrightness: 1 }, + to: async (next, cancel) => { + this.state.leaving = false; + this._animationCancel = cancel; + if (this.state.offscreen) { + if (this.state.closeFast) { + this.state.leaving = true; + await next({ opacity: 0, height: 0 }); + api.setState(state => ({ data: state.data.filter(n => n.id !== this.props.id) })); + return; + } + await next({ opacity: 1, height: this._contentRef.offsetHeight, loadbrightness: 1 }); + if (this.props.timeout) { + await next({ progress: 0 }); + } else { + if (this.state.loading && this.state.progress !== -1) { + await next({ progress: 0 }); + } else { + await next({ progress: 100 }); + } + } + return; + } + const isSettingHeight = this._ref.offsetHeight !== this._contentRef.offsetHeight; + await next({ opacity: 1, height: this._contentRef.offsetHeight }); + if (isSettingHeight) Dispatcher.dirtyDispatch({ type: 'XL_NOTIFS_ANIMATED' }); + this.state.initialAnimDone = true; + if (this.state.resetBar || (this.state.hovered && LibrarySettings.notifications.timeoutReset)) { + await next({ progress: 0 }); /* shit gets reset */ + this.state.resetBar = false; + } + + if (!this.props.timeout && !this.state.closeFast) { + if (!this.state.loading) { + await next({ progress: 100 }); + } else { + await next({ loadbrightness: 1 }); + if (this.state.progress === -1) await next({ progress: 100 }); + else await next({ progress: this.state.progress }); + } + if (this.state.progress < 100 || !this.state.loading) return; + } + if (this.state.hovered && !this.state.closeFast) return; + if (!this.state.closeFast && !LibrarySettings.notifications.timeoutReset) this._startProgressing = Date.now(); + await next({ progress: 100 }); + if (this.state.hovered && !this.state.closeFast) return; /* race condition: notif is hovered, but it continues and closes! */ + this.state.leaving = true; + if (!this.state.closeFast) { + api.setState(state => { + const dt = state.data.find(m => m.id === this.props.id); + if (dt) dt.leaving = true; + return { data: state.data }; + }); + } + this.props.onLeave(); + await next({ opacity: 0, height: 0 }); + api.setState(state => ({ data: state.data.filter(n => n.id !== this.props.id) })); + }, + config: key => { + if (key === 'progress') { + let duration = this._timeout; + if (this.state.closeFast || !this.props.timeout || this.state.resetBar || this.state.hovered) duration = 150; + if (this.state.offscreen) duration = 0; /* don't animate at all */ + return { duration }; + } + if (key === 'loadbrightness') return { duration: 750 }; + return config; + } + }, + e => { + return React.createElement( + ReactSpring.animated.div, + { + style: { + height: e.height, + opacity: e.opacity + }, + className: 'xenoLib-notification', + ref: e => e && (this._ref = e) + }, + React.createElement( + 'div', + { + className: 'xenoLib-notification-content-wrapper', + ref: this._setContentRef, + onMouseEnter: e => { + if (this.state.leaving || !this.props.timeout || this.state.closeFast) return; + this._animationCancel(); + if (this._startProgressing) { + this._timeout -= Date.now() - this._startProgressing; + } + this.state.hovered = true; + this.forceUpdate(); + }, + onMouseLeave: e => { + if (this.state.leaving || !this.props.timeout || this.state.closeFast) return; + this._animationCancel(); + this.setState({ hovered: false }); + }, + style: { + '--grad-one': this.state.color, + '--grad-two': ColorConverter.lightenColor(this.state.color, 20), + '--bar-color': ColorConverter.darkenColor(this.state.color, 30) + }, + onClick: e => { + if (!this.props.onClick) return; + if (e.target && e.target.getAttribute('role') === 'button') return; + this.props.onClick(); + this.closeNow(); + }, + onContextMenu: e => { + if (!this.props.onContext) return; + this.props.onContext(); + this.closeNow(); + } + }, + React.createElement( + 'div', + { + className: 'xenoLib-notification-content', + style: { + backdropFilter: LibrarySettings.notifications.backdrop ? 'blur(5px)' : undefined, + background: ColorConverter.int2rgba(ColorConverter.hex2int(LibrarySettings.notifications.backdropColor), LibrarySettings.notifications.backdrop ? 0.3 : 1.0), + border: LibrarySettings.notifications.backdrop ? 'none' : undefined + }, + ref: e => { + if (!LibrarySettings.notifications.backdrop || !e) return; + e.style.setProperty('backdrop-filter', e.style.backdropFilter, 'important'); + e.style.setProperty('background', e.style.background, 'important'); + e.style.setProperty('border', e.style.border, 'important'); + } + }, + React.createElement(ReactSpring.animated.div, { + className: XenoLib.joinClassNames('xenoLib-notification-loadbar', { 'xenoLib-notification-loadbar-striped': !this.props.timeout && this.state.loading, 'xenoLib-notification-loadbar-user': !this.props.timeout && !this.state.loading }), + style: { right: e.progress.to(e => 100 - e + '%'), filter: e.loadbrightness.to(e => `brightness(${e * 100}%)`) } + }), + React.createElement( + XenoLib.ReactComponents.Button, + { + look: XenoLib.ReactComponents.Button.Looks.BLANK, + size: XenoLib.ReactComponents.Button.Sizes.NONE, + onClick: e => { + e.preventDefault(); + e.stopPropagation(); + this.closeNow(); + }, + onContextMenu: e => { + e.preventDefault(); + e.stopPropagation(); + const state = api.getState(); + state.data.forEach(notif => utils.remove(notif.id)); + }, + className: 'xenoLib-notification-close' + }, + CloseButton + ), + this.state.counter > 1 && BadgesModule.NumberBadge({ count: this.state.counter, className: 'xenLib-notification-counter', color: '#2196f3' }), + this.state.contentParsed + ) + ) + ); + } + ); + } + } + function NotificationsWrapper(e) { + const notifications = useStore(e => { + return e.data; + }); + return notifications.map(item => React.createElement(XenoLib.ReactComponents.ErrorBoundary, { label: `Notification ${item.id}`, onError: () => api.setState(state => ({ data: state.data.filter(n => n.id !== item.id) })), key: item.id.toString() }, React.createElement(Notification, { ...item, leaving: false }))).reverse(); + } + NotificationsWrapper.displayName = 'XenoLibNotifications'; + const DOMElement = document.createElement('div'); + DOMElement.className = XenoLib.joinClassNames('xenoLib-notifications', `xenoLib-centering-${LibrarySettings.notifications.position}`); + ReactDOM.render(React.createElement(NotificationsWrapper, {}), DOMElement); + document.querySelector('#app-mount').appendChild(DOMElement); + } + } catch (e) { + Logger.stacktrace('There has been an error loading the Notifications system, fallback object has been put in place to prevent errors', e); + XenoLib.Notifications = { + success(content, options = {}) { }, + info(content, options = {}) { }, + warning(content, options = {}) { }, + danger(content, options = {}) { }, + error(content, options = {}) { }, + show(content, options = {}) { }, + remove(id) { }, + update(id, options) { } + }; + } + /* NOTIFICATIONS END */ + + global.XenoLib = XenoLib; + + const notifLocations = ['topLeft', 'topMiddle', 'topRight', 'bottomLeft', 'bottomMiddle', 'bottomRight']; + const notifLocationClasses = [`${XenoLib.getClass('selected topLeft')} ${XenoLib.getClass('topLeft option')}`, `topMiddle-xenoLib ${XenoLib.getClass('topLeft option')}`, `${XenoLib.getClass('selected topRight')} ${XenoLib.getClass('topLeft option')}`, `${XenoLib.getClass('selected bottomLeft')} ${XenoLib.getClass('topLeft option')}`, `bottomMiddle-xenoLib ${XenoLib.getClass('topLeft option')}`, `${XenoLib.getClass('selected bottomRight')} ${XenoLib.getClass('topLeft option')}`]; + const PositionSelectorWrapperClassname = XenoLib.getClass('topLeft wrapper'); + const PositionSelectorSelectedClassname = XenoLib.getClass('topLeft selected'); + const PositionSelectorHiddenInputClassname = XenoLib.getClass('topLeft hiddenInput'); + const FormText = WebpackModules.getByDisplayName('FormText'); + class NotificationPosition extends React.PureComponent { + constructor(props) { + super(props); + this.state = { + position: props.position + }; + } + componentDidMount() { + this._notificationId = XenoLib.Notifications.show('Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur lacinia justo eget libero ultrices mollis.', { timeout: 0 }); + } + componentWillUnmount() { + XenoLib.Notifications.remove(this._notificationId); + } + getSelected() { + switch (this.state.position) { + case 'topLeft': + return 'Top Left'; + case 'topMiddle': + return 'Top Middle'; + case 'topRight': + return 'Top Right'; + case 'bottomLeft': + return 'Bottom Left'; + case 'bottomMiddle': + return 'Bottom Middle'; + case 'bottomRight': + return 'Bottom Right'; + default: + return 'Unknown'; + } + } + render() { + return React.createElement( + 'div', + {}, + React.createElement( + 'div', + { + className: PositionSelectorWrapperClassname + }, + notifLocations.map((e, i) => { + return React.createElement( + 'label', + { + className: XenoLib.joinClassNames(notifLocationClasses[i], { [PositionSelectorSelectedClassname]: this.state.position === e }) + }, + React.createElement('input', { + type: 'radio', + name: 'xenolib-notif-position-selector', + value: e, + onChange: () => { + this.props.onChange(e); + this.setState({ position: e }); + }, + className: PositionSelectorHiddenInputClassname + }) + ); + }) + ), + React.createElement( + FormText, + { + type: FormText.Types.DESCRIPTION, + className: DiscordClasses.Margins.marginTop8 + }, + this.getSelected() + ) + ); + } + } + + class NotificationPositionField extends Settings.SettingField { + constructor(name, note, onChange, value) { + super(name, note, onChange, NotificationPosition, { + position: value, + onChange: reactElement => position => { + this.onChange(position); + } + }); + } + } + + const _radioGroup = WebpackModules.getByDisplayName('RadioGroup'); + class RadioGroupWrapper extends React.PureComponent { + render() { + return React.createElement(_radioGroup, this.props); + } + } + + class RadioGroup extends Settings.SettingField { + constructor(name, note, defaultValue, values, onChange, options = {}) { + super(name, note, onChange, RadioGroupWrapper, { + noteOnTop: true, + disabled: !!options.disabled, + options: values, + onChange: reactElement => option => { + reactElement.props.value = option.value; + reactElement.forceUpdate(); + this.onChange(option.value); + }, + value: defaultValue + }); + } + } + + const _switchItem = WebpackModules.getByDisplayName('SwitchItem'); + class SwitchItemWrapper extends React.PureComponent { + render() { + return React.createElement(_switchItem, this.props); + } + } + + class Switch extends Settings.SettingField { + constructor(name, note, isChecked, onChange, options = {}) { + super(name, note, onChange); + this.disabled = !!options.disabled; + this.value = !!isChecked; + } + + onAdded() { + const reactElement = ReactDOM.render(React.createElement(SwitchItemWrapper, { + children: this.name, + note: this.note, + disabled: this.disabled, + hideBorder: false, + value: this.value, + onChange: (e) => { + reactElement.props.value = e; + reactElement.forceUpdate(); + this.onChange(e); + } + }), this.getElement()); + } + } + + XenoLib.buildSetting = function buildSetting(data) { + const { name, note, type, value, onChange, id } = data; + let setting = null; + if (type == "color") setting = new XenoLib.Settings.ColorPicker(name, note, value, onChange, { disabled: data.disabled, defaultColor: value }); + else if (type == "dropdown") setting = new Settings.Dropdown(name, note, value, data.options, onChange); + else if (type == "file") setting = new Settings.FilePicker(name, note, onChange); + else if (type == "keybind") setting = new Settings.Keybind(name, note, value, onChange); + else if (type == "radio") setting = new RadioGroup(name, note, value, data.options, onChange, { disabled: data.disabled }); + else if (type == "slider") setting = new Settings.Slider(name, note, data.min, data.max, value, onChange, data); + else if (type == "switch") setting = new Switch(name, note, value, onChange, { disabled: data.disabled }); + else if (type == "textbox") setting = new Settings.Textbox(name, note, value, onChange, { placeholder: data.placeholder || "" }); + if (id) setting.id = id; + return setting; + } + + return class CXenoLib extends Plugin { + constructor() { + super(); + this.settings = LibrarySettings; + XenoLib.changeName(__filename, '1XenoLib'); /* prevent user from changing libs filename */ + try { + WebpackModules.getByProps('openModal', 'hasModalOpen').closeModal(`${this.name}_DEP_MODAL`); + } catch (e) { } + } + load() { + super.load(); + if (!BdApi.Plugins) return; /* well shit what now */ + if (!BdApi.isSettingEnabled || !BdApi.disableSetting) return; + const prev = BdApi.isSettingEnabled('fork-ps-2'); + if (prev) BdApi.disableSetting('fork-ps-2'); + const list = BdApi.Plugins.getAll().filter(k => k._XL_PLUGIN); + for (let p = 0; p < list.length; p++) { + try { + BdApi.Plugins.reload(list[p].getName()); + } catch (e) { + try { + Logger.stacktrace(`Failed to reload plugin ${list[p].getName()}`, e); + } catch (e) { + Logger.error(`Failed telling you about failing to reload a plugin`, list[p], e); + } + } + } + if (prev) BdApi.enableSetting('fork-ps-2'); + } + buildSetting(data) { + if (data.type === 'position') { + const setting = new NotificationPositionField(data.name, data.note, data.onChange, data.value); + if (data.id) setting.id = data.id; + return setting; + } else if (data.type === 'color') { + const setting = new XenoLib.Settings.ColorPicker(data.name, data.note, data.value, data.onChange, data.options); + if (data.id) setting.id = data.id; + return setting; + } + return XenoLib.buildSetting(data); + } + getSettingsPanel() { + return this.buildSettingsPanel() + .append(new XenoLib.Settings.PluginFooter(() => this.showChangelog())) + .getElement(); + } + saveSettings(category, setting, value) { + this.settings[category][setting] = value; + LibrarySettings[category][setting] = value; + PluginUtilities.saveSettings(this.name, LibrarySettings); + if (category === 'notifications') { + if (setting === 'position') { + const DOMElement = document.querySelector('.xenoLib-notifications'); + if (DOMElement) { + DOMElement.className = XenoLib.joinClassNames('xenoLib-notifications', `xenoLib-centering-${LibrarySettings.notifications.position}`); + Dispatcher.dirtyDispatch({ type: 'XL_NOTIFS_ANIMATED' }); + } + } else if (setting === 'backdrop' || setting === 'backdropColor') { + Dispatcher.wait(() => Dispatcher.dispatch({ type: 'XL_NOTIFS_SETTINGS_UPDATE', key: UPDATEKEY }), (UPDATEKEY = {})); + } + } else if (category === 'addons') { + if (setting === 'extra') { + if (value && !patchAddonCardAnyway.patched) patchAddonCardAnyway(true); + XenoLib.Notifications.warning('Reopen plugins section for immediate effect'); + } + } + } + showChangelog(footer) { + return; + XenoLib.showChangelog(`${this.name} has been updated!`, this.version, this._config.changelog); + } + get name() { + return config.info.name; + } + get short() { + let string = ''; + + for (let i = 0, len = config.info.name.length; i < len; i++) { + const char = config.info.name[i]; + if (char === char.toUpperCase()) string += char; + } + + return string; + } + get author() { + return config.info.authors.map(author => author.name).join(', '); + } + get version() { + return config.info.version; + } + get description() { + return config.info.description; + } + }; + }; + + /* Finalize */ + + let ZeresPluginLibraryOutdated = false; + try { + const a = (c, a) => ((c = c.split('.').map(b => parseInt(b))), (a = a.split('.').map(b => parseInt(b))), !!(a[0] > c[0])) || !!(a[0] == c[0] && a[1] > c[1]) || !!(a[0] == c[0] && a[1] == c[1] && a[2] > c[2]), + b = BdApi.Plugins.get('ZeresPluginLibrary'); + ((b, c) => b && b._config && b._config.info && b._config.info.version && a(b._config.info.version, c))(b, '1.2.28') && (ZeresPluginLibraryOutdated = !0); + } catch (e) { + console.error('Error checking if ZeresPluginLibrary is out of date', e); + } + + return !global.ZeresPluginLibrary || ZeresPluginLibraryOutdated + ? class { + constructor() { + this._config = config; + this.start = this.load = this.handleMissingLib; + } + getName() { + return this.name.replace(/\s+/g, ''); + } + getAuthor() { + return this.author; + } + getVersion() { + return this.version; + } + getDescription() { + return this.description + ' You are missing ZeresPluginLibrary for this plugin, please enable the plugin and click Download Now.'; + } + start() { } + stop() { } + handleMissingLib() { + const a = BdApi.findModuleByProps('openModal', 'hasModalOpen'); + if (a && a.hasModalOpen(`${this.name}_DEP_MODAL`)) return; + const b = !global.ZeresPluginLibrary, + c = ZeresPluginLibraryOutdated ? 'Outdated Library' : 'Missing Library', + d = `The Library ZeresPluginLibrary required for ${this.name} is ${ZeresPluginLibraryOutdated ? 'outdated' : 'missing'}.`, + e = BdApi.findModuleByDisplayName('Text'), + f = BdApi.findModuleByDisplayName('ConfirmModal'), + g = () => BdApi.alert(c, BdApi.React.createElement('span', {}, BdApi.React.createElement('div', {}, d), `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.`, b || ZeresPluginLibraryOutdated ? BdApi.React.createElement('div', {}, BdApi.React.createElement('a', { href: 'https://betterdiscord.net/ghdl?id=2252', target: '_blank' }, 'Click here to download ZeresPluginLibrary')) : null)); + if (!a || !f || !e) return console.error(`Missing components:${(a ? '' : ' ModalStack') + (f ? '' : ' ConfirmationModalComponent') + (e ? '' : 'TextElement')}`), g(); + class h extends BdApi.React.PureComponent { + constructor(a) { + super(a), (this.state = { hasError: !1 }); + } + componentDidCatch(a) { + console.error(`Error in ${this.props.label}, screenshot or copy paste the error above to Lighty for help.`), this.setState({ hasError: !0 }), 'function' == typeof this.props.onError && this.props.onError(a); + } + render() { + return this.state.hasError ? null : this.props.children; + } + } + let i = !1, + j = !1; + const k = a.openModal( + b => { + if (j) return null; + try { + return BdApi.React.createElement( + h, + { + label: 'missing dependency modal', + onError: () => { + a.closeModal(k), g(); + } + }, + BdApi.React.createElement( + f, + Object.assign( + { + header: c, + children: BdApi.React.createElement(e, { size: e.Sizes.SIZE_16, children: [`${d} Please click Download Now to download it.`] }), + red: !1, + confirmText: 'Download Now', + cancelText: 'Cancel', + onCancel: b.onClose, + onConfirm: () => { + if (i) return; + i = !0; + const b = require('request'), + c = require('fs'), + d = require('path'); + b('https://raw.githubusercontent.com/rauenzi/BDPluginLibrary/master/release/0PluginLibrary.plugin.js', (b, e, f) => { + try { + if (b || 200 !== e.statusCode) return a.closeModal(k), g(); + c.writeFile(d.join(BdApi.Plugins && BdApi.Plugins.folder ? BdApi.Plugins.folder : window.ContentManager.pluginsFolder, '0PluginLibrary.plugin.js'), f, () => { }); + } catch (b) { + console.error('Fatal error downloading ZeresPluginLibrary', b), a.closeModal(k), g(); + } + }); + } + }, + b, + { onClose: () => { } } + ) + ) + ); + } catch (b) { + return console.error('There has been an error constructing the modal', b), (j = !0), a.closeModal(k), g(), null; + } + }, + { modalKey: `${this.name}_DEP_MODAL` } + ); + } + get name() { + return config.info.name; + } + get short() { + let string = ''; + for (let i = 0, len = config.info.name.length; i < len; i++) { + const char = config.info.name[i]; + if (char === char.toUpperCase()) string += char; + } + return string; + } + get author() { + return config.info.authors.map(author => author.name).join(', '); + } + get version() { + return config.info.version; + } + get description() { + return config.info.description; + } + } + : buildPlugin(global.ZeresPluginLibrary.buildPlugin(config)); +})(); + +/*@end@*/ diff --git a/Plugins/BetterImageViewer.plugin.js b/Plugins/BetterImageViewer.plugin.js new file mode 100644 index 0000000..8934d3f --- /dev/null +++ b/Plugins/BetterImageViewer.plugin.js @@ -0,0 +1,2207 @@ +//META{"name":"BetterImageViewer","source":"https://github.com/1Lighty/BetterDiscordPlugins/blob/master/Plugins/BetterImageViewer/BetterImageViewer.plugin.js","website":"https://1lighty.github.io/BetterDiscordStuff/?plugin=BetterImageViewer","authorId":"239513071272329217","invite":"NYvWdN5","donate":"https://paypal.me/lighty13"}*// +/*@cc_on +@if (@_jscript) + + // Offer to self-install for clueless users that try to run this directly. + var shell = WScript.CreateObject('WScript.Shell'); + var fs = new ActiveXObject('Scripting.FileSystemObject'); + var pathPlugins = shell.ExpandEnvironmentStrings('%APPDATA%\\BetterDiscord\\plugins'); + var pathSelf = WScript.ScriptFullName; + // Put the user at ease by addressing them in the first person + shell.Popup('It looks like you\'ve mistakenly tried to run me directly. \n(Don\'t do that!)', 0, 'I\'m a plugin for BetterDiscord', 0x30); + if (fs.GetParentFolderName(pathSelf) === fs.GetAbsolutePathName(pathPlugins)) { + shell.Popup('I\'m in the correct folder already.\nJust go to settings, plugins and enable me.', 0, 'I\'m already installed', 0x40); + } else if (!fs.FolderExists(pathPlugins)) { + shell.Popup('I can\'t find the BetterDiscord plugins folder.\nAre you sure it\'s even installed?', 0, 'Can\'t install myself', 0x10); + } else if (shell.Popup('Should I copy myself to BetterDiscord\'s plugins folder for you?', 0, 'Do you need some help?', 0x34) === 6) { + fs.CopyFile(pathSelf, fs.BuildPath(pathPlugins, fs.GetFileName(pathSelf)), true); + // Show the user where to put plugins in the future + shell.Exec('explorer ' + pathPlugins); + shell.Popup('I\'m installed!\nJust go to settings, plugins and enable me!', 0, 'Successfully installed', 0x40); + } + WScript.Quit(); + +@else@*/ + +module.exports = (() => { + /* Setup */ + const config = { + main: 'index.js', + info: { + name: 'BetterImageViewer', + authors: [ + { + name: 'Lighty', + discord_id: '239513071272329217', + github_username: '1Lighty', + twitter_username: '' + } + ], + version: '1.5.6', + description: 'Move between images in the entire channel with arrow keys, image zoom enabled by clicking and holding, scroll wheel to zoom in and out, hold shift to change lens size. Image previews will look sharper no matter what scaling you have, and will take up as much space as possible.', + github: 'https://github.com/1Lighty', + github_raw: 'https://raw.githubusercontent.com/1Lighty/BetterDiscordPlugins/master/Plugins/BetterImageViewer/BetterImageViewer.plugin.js' + }, + changelog: [ + { + title: 'Fixed', + type: 'fixed', + items: ['Fixed not being able to search, open image and then navigate thru images that match that search.'] + } + ], + defaultConfig: [ + { + type: 'category', + id: 'ui', + name: 'UI settings', + collapsible: true, + shown: true, + settings: [ + { + name: 'Show image index and number of images left (estimated)', + id: 'imageIndex', + type: 'switch', + value: true + }, + { + name: 'Show navigation buttons', + id: 'navButtons', + type: 'switch', + value: true + }, + { + name: 'Show image filename', + id: 'infoFilename', + type: 'switch', + value: true + }, + { + name: 'Show image resolution', + note: 'Left is downscaled, right is original', + id: 'infoResolution', + type: 'switch', + value: true + }, + { + name: 'Show image size', + note: "Left is downscaled, right is original, ~ means they're the same", + id: 'infoSize', + type: 'switch', + value: true + }, + { + name: 'Always load full resolution image', + note: "You won't notice a difference. You can also force it to load the full resolution image by ctrl + clicking the image preview. the first resolution on bottom right will turn red when it's enabled.", + id: 'loadFull', + type: 'switch', + value: false + } + ] + }, + { + type: 'category', + id: 'behavior', + name: 'Behavior settings', + collapsible: true, + shown: false, + settings: [ + { + name: 'Use search API', + note: "Without this, you'll only be able to view images currently cached in Discord.", + id: 'searchAPI', + type: 'switch', + value: true + }, + { + name: 'Trigger search when hovering over navigation buttons when needed', + id: 'hoverSearch', + type: 'switch', + value: false + }, + { + name: 'DEBUG', + note: 'do not touch', + id: 'debug', + type: 'switch', + value: false + } + ] + }, + { + type: 'category', + id: 'chat', + name: 'Chat image settings', + collapsible: true, + shown: false, + settings: [ + { + name: 'Scale images to be sharper', + id: 'scale', + type: 'switch', + value: true + }, + { + name: 'Resize images to take up more space', + id: 'resize', + type: 'switchbeta', + value: false + } + ] + }, + { + type: 'category', + id: 'zoom', + name: 'Image Zoom settings', + collapsible: true, + shown: false, + settings: [ + { + name: 'Enable image zoom', + id: 'enabled', + type: 'switch', + value: true + }, + { + name: 'Zoom enable mode', + id: 'enableMode', + type: 'radio', + value: 0, + options: [ + { name: 'Click and hold', value: 0 }, + { name: 'Click to toggle', value: 1 }, + { name: 'Scroll to toggle', value: 2 } + ] + }, + { + name: 'Anti-aliasing', + note: 'On low resolution images, pixels become blurry', + id: 'interp', + type: 'switch', + value: true + }, + { + name: 'Round lens', + note: 'why', + id: 'round', + type: 'switch', + value: true + }, + { + name: 'Allow lens to go out of bounds', + note: 'Allows the lens to go beyond the border of the image', + id: 'outOfBounds', + type: 'switch', + value: false + }, + { + name: 'Allow lens to clip out of view', + note: 'Allows the lens to go beyond the window', + id: 'outOfScreen', + type: 'switch', + value: true + }, + { + name: 'Movement smoothing', + note: 'Not recommended to disable. Smooths out movement and zoom', + id: 'smoothing', + type: 'switch', + value: true + } + ] + } + ] + }; + + /* Build */ + const buildPlugin = ([Plugin, Api]) => { + const { Utilities, WebpackModules, DiscordModules, ReactComponents, DiscordAPI, Logger, PluginUtilities, PluginUpdater, Structs } = Api; + const { React, ReactDOM, DiscordConstants, Dispatcher, GuildStore, GuildMemberStore, MessageStore, APIModule, NavigationUtils, SelectedChannelStore } = DiscordModules; + + const Patcher = XenoLib.createSmartPatcher(Api.Patcher); + + const ChannelStore = WebpackModules.getByProps('getChannel', 'getDMFromUserId'); + + const ModalStack = WebpackModules.getByProps('openModal', 'hasModalOpen'); + + let PluginBrokenFatal = false; + let NoImageZoom = false; + let overlayDOMNode; + + const { ARROW_LEFT, ARROW_RIGHT } = (() => { + try { + const keys = DiscordConstants.KeyboardKeys; + if (!keys) throw 'KeyboardKeys is undefined'; + return keys; + } catch (e) { + Logger.stacktrace('Failed to get KeyboardKeys', e); + return { ARROW_LEFT: 37, ARROW_RIGHT: 39 }; + } + })(); + const Clickable = WebpackModules.getByDisplayName('Clickable'); + const TextElement = WebpackModules.getByDisplayName('Text'); + + const _ImageUtils = WebpackModules.getByProps('getImageSrc'); + const ImageUtils = Object.assign({}, WebpackModules.getByProps('getImageSrc') || {}, WebpackModules.getByProps('getRatio') || {}, { + zoomFit: (e, t) => ImageUtils.fit(e, t, Math.ceil(Math.round(0.86 * window.innerWidth * devicePixelRatio)), Math.ceil(Math.round(0.8 * window.innerHeight * devicePixelRatio))), + getImageSrc: (e, t, n, r = 1, a = null) => { + var o = t; + var i = n; + if (r < 1) { + o = Math.round(t * r); + i = Math.round(n * r); + } + return ImageUtils.getSrcWithWidthAndHeight(e, t, n, o, i, a); + }, + getSizedImageSrc: (e, t, n, r) => _ImageUtils.getSizedImageSrc(e, t, n, r) + }); + + const TrustedStore = WebpackModules.getByProps('isTrustedDomain'); + + const ChannelMessages = (() => { + try { + const _channelMessages = WebpackModules.getByProps('_channelMessages')._channelMessages; + if (!_channelMessages) throw '_channelMessages is undefined'; + return _channelMessages; + } catch (e) { + Logger.stacktrace('Failed to get _channelMessages! Plugin will not work!', e); + PluginBrokenFatal = true; + return {}; + } + })(); + + const getRatio = (width, height, minW = 400, minH = 300) => ImageUtils.getRatio(width, height, minW, minH); + const getPlaceholder = (src, width, height, minW = 400, minH = 300) => ImageUtils.getImageSrc(src, width, height, getRatio(width, height, minW, minH), null); + + function extractImages(message) { + const images = []; + if (Array.isArray(message.attachments)) { + message.attachments.forEach(({ filename, width, height, url: original, proxy_url: src }) => { + if (!DiscordConstants.IMAGE_RE.test(filename) || (width <= 1 && height <= 1)) return; + const max = ImageUtils.zoomFit(width, height); + const placeholder = getPlaceholder(src, width, height, max.width, max.height); + images.push({ width, height, src, original, placeholder }); + }); + } + if (Array.isArray(message.embeds)) { + const appendImage = image => { + const { width, height, url: original, proxyURL: src } = image; + if (images.findIndex(e => e.src === src) !== -1) return; + if (!src || (width <= 1 && height <= 1)) return; + const max = ImageUtils.zoomFit(width, height); + const placeholder = getPlaceholder(src, width, height, max.width, max.height); + images.push({ width, height, original, src, placeholder }); + }; + message.embeds.forEach(({ image, images }) => { + if (!image && (!images || !images.length)) return; + if (images && images.length) for (const image of images) appendImage(image); + if (image) appendImage(image); + }); + } + return images; + } + const ReactSpring = WebpackModules.getByProps('useTransition'); + const zoomConfig = { mass: 1, tension: 1750, friction: 75, clamp: false }; + + class Image extends (() => { + const Image = WebpackModules.getByDisplayName('Image'); + if (Image) return Image; + Logger.error('Failed to get Image!'); + NoImageZoom = true; + return class error { }; + })() { + constructor(props) { + super(props); + this.state = { zooming: false, visible: false, panelWH: props.__BIV_hiddenSettings.panelWH, zoom: 1.5, loaded: false, failedLoad: false }; + XenoLib._.bindAll(this, ['handleMouseDown', 'handleMouseUp', 'handleMouseWheel', 'setRef']); + try { + this._handleMouseMove = this.handleMouseMove.bind(this); + this.handleMouseMove = e => this.state.zooming && this._handleMouseMove(e.clientX, e.clientY); + this._handleSaveLensWHChangeDC = new TimingModule.DelayedCall(1000, this._handleSaveLensWHChange.bind(this)); + this._controller = new ReactSpring.Controller({ panelX: 0, panelY: 0, panelWH: 0, offsetX: 0, offsetY: 0 }); + this._zoomController = new ReactSpring.Controller({ zoom: 1.5 }); + } catch (err) { + Logger.stacktrace(`Failed constructing Image`, err); + XenoLib.Notifications.error(`[**${config.info.name}**] Image zoom has encountered an error and has been temporarily disabled to prevent Discord from crashing. More info in console.`, { timeout: 0 }); + this.__BIV_crash = true; + } + } + componentDidMount() { + if (super.componentDidMount) super.componentDidMount(); + if (this.__BIV_crash) return; + window.addEventListener('mouseup', this.handleMouseUp); + window.addEventListener('mousemove', this.handleMouseMove); + window.addEventListener('mousewheel', this.handleMouseWheel); + this.getRawImage(); + } + componentWillUnmount() { + if (super.componentWillUnmount) super.componentWillUnmount(); + if (this.__BIV_crash) return; + window.removeEventListener('mouseup', this.handleMouseUp); + window.removeEventListener('mousemove', this.handleMouseMove); + window.removeEventListener('mousewheel', this.handleMouseWheel); + this._handleSaveLensWHChangeDC.cancel(); + this._handleSaveLensWHChange(); + this._controller.dispose(); + this._controller = null; + this._zoomController.dispose(); + this._zoomController = null; + } + componentDidUpdate(prevProps, prevState, snapshot) { + if (super.componentDidUpdate) super.componentDidUpdate(prevProps, prevState, snapshot); + if (this.__BIV_crash) return; + if (this.props.src !== prevProps.src) { + this.state.zoom = 1.5; + this.updateZoomController({ zoom: 1.5, immediate: !this.state.zooming }); + if (this.state.zooming) this.setState({ zooming: false }); + this.getRawImage(); + } else if (this.state.zooming !== prevState.zooming && !this.state.loaded && this.state.raw && !this.state.failedLoad && !this.props.__BIV_animated) { + /* only trigger if zoom state changed, raw image not loaded, raw link set, didn't fail and is not a GIFV */ + if (this.state.zooming) { + if (ImageUtils.isImageLoaded(this.state.raw)) this.setState({ loaded: true }); + else { + /* zoom started, try to load raw */ + this._loadCancel = ImageUtils.loadImage(this.state.raw, failed => { + this._loadCancel = null; + /* load failed, either the URL is invalid, host is dead or it's e621/e926, which is a special case + * do note, the special cases are not handled if SaveToRedux is not present */ + if ((this.state.failedLoad = failed)) return this.updateRawImage(true, true); + this.setState({ loaded: true }); + }); + } + } else { + if (typeof this._loadCancel === 'function') { + this._loadCancel(); + this._loadCancel = null; + } + } + } + if (!this.props.__BIV_settings.enabled) return; + if (!this._ref) return Logger.warn('this._ref is null!'); + this._bcr = this._ref.getBoundingClientRect(); + } + updateController(props, noStart = false) { + this._controller.update({ ...props, immediate: !this.props.__BIV_settings.smoothing || props.immediate, config: zoomConfig }); + if (!noStart) this._controller.start(); + } + updateZoomController(props) { + this._zoomController.update({ ...props, immediate: !this.props.__BIV_settings.smoothing || props.immediate, config: zoomConfig }).start(); + } + _handleSaveLensWHChange() { + Dispatcher.dirtyDispatch({ type: 'BIV_LENS_WH_CHANGE', value: this.state.panelWH }); + } + handleMouseDown(e) { + if (!this.props.__BIV_settings.enabled) return; + if (e.button !== DiscordConstants.MouseButtons.PRIMARY) return; + if (e.ctrlKey) { + Dispatcher.dirtyDispatch({ type: 'BIV_LOAD_FULLRES' }); + return; + } + if (this.state.zooming) return this.setState({ zooming: false }); + else if (this.props.__BIV_settings.enableMode === 2) return; /* scroll to toggle */ + this._handleMouseMove(e.clientX, e.clientY, true); + this.setState({ zooming: true, visible: true }); + e.preventDefault(); + } + handleMouseUp() { + /* click and hold mode */ + if (this.props.__BIV_settings.enableMode !== 0) return; + this.setState({ zooming: false }); + } + handleMouseMove(cx, cy, start) { + this._bcr = this._ref.getBoundingClientRect(); + if (!this.props.__BIV_settings.outOfBounds) { + cx = Math.min(this._bcr.left + this._bcr.width, Math.max(this._bcr.left, cx)); + cy = Math.min(this._bcr.top + this._bcr.height, Math.max(this._bcr.top, cy)); + } + let panelWH = this.state.panelWH; + if (!this.props.__BIV_settings.outOfScreen) { + if (Structs.Screen.height < Structs.Screen.width && panelWH > Structs.Screen.height) panelWH = Structs.Screen.height - 2; + else if (Structs.Screen.height > Structs.Screen.width && panelWH > Structs.Screen.width) panelWH = Structs.Screen.width - 2; + } + const offsetX = cx - this._bcr.left; + const offsetY = cy - this._bcr.top; + let panelX = cx - panelWH / 2; + let panelY = cy - panelWH / 2; + if (!this.props.__BIV_settings.outOfScreen) { + if (panelX < 0) panelX = 0; + else if (panelX + panelWH > Structs.Screen.width) panelX = Structs.Screen.width - (panelWH + 2); + if (panelY < 0) panelY = 0; + else if (panelY + panelWH > Structs.Screen.height) panelY = Structs.Screen.height - (panelWH + 2); + } + this.updateController({ panelX, panelY, offsetX, offsetY, panelWH, immediate: start }); + } + handleMouseWheel(e) { + if (!this.props.__BIV_settings.enabled) return; + /* scroll to toggle mode */ + const scrollToggle = this.props.__BIV_settings.enableMode === 2; + if ((!scrollToggle || (scrollToggle && e.shiftKey)) && !this.state.zooming) return; + if (e.deltaY < 0) { + if (e.shiftKey) { + this.state.panelWH *= 1.1; + if (Structs.Screen.height > Structs.Screen.width && this.state.panelWH > (this.props.__BIV_settings.outOfScreen ? Structs.Screen.height * 2 : Structs.Screen.height)) this.state.panelWH = this.props.__BIV_settings.outOfScreen ? Structs.Screen.height * 2 : Structs.Screen.height - 2; + else if (Structs.Screen.height < Structs.Screen.width && this.state.panelWH > (this.props.__BIV_settings.outOfScreen ? Structs.Screen.width * 2 : Structs.Screen.width)) this.state.panelWH = this.props.__BIV_settings.outOfScreen ? Structs.Screen.width * 2 : Structs.Screen.width - 2; + this.state.panelWH = Math.ceil(this.state.panelWH); + this._handleMouseMove(e.clientX, e.clientY); + this._handleSaveLensWHChangeDC.delay(); + } else { + this.state.zoom = Math.min(this.state.zoom * 1.1, 60); + this.updateZoomController({ zoom: this.state.zoom }); + if (scrollToggle && !this.state.zooming) { + this._handleMouseMove(e.clientX, e.clientY, true); + this.setState({ zooming: true, visible: true }); + } + } + } else if (e.deltaY > 0) { + if (e.shiftKey) { + this.state.panelWH *= 0.9; + if (this.state.panelWH < 75) this.state.panelWH = 75; + this.state.panelWH = Math.ceil(this.state.panelWH); + this._handleMouseMove(e.clientX, e.clientY); + this._handleSaveLensWHChangeDC.delay(); + } else { + const nextZoom = this.state.zoom * 0.9; + this.state.zoom = Math.max(nextZoom, 1); + this.updateZoomController({ zoom: this.state.zoom }); + if (scrollToggle && nextZoom < 1) this.setState({ zooming: false }); + } + } + } + getRawImage(failed) { + if (this.props.__BIV_animated) return; + if (typeof this.__BIV_failNum !== 'number') this.__BIV_failNum = 0; + if (failed) this.__BIV_failNum++; + else this.__BIV_failNum = 0; + const src = this.props.src; + const fullSource = (() => { + const split = src.split('?')[0]; + /* a user experienced some issues due to EXIF data */ + const isJpeg = split.indexOf('//media.discordapp.net/attachments/') !== -1 && split.search(/.jpe?g$/i) !== -1; + const SaveToRedux = BdApi.Plugins.get('SaveToRedux'); + const needsSize = src.substr(src.indexOf('?')).indexOf('size=') !== -1; + try { + if (SaveToRedux && !PluginUpdater.defaultComparator(SaveToRedux.version, '2.0.12')) return SaveToRedux.formatURL((!isJpeg && this.props.__BIV_original) || '', needsSize, '', '', split, this.__BIV_failNum).url; + } catch (_) { } + return split + (needsSize ? '?size=2048' : ''); + })(); + this.state.failedLoad = false; + this.state.loaded = ImageUtils.isImageLoaded(fullSource); + this.state.raw = fullSource; + } + renderLens(ea, props) { + return React.createElement( + ReactSpring.animated.div, + { + style: { + width: props.panelWH, + height: props.panelWH, + transform: ReactSpring.to([props.panelX, props.panelY], (x, y) => `translate3d(${x}px, ${y}px, 0)`), + opacity: ea.opacity + }, + className: XenoLib.joinClassNames('BIV-zoom-lens', { 'BIV-zoom-lens-round': this.props.__BIV_settings.round }) + }, + React.createElement( + ReactSpring.animated.div, + { + style: { + transform: ReactSpring.to([props.imgContainerLeft, props.imgContainerTop], (x, y) => `translate3d(${-x}px, ${-y}px, 0)`) + } + }, + React.createElement(this.props.__BIV_animated ? ReactSpring.animated.video : ReactSpring.animated.img, { + onError: _ => this.getRawImage(true), + src: this.props.__BIV_animated ? this.props.__BIV_src : this.state.loaded ? this.state.raw : this.props.src, + style: { + transform: props.img.to(({ x, y }) => `translate3d(${x}px, ${y}px, 0)`), + width: props.img.to(({ w }) => w).to(e => e), + height: props.img.to(({ h }) => h).to(e => e) /* even when you animate everything at the same time */, + ...(this.props.__BIV_settings.interp ? {} : { imageRendering: 'pixelated' }) + }, + ...(this.props.__BIV_animated ? { autoPlay: true, muted: true, loop: true } : {}) + }) + ) + ); + } + setRef(e) { + this._ref = e; + } + render() { + const ret = super.render(); + if (this.__BIV_crash) return ret; + for (const prop in ret.props) if (!prop.indexOf('__BIV')) delete ret.props[prop]; + if (!this.props.__BIV_settings.enabled) return ret; + ret.props.onMouseDown = this.handleMouseDown; + ret.ref = this.setRef; + if (this.state.visible) { + ret.props.children.push( + React.createElement( + XenoLib.ReactComponents.ErrorBoundary, + { + label: 'Image zoom', + onError: () => { + XenoLib.Notifications.error(`[**${config.info.name}**] Image zoom has encountered a rendering error and has been temporarily disabled to prevent Discord from crashing. More info in console.`, { timeout: 0 }); + } + }, + ReactDOM.createPortal( + React.createElement( + ReactSpring.Spring, + { + native: true, + from: { opacity: 0 }, + to: { opacity: this.state.zooming ? 1 : 0 }, + config: { duration: 100 }, + onRest: () => { + if (!this.state.zooming) this.setState({ visible: false }); + } + }, + ea => [ + React.createElement(ReactSpring.animated.div, { + style: { + opacity: ea.opacity + }, + className: 'BIV-zoom-backdrop', + onMouseDown: this.handleMouseDown + }), + this.renderLens(ea, { + imgContainerLeft: this._controller.springs.panelX, + imgContainerTop: this._controller.springs.panelY, + img: ReactSpring.to([this._zoomController.springs.zoom, this._controller.springs.offsetX, this._controller.springs.offsetY], (z, x, y) => { + return { + x: this._bcr.left - ((this._bcr.width * z - this._bcr.width) / (this._bcr.width * z)) * x * z, + y: this._bcr.top - ((this._bcr.height * z - this._bcr.height) / (this._bcr.height * z)) * y * z, + w: z * this.props.width, + h: z * this.props.height + }; + }), + panelX: this._controller.springs.panelX, + panelY: this._controller.springs.panelY, + panelWH: this._controller.springs.panelWH + }) + ] + ), + overlayDOMNode + ) + ) + ); + } + return ret; + } + } + + class LazyImage extends (() => { + const LazyImage = WebpackModules.getByDisplayName('LazyImage'); + if (LazyImage) return LazyImage; + Logger.error('Failed to get LazyImage! Plugin will not work!'); + PluginBrokenFatal = true; + return class error { }; + })() { + constructor(props) { + super(props); + this.renderChildren = this.renderChildren.bind(this); + } + componentDidUpdate(props, prevState, snapshot) { + this._cancellers.forEach(e => e()); + this._cancellers.clear(); + super.componentDidUpdate(props, prevState, snapshot); + if (this.__BIV_updating) return; + this.__BIV_updating = true; + const max = ImageUtils.zoomFit(this.props.width, this.props.height); + const src = getPlaceholder(this.props.src, this.props.width, this.props.height, max.width, max.height); + const isLoaded = ImageUtils.isImageLoaded(src); + if (!isLoaded) { + if (this.state.readyState !== 'LOADING') this.setState({ readyState: 'LOADING' }); + this.loadImage(this.getSrc(this.getRatio(), false), this.handleImageLoad); + } else if (this.state.readyState !== 'READY') { + this.setState({ readyState: 'READY' }); + } + this.__BIV_updating = false; + } + renderChildren(e) { + return React.createElement('img', { + className: e.className || undefined, + alt: e.alt, + src: e.src, + style: e.size, + key: this.props.id /* force React to create a new element for a smooth transition */ + }); + } + render() { + const ret = super.render(); + if (!ret) { + Logger.warn('LazyImage render returned null!', new Error()); /* should not occur */ + return ret; + } + ret.props.__BIV_original = this.props.__BIV_original; + ret.props.children = this.renderChildren; + return ret; + } + } + const MessageRecordUtils = WebpackModules.getByProps('createMessageRecord'); + const Tooltip = WebpackModules.getByDisplayName('Tooltip'); + + const SearchCache = {}; + const OldSearchCache = {}; + const ForwardSearchCache = {}; + const OldForwardSearchCache = {}; + + function stripDeletedMessage(channelId, messageId) { + for (const cache of [SearchCache, OldSearchCache, ForwardSearchCache, OldForwardSearchCache]) { + const chc = cache[channelId]; + if (!Array.isArray(chc)) continue; + const idx = chc.findIndex(e => e.id === messageId); + if (idx === -1) continue; + chc.splice(idx, 1); + } + } + function stripPurgedMessages(channelId, messageIds) { + for (const cache of [SearchCache, OldSearchCache, ForwardSearchCache, OldForwardSearchCache]) { + const chc = cache[channelId]; + if (!Array.isArray(chc)) continue; + for (const messageId of messageIds) { + const idx = chc.findIndex(e => e.id === messageId); + if (idx === -1) continue; + chc.splice(idx, 1); + } + } + } + class ErrorCatcher extends React.PureComponent { + constructor(props) { + super(props); + this.state = { errorLevel: 0, errorTimeout: false }; + } + componentDidCatch(err, inf) { + Logger.err(`Error in ${this.props.label}, screenshot or copy paste the error above to Lighty for help.`); + this.setState({ errorTimeout: true }); + if (typeof this.props.onError === 'function') this.props.onError(err, this.state.errorLevel); + setImmediate(_ => this.setState({ errorLevel: this.state.errorLevel + 1 })); + } + render() { + if (this.state.errorTimeout) return null; + if (!this.state.errorLevel) return this.props.children; + if (Array.isArray(this.props.fallback) && this.props.fallback[this.state.errorLevel - 1]) return this.props.fallback[this.state.errorLevel - 1]; + return null; + } + } + + const MessageTimestamp = (() => { + try { + const MessageTimestamp = WebpackModules.getByProps('MessageTimestamp').MessageTimestamp; + if (!MessageTimestamp) throw 'MessageTimestamp is undefined'; + return MessageTimestamp; + } catch (e) { + Logger.stacktrace('Failed to get MessageTimestamp! Plugin will not work', e); + PluginBrokenFatal = true; + return () => { }; + } + })(); + + const TimingModule = WebpackModules.getByProps('DelayedCall'); + const APIEncodeModule = WebpackModules.getByProps('stringify', 'parse', 'encode'); + const ImageModal = WebpackModules.getByDisplayName('ImageModal'); + const ImageProps = ['height', 'width', 'original', 'placeholder', 'src']; + const UsernameClassname = XenoLib.getClass('botTag username'); + const ClickableClassname = XenoLib.getClass('username clickable'); + const CozyClassname = XenoLib.getClass('zalgo cozy'); + + /* discord gay */ + const LeftCaretIcon = e => React.createElement('svg', { ...e, name: 'LeftCaret', width: 24, height: 24, viewBox: '0 0 24 24' }, React.createElement('polygon', { points: '18.35 4.35 16 2 6 12 16 22 18.35 19.65 10.717 12', fill: 'currentColor', fillRule: 'nonzero' })); + const RightCaretIcon = e => React.createElement('svg', { ...e, name: 'RightCaret', width: 24, height: 24, viewBox: '0 0 24 24' }, React.createElement('polygon', { points: '8.47 2 6.12 4.35 13.753 12 6.12 19.65 8.47 22 18.47 12', fill: 'currentColor', fillRule: 'nonzero' })); + const WarningTriangleIcon = e => React.createElement('svg', { ...e, name: 'WarningTriangle', width: 16, height: 16, viewBox: '0 0 24 24' }, React.createElement('path', { d: 'M1,21 L23,21 L12,2 L1,21 L1,21 Z M13,18 L11,18 L11,16 L13,16 L13,18 L13,18 Z M13,14 L11,14 L11,10 L13,10 L13,14 L13,14 Z', fill: 'currentColor' })); + const UpdateAvailableIcon = e => React.createElement('svg', { ...e, name: 'UpdateAvailable', width: 16, height: 16, viewBox: '0 0 24 24', className: 'BIV-searching-icon-spin' }, React.createElement('path', { d: 'M5,8 L9,12 L6,12 C6,15.31 8.69,18 12,18 C13.01,18 13.97,17.75 14.8,17.3 L16.26,18.76 C15.03,19.54 13.57,20 12,20 C7.58,20 4,16.42 4,12 L1,12 L5,8 Z M18,12 C18,8.69 15.31,6 12,6 C10.99,6 10.03,6.25 9.2,6.7 L7.74,5.24 C8.97,4.46 10.43,4 12,4 C16.42,4 20,7.58 20,12 L23,12 L19,16 L15,12 L18,12 Z', fill: 'currentColor', fillRule: 'nonzero' })); + const SearchIcon = e => React.createElement('svg', { ...e, name: 'Nova_Search', width: 24, height: 24, viewBox: '0 0 24 24' }, React.createElement('path', { d: 'M21.707 20.293L16.314 14.9C17.403 13.504 18 11.799 18 10C18 7.863 17.167 5.854 15.656 4.344C14.146 2.832 12.137 2 10 2C7.863 2 5.854 2.832 4.344 4.344C2.833 5.854 2 7.863 2 10C2 12.137 2.833 14.146 4.344 15.656C5.854 17.168 7.863 18 10 18C11.799 18 13.504 17.404 14.9 16.314L20.293 21.706L21.707 20.293ZM10 16C8.397 16 6.891 15.376 5.758 14.243C4.624 13.11 4 11.603 4 10C4 8.398 4.624 6.891 5.758 5.758C6.891 4.624 8.397 4 10 4C11.603 4 13.109 4.624 14.242 5.758C15.376 6.891 16 8.398 16 10C16 11.603 15.376 13.11 14.242 14.243C13.109 15.376 11.603 16 10 16Z', fill: 'currentColor' })); + const TimerIcon = e => React.createElement('svg', { ...e, name: 'Timer', width: 16, height: 16, viewBox: '0 0 24 24' }, React.createElement('path', { d: 'M15 1H9v2h6V1zm-4 13h2V8h-2v6zm8.03-6.61l1.42-1.42c-.43-.51-.9-.99-1.41-1.41l-1.42 1.42C16.07 4.74 14.12 4 12 4c-4.97 0-9 4.03-9 9s4.02 9 9 9 9-4.03 9-9c0-2.12-.74-4.07-1.97-5.61zM12 20c-3.87 0-7-3.13-7-7s3.13-7 7-7 7 3.13 7 7-3.13 7-7 7z', fill: 'currentColor', fillRule: 'nonzero' })); + const ClearIcon = e => React.createElement('svg', { ...e, name: 'Clear', width: 18, height: 18, viewBox: '0 0 18 18' }, React.createElement('path', { d: 'M9,2 C12.871,2 16,5.129 16,9 C16,12.871 12.871,16 9,16 C5.129,16 2,12.871 2,9 C2,5.129 5.129,2 9,2 L9,2 Z M11.6925,5.25 L9,7.9425 L6.3075,5.25 L5.25,6.3075 L7.9425,9 L5.25,11.6925 L6.3075,12.75 L9,10.0575 L11.6925,12.75 L12.75,11.6925 L10.0575,9 L12.75,6.3075 L11.6925,5.25 Z', fill: 'currentColor', fillRule: 'nonzero' })); + + const EditorTools = WebpackModules.getByProps('getFirstTextBlock'); + const SearchTools = WebpackModules.getByProps('tokenizeQuery', 'getSearchQueryFromTokens'); + // const SearchResultsWrap = XenoLib.getSingleClass('noResults searchResultsWrap') || 'ERRORCLASS'; + const SearchStore = WebpackModules.getByProps('getCurrentSearchId'); + + const currentChannel = _ => { + const channel = ChannelStore.getChannel(SelectedChannelStore.getChannelId()); + return channel ? Structs.Channel.from(channel) : null; + } + + class RichImageModal extends (() => { + if (ImageModal) return ImageModal; + Logger.error('ImageModal is undefined! Plugin will not work!'); + PluginBrokenFatal = true; + return class error { }; + })() { + constructor(props) { + super(props); + this.state = { + src: props.src, + original: props.original, + width: props.width, + height: props.height, + __BIV_data: props.__BIV_data, + __BIV_index: props.__BIV_index, + requesting: false, + indexing: false, + internalError: false, + rateLimited: false, + localRateLimited: 0, + needsSearch: false, + isNearingEdge: false, + controlsHovered: false, + unknownError: false, + controlsVisible: false, + imageSize: null, + originalImageSize: null, + basicImageInfo: null, + showFullRes: false + }; + XenoLib._.bindAll(this, ['handleMessageCreate', 'handleMessageDelete', 'handlePurge', 'handleKeyDown', 'handlePrevious', 'handleNext', 'handleMouseLeave']); + this.handleMouseEnterLeft = this._handleMouseEnter.bind(this, false); + this.handleMouseEnterRight = this._handleMouseEnter.bind(this, true); + this.handleFastJumpLeft = this._handleFastJump.bind(this, false); + this.handleFastJumpRight = this._handleFastJump.bind(this, true); + if (props.__BIV_index === -1) { + this.state.internalError = true; + return; + } + try { + if (!currentChannel()) { + this.state.internalError = true; + return; + } + const filtered = this.filterMessages(true); + if (!props.__BIV_isSearch && filtered.findIndex(m => m.id === this.state.__BIV_data.messageId) === -1) { + this.state.internalError = true; + return; + } + this._lastSearch = 0; + this._cancellers = new Set(); + this._cachedMessages = [props.__BIV_data.messageId]; + this._preloading = new Set(); + if (!props.__BIV_isSearch) { + if (SearchCache[currentChannel().id]) { + OldSearchCache[currentChannel().id] = [...SearchCache[currentChannel().id]]; + if (SearchCache[currentChannel().id].noBefore) OldSearchCache[currentChannel().id].noBefore = SearchCache[currentChannel().id].noBefore; + if (SearchCache[currentChannel().id]._totalResults) OldSearchCache[currentChannel().id]._totalResults = SearchCache[currentChannel().id]._totalResults; + } + const cache = SearchCache[currentChannel().id]; + if (cache && filtered[0]) { + const idx = cache.findIndex(e => e.id === filtered[0].id); + /* better cache utilization */ + if (idx !== -1) { + this._searchCache = cache.slice(0, idx + 1); + if (cache.noBefore) this._searchCache.noBefore = cache.noBefore; + if (cache._totalResults) this._searchCache._totalResults = cache._totalResults; + SearchCache[currentChannel().id] = this._searchCache; + } + } + if (!this._searchCache) this._searchCache = SearchCache[currentChannel().id] = []; + if (!this._searchCache._totalResults) this._searchCache._totalResults = 0; + if (!ChannelMessages[currentChannel().id].hasMoreBefore) this._searchCache.noBefore = true; + if (ForwardSearchCache[currentChannel().id]) OldForwardSearchCache[currentChannel().id] = [...ForwardSearchCache[currentChannel().id]]; + if (ChannelMessages[currentChannel().id].hasMoreAfter && !ChannelMessages[currentChannel().id]._after._wasAtEdge) { + filtered.reverse(); + const cache = ForwardSearchCache[currentChannel().id]; + if (cache && filtered[0]) { + const idx = cache.findIndex(e => e.id === filtered[0].id); + /* god I hope I did this right */ + if (idx !== -1) { + this._forwardSearchCache = cache.slice(idx); + ForwardSearchCache[currentChannel().id] = this._forwardSearchCache; + } + } + } + if (!this._forwardSearchCache) this._forwardSearchCache = ForwardSearchCache[currentChannel().id] = []; + this._followNew = ChannelMessages[currentChannel().id]._after._wasAtEdge; + this._searchId = DiscordAPI.currentGuild ? DiscordAPI.currentGuild.id : currentChannel().id; + } else { + this._followNew = false; + this._searchCache = []; + this._forwardSearchCache = []; + const images = []; + this._searchId = SearchStore.getCurrentSearchId(); + const searchResults = SearchStore.getResults(this._searchId); + this._includeNonHits = !searchResults.some(m => m.findIndex(e => e.id === props.__BIV_data.messageId && e.isSearchHit) !== -1); + searchResults.forEach(group => { + group.forEach(iMessage => { + if ((!this._includeNonHits && !iMessage.isSearchHit) || images.findIndex(e => e.id === iMessage.id) !== -1) return; + if (!extractImages(iMessage).length) return; + images.push(iMessage); + }); + }); + images.sort((a, b) => a.timestamp.unix() - b.timestamp.unix()); + /* discord search is le broken lol */ + /* if (Utilities.getNestedProp(ZeresPluginLibrary.ReactTools.getOwnerInstance(document.querySelector(`.${SearchResultsWrap}`)), 'state.searchMode') === DiscordConstants.SearchModes.OLDEST) images.reverse(); */ + this._searchCache._totalResults = SearchStore.getTotalResults(this._searchId); + this._searchCache.unshift(...images); + let searchString = EditorTools.getFirstTextBlock(SearchStore.getEditorState(this._searchId)); + let searchquery; + let o; + let s; + for (o = SearchTools.tokenizeQuery(searchString), searchquery = SearchTools.getSearchQueryFromTokens(o), s = 0; s < o.length; s++) { + SearchTools.filterHasAnswer(o[s], o[s + 1]) || (searchString = searchString.substring(0, o[s].start) + searchString.substring(o[s].end)); + } + this._searchProps = searchquery; + } + this._searchType = GuildStore.getGuild(this._searchId) ? DiscordConstants.SearchTypes.GUILD : DiscordConstants.SearchTypes.CHANNEL; + this._imageCounter = {}; + this._oFM = []; + this.calculateImageNumNMax(); + } catch (err) { + Logger.stacktrace('Failed constructing RichImageModal', err); + /* XenoLib.Notifications.error(`[**${config.info.name}**] Serious internal error. More info in console.`) */ + this.state.internalError = -1; + } + } + componentDidMount() { + if (super.componentDidMount) super.componentDidMount(); + if (this.state.internalError) return; + window.addEventListener('keydown', this.handleKeyDown); + Dispatcher.subscribe('MESSAGE_CREATE', this.handleMessageCreate); + Dispatcher.subscribe('MESSAGE_DELETE', this.handleMessageDelete); + Dispatcher.subscribe('MESSAGE_DELETE_BULK', this.handlePurge); + this.handlePreLoad(); + } + componentWillUnmount() { + if (super.componentWillUnmount) super.componentWillUnmount(); + if (this.state.internalError) return; + window.removeEventListener('keydown', this.handleKeyDown); + Dispatcher.unsubscribe('MESSAGE_CREATE', this.handleMessageCreate); + Dispatcher.unsubscribe('MESSAGE_DELETE', this.handleMessageDelete); + Dispatcher.unsubscribe('MESSAGE_DELETE_BULK', this.handlePurge); + this._cancellers.forEach(e => e()); + this._cancellers.clear(); + } + filterMessages(noCache) { + const chan = this.props.__BIV_isSearch ? [] : ChannelMessages[currentChannel().id]; + const arr = [...((!noCache && this._searchCache) || []), ...(!this.props.__BIV_isSearch ? [...chan._before._messages, ...chan._array, ...chan._after._messages] : []), ...((!noCache && this._forwardSearchCache) || [])]; + return arr.filter((m, i) => arr.findIndex(a => a.id === m.id) === i && extractImages(m).length).sort((a, b) => a.timestamp.unix() - b.timestamp.unix()); + } + getMessage(id) { + return MessageStore.getMessage(currentChannel().id, id) || this.filterMessages().find(m => m.id === id); + } + calculateImageNumNMax() { + const filtered = this.filterMessages(); + this._oFM = [...filtered]; + filtered.reverse(); + this._imageCounter = {}; + let imageCount = 1; + filtered.forEach(message => { + const images = extractImages(message); + this._imageCounter[message.id] = imageCount; + imageCount += images.length; + }); + this._maxImages = imageCount - 1; + } + processCache(cache, lastId, reverse) { + const OldChannelCache = cache[currentChannel().id]; + if (OldChannelCache && OldChannelCache.findIndex(m => m.id === lastId) !== -1) { + const idx = OldChannelCache.findIndex(m => m.id === lastId); + const images = reverse ? OldChannelCache.slice(idx) : OldChannelCache.slice(0, idx + 1); + if (images.length > 2) { + images.sort((a, b) => a.timestamp.unix() - b.timestamp.unix()); + if (reverse) { + this._forwardSearchCache.push(...images); + } else { + this._searchCache.unshift(...images); + if (OldChannelCache.noBefore) this._searchCache.noBefore = OldChannelCache.noBefore; + if (OldChannelCache._totalResults) this._searchCache._totalResults = OldChannelCache._totalResults; + } + this.calculateImageNumNMax(); + this.forceUpdate(); + return true; + } + } + } + handleSearch(lastId, reverse) { + if (!this.props.__BIV_settings.behavior.searchAPI) return; + if (!this.props.__BIV_isSearch && reverse && !ChannelMessages[currentChannel().id].hasMoreAfter) return Logger.warn("Illegal operation, attempted to reverse search, but we're on newest image\n", new Error().stack); + this.state.needsSearch = false; + if ((this.state.requesting && !this.state.indexing) || (!reverse && this._searchCache.noBefore) || (reverse && this._followNew)) return; + /* fully utilize both caches */ + if (!this.props.__BIV_isSearch && this.processCache(OldForwardSearchCache, lastId, reverse)) return; + if (!this.props.__BIV_isSearch && this.processCache(OldSearchCache, lastId, reverse)) return; + if (this.state.rateLimited) return; + if (!this.state.indexing && Date.now() - this._lastSearch < 1500) { + if (!this.state.localRateLimited) { + this.state.localRateLimited = this.setState({ + localRateLimited: setTimeout(() => { + this.state.localRateLimited = 0; + this.handleSearch(lastId, reverse); + }, 1500 - (Date.now() - this._lastSearch)) + }); + } + return; + } + this._lastSearch = Date.now(); + const query = Object.assign({}, this.props.__BIV_isSearch ? this._searchProps : { channel_id: currentChannel().id }, { has: 'image', include_nsfw: true, [reverse ? 'min_id' : 'max_id']: lastId }, reverse ? { sort_order: 'asc' } : {}); + APIModule.get({ + url: this._searchType === DiscordConstants.SearchTypes.GUILD ? DiscordConstants.Endpoints.SEARCH_GUILD(this._searchId) : DiscordConstants.Endpoints.SEARCH_CHANNEL(this._searchId), + query: APIEncodeModule.stringify(query) + }) + .then(content => { + if (content.status === 202) { + this.setState({ indexing: true }); + setTimeout(() => this.handleSearch(lastId, reverse), content.body.retry_after || 5e3); + return; + } else if (content.status === 429) { + this.setState({ rateLimited: content.body.retry_after || 5e3 }); + setTimeout(() => { + this.setState({ rateLimited: false }); + this.handleSearch(lastId, reverse); + }, content.body.retry_after || 5e3); + return; + } else if (content.status >= 400) { + throw `Status ${content.status}`; + } + if (content.body.total_results <= 25) { + if (reverse) this._followNew = true; + else this._searchCache.noBefore = true; + } + const filtered = this.filterMessages(); + const images = [reverse ? filtered[filtered.length - 1] : filtered[0]]; + content.body.messages.forEach(group => { + group.forEach(message => { + if ((this.props.__BIV_isSearch && !this._includeNonHits && !message.hit) || images.findIndex(e => e.id === message.id) !== -1) return; + const iMessage = MessageRecordUtils.createMessageRecord(message); + if (!extractImages(iMessage).length) return; + images.push(iMessage); + }); + }); + images.sort((a, b) => a.timestamp.unix() - b.timestamp.unix()); + if (reverse) { + this._forwardSearchCache.push(...images); + } else { + if (this._searchCache.noBefore) this._searchCache._totalResults = 0; + else this._searchCache._totalResults = content.body.total_results - 25; + this._searchCache.unshift(...images); + } + this.calculateImageNumNMax(); + this.setState({ requesting: false, indexing: false }); + }) + .catch(err => (Logger.stacktrace('There has been an issue searching', err), this.setState({ unknownError: true, indexing: false }), setTimeout(() => this.setState({ requesting: false }), 1000))); + this.setState({ requesting: true, unknownError: false }); + } + handleMessageCreate({ optimistic, channelId, message }) { + if (this.props.__BIV_isSearch) return; + if (optimistic || channelId !== currentChannel().id || !extractImages(message).length) return; + if (this._followNew) this._forwardSearchCache.push(MessageRecordUtils.createMessageRecord(message)); + this.calculateImageNumNMax(); + this.forceUpdate(); + } + handleMessageDeletes() { + /* While for people that have loggers, which preserve deleted and purged messages, + this is a non issue as the image stays. However for everyone else, if the image + we are currently displaying is deleted, we will get lost and be unable to tell what + image is before and after the current image. So force the user to go to a non + deleted image, if that fails, force close we don't want to cause bugs like that, + also it might trip internalError if we don't iirc which will hide everything the + plugin adds, that's a big nono. + */ + if (this.handleStart(true, true) && this.handleEnd(true, true)) { + this.props.onClose(); /* we are trapped on an image that does not exist, give up */ + return XenoLib.Notifications.warning('[**BetterImageViewer**] All visible images were deleted, forcefully closed to avoid issues.', { timeout: 0 }); /* tell the user about the sudden closure, ps toasts suck for this */ + } + this.calculateImageNumNMax(); + this.forceUpdate(); + } + handleMessageDelete(e) { + const { channelId, id: messageId } = e; + stripDeletedMessage(channelId, messageId); + if (messageId !== this.state.__BIV_data.messageId) return; + this.handleMessageDeletes(); + } + handlePurge(e) { + const { channelId, ids: messageIds } = e; + stripPurgedMessages(channelId, messageIds); + if (channelId !== currentChannel().id || messageIds.indexOf(this.state.__BIV_data.messageId) === -1) return; + for (const messageId of messageIds) { + if (messageId === this.state.__BIV_data.messageId) continue; + const idx = this._oFM.findIndex(e => e.id === messageId); + if (idx === -1) continue; + this._oFM.splice(idx, 1); + } + this.handleMessageDeletes(); + } + _handleMouseEnter(next) { + this.state.controlsHovered = true; + if (this.props.__BIV_settings.behavior.debug) this.forceUpdate(); + if (!this.state.needsSearch || (this.state.needsSearch === -1 && !next) || !this.props.__BIV_settings.behavior.hoverSearch) return; + const filtered = this.filterMessages(); + this.handleSearch(next ? filtered[filtered.length - 1].id : filtered[0].id, next); + } + handleMouseLeave() { + this.state.controlsHovered = false; + if (this.props.__BIV_settings.behavior.debug) this.forceUpdate(); + } + handlePreLoad(keyboardMode, next, subsidiaryMessageId) { + const filtered = this.filterMessages(); + const targetIdx = filtered.findIndex(m => m.id === (subsidiaryMessageId ? subsidiaryMessageId : this.state.__BIV_data.messageId)); + if (targetIdx === -1) Logger.warn('Unknown message\n', new Error()); + const isNearingEdge = next ? filtered.length - (targetIdx + 1) < 5 : targetIdx + 1 < 5; + this.setState({ isNearingEdge, showFullRes: false }); + if (keyboardMode === -1 || isNearingEdge) { + /* search required, wait for user input if none of these are tripped */ + if (keyboardMode || this.state.controlsHovered) { + if (!next || (next && (this.props.__BIV_isSearch || ChannelMessages[currentChannel().id].hasMoreAfter))) this.handleSearch(next ? filtered[filtered.length - 1].id : filtered[0].id, next); + } else { + this.state.needsSearch = next ? -1 : 1; + } + } + if (targetIdx === -1) { + XenoLib.Notifications.error(`[**${config.info.name}**] Anomaly detected, disabling controls.`, { timeout: 5000 }); + return this.setState({ internalError: true }); + } + const handleMessage = message => { + if (this._cachedMessages.indexOf(message.id) !== -1 || this._preloading.has(message.id)) return; + const data = extractImages(message); + data.forEach(image => { + const max = ImageUtils.zoomFit(image.width, image.height); + const src = getPlaceholder(image.src, image.width, image.height, max.width, max.height); + if (ImageUtils.isImageLoaded(src)) { + this._cachedMessages.push(message.id); + return; + } + const cancel = ImageUtils.loadImage(src, (e, r) => { + if (cancel) this._cancellers.delete(cancel); + this._cachedMessages.push(message.id); + this._preloading.delete(message.id); + }); + if (cancel) this._cancellers.add(cancel); + this._preloading.add(message.id); + }); + }; + filtered.slice(Math.max(targetIdx - 5, 0), Math.max(targetIdx, 0)).forEach(handleMessage); + filtered.slice(Math.min(targetIdx + 1, filtered.length), Math.min(targetIdx + 5, filtered.length)).forEach(handleMessage); + } + handleKeyDown(e) { + if (this.state.internalError) return; + switch (e.which) { + case ARROW_LEFT: + case ARROW_RIGHT: + e.preventDefault(); + this.state.controlsInactive = true; + this.handleChangeImage(e.which === ARROW_RIGHT, true); + } + } + handleEnd(keyboardMode, useInternal) { + const filtered = useInternal ? this._oFM : this.filterMessages(); + const previousMessage = filtered.find((e, i) => filtered[i - 1] && filtered[i - 1].id === this.state.__BIV_data.messageId); + if (!previousMessage) return true; + const newData = { + images: extractImages(previousMessage), + messageId: previousMessage.id + }; + this.setState({ + __BIV_data: newData, + __BIV_index: 0, + ...newData.images[0] + }); + this.requestImageInfo(newData.images[0]); + this.handlePreLoad(keyboardMode, true, previousMessage.id); + } + handleStart(keyboardMode, useInternal) { + const filtered = useInternal ? this._oFM : this.filterMessages(); + const previousMessage = filtered.find((e, i) => filtered[i + 1] && filtered[i + 1].id === this.state.__BIV_data.messageId); + if (!previousMessage) return true; + const newData = { + images: extractImages(previousMessage), + messageId: previousMessage.id + }; + this.setState({ + __BIV_data: newData, + __BIV_index: newData.images.length - 1, + ...newData.images[newData.images.length - 1] + }); + this.requestImageInfo(newData.images[newData.images.length - 1]); + this.handlePreLoad(keyboardMode, false, previousMessage.id); + } + handleChangeImage(next, keyboardMode) { + if (next) { + if (this.state.__BIV_index === this.state.__BIV_data.images.length - 1) { + if (!this.handleEnd(keyboardMode)) return; + } else this.state.__BIV_index++; + } else { + if (!this.state.__BIV_index) { + if (!this.handleStart(keyboardMode)) return; + } else this.state.__BIV_index--; + } + this.handlePreLoad(keyboardMode, next); + this.setState(this.state.__BIV_data.images[this.state.__BIV_index]); + this.requestImageInfo(this.state.__BIV_data.images[this.state.__BIV_index]); + } + handlePrevious() { + this.handleChangeImage(); + } + handleNext() { + this.handleChangeImage(true); + } + _handleFastJump(next) { + const filtered = this.filterMessages(); + const iMessage = next ? filtered[filtered.length - 1] : filtered[0]; + const newData = { + images: extractImages(iMessage), + messageId: iMessage.id + }; + this.setState({ + __BIV_data: newData, + __BIV_index: newData.images.length - 1, + ...newData.images[newData.images.length - 1] + }); + this.requestImageInfo(newData.images[newData.images.length - 1]); + this.handlePreLoad(-1, next); + } + render() { + if (this.state.internalError === -1) throw 'If you see this, something went HORRIBLY wrong!'; + for (const prop of ImageProps) this.props[prop] = this.state[prop]; + const ret = super.render(); + if (this.state.internalError) return ret; + const message = this.state.__BIV_data && this.getMessage(this.state.__BIV_data.messageId); + if ((!message && this.state.__BIV_data)) return ret; + if (!message) { + if (!this.__couldNotFindMessage) XenoLib.Notifications.error(`[**${config.info.name}**] Something went wrong.. Could not find associated message for current image.`, { timeout: 7500 }); + this.__couldNotFindMessage = true; + return ret; + } else this.__couldNotFindMessage = false; + const currentImage = this._imageCounter[this.state.__BIV_data.messageId] + (this.state.__BIV_data.images.length - 1 - this.state.__BIV_index); + ret.props.children[0].type = LazyImage; + ret.props.children[0].props.id = message.id + currentImage; + ret.props.children[0].props.__BIV_original = this.props.original; + const iMember = DiscordAPI.currentGuild && GuildMemberStore.getMember(DiscordAPI.currentGuild.id, message.author.id); + ret.props.children.push( + ReactDOM.createPortal( + [ + this.props.__BIV_settings.ui.navButtons || this.props.__BIV_settings.behavior.debug + ? [ + React.createElement( + Clickable, + { + className: XenoLib.joinClassNames('BIV-left', { 'BIV-disabled': currentImage === this._maxImages && (this._searchCache.noBefore || this.state.rateLimited), 'BIV-inactive': this.state.controlsInactive, 'BIV-hidden': !this.state.controlsVisible }), + onClick: this.handlePrevious, + onContextMenu: this.handleFastJumpLeft, + onMouseEnter: this.handleMouseEnterLeft, + onMouseLeave: this.handleMouseLeave + }, + React.createElement(LeftCaretIcon) + ), + React.createElement( + Clickable, + { + className: XenoLib.joinClassNames('BIV-right', { 'BIV-disabled': currentImage === 1, 'BIV-inactive': this.state.controlsInactive, 'BIV-hidden': !this.state.controlsVisible }), + onClick: this.handleNext, + onContextMenu: this.handleFastJumpRight, + onMouseEnter: this.handleMouseEnterRight, + onMouseLeave: this.handleMouseLeave + }, + React.createElement(RightCaretIcon) + ) + ] + : null, + React.createElement( + 'div', + { + className: XenoLib.joinClassNames('BIV-info', { 'BIV-inactive': this.state.controlsInactive, 'BIV-hidden': !this.state.controlsVisible }) + }, + this.props.__BIV_settings.ui.imageIndex || this.props.__BIV_settings.behavior.debug + ? React.createElement( + TextElement, + { + className: 'BIV-text-bold' + }, + 'Image ', + currentImage, + ' of ', + this._maxImages, + this._searchCache._totalResults || this.props.__BIV_settings.behavior.debug + ? React.createElement( + Tooltip, + { + text: `Estimated ${this._maxImages + this._searchCache._totalResults} images in current channel`, + position: 'top' + }, + e => React.createElement('span', e, ' (~', this._maxImages + this._searchCache._totalResults, ')') + ) + : undefined + ) + : null, + React.createElement( + 'div', + { + className: 'BIV-info-wrapper' + }, + React.createElement( + TextElement, + { + className: XenoLib.joinClassNames(CozyClassname, 'BIV-info-wrapper-text') + }, + React.createElement( + 'span', + { + className: XenoLib.joinClassNames(UsernameClassname, ClickableClassname), + onContextMenu: e => { + WebpackModules.getByProps('openUserContextMenu').openUserContextMenu(e, message.author, ChannelStore.getChannel(message.channel_id)); + }, + style: + iMember && iMember.colorString + ? { + color: iMember.colorString + } + : null, + onClick: () => { + this.props.onClose(); + NavigationUtils.transitionTo(`/channels/${(DiscordAPI.currentGuild && DiscordAPI.currentGuild.id) || '@me'}/${message.channel_id}${message.id ? '/' + message.id : ''}`); + } + }, + (iMember && iMember.nick) || message.author.username + ), + React.createElement(MessageTimestamp, { + timestamp: DiscordModules.Moment(message.timestamp) + }), + (this.props.__BIV_settings.behavior.debug || this._searchCache.noBefore) && + React.createElement( + 'div', + { + className: XenoLib.joinClassNames('BIV-requesting', TextElement.Colors.ERROR) + }, + React.createElement( + Tooltip, + { + text: 'You have reached the start of the channel' + }, + e => React.createElement(LeftCaretIcon, e) + ) + ), + (this.props.__BIV_settings.behavior.debug || (this.state.isNearingEdge && !this.props.__BIV_settings.behavior.searchAPI)) && + React.createElement( + 'div', + { + className: XenoLib.joinClassNames('BIV-requesting', TextElement.Colors.STATUS_YELLOW) + }, + React.createElement( + Tooltip, + { + text: 'You are nearing the edge of available images. If you want more, enable search API.' + }, + e => React.createElement(WarningTriangleIcon, e) + ) + ), + (this.props.__BIV_settings.behavior.debug || (this.state.requesting && !this.state.unknownError)) && + React.createElement( + 'div', + { + className: 'BIV-requesting' + }, + React.createElement( + Tooltip, + { + text: 'Requesting more...' + }, + e => React.createElement(UpdateAvailableIcon, e) + ) + ), + (this.props.__BIV_settings.behavior.debug || this.state.indexing) && + React.createElement( + 'div', + { + className: 'BIV-requesting' + }, + React.createElement( + Tooltip, + { + text: 'Indexing channel...' + }, + e => React.createElement(SearchIcon, e) + ) + ), + this.props.__BIV_settings.behavior.debug || this.state.localRateLimited || this.state.rateLimited + ? React.createElement( + 'div', + { + className: XenoLib.joinClassNames('BIV-requesting', TextElement.Colors.ERROR) + }, + React.createElement( + Tooltip, + { + text: 'You have been rate limited, please wait' + }, + e => React.createElement(TimerIcon, e) + ) + ) + : undefined, + (this.props.__BIV_settings.behavior.debug || this._followNew) && + React.createElement( + 'div', + { + className: XenoLib.joinClassNames('BIV-requesting', TextElement.Colors.ERROR) + }, + React.createElement( + Tooltip, + { + text: 'You have reached the end of the channel and are listening for new images' + }, + e => React.createElement(RightCaretIcon, e) + ) + ), + (this.props.__BIV_settings.behavior.debug || this.state.unknownError) && + React.createElement( + 'div', + { + className: XenoLib.joinClassNames('BIV-requesting', TextElement.Colors.ERROR) + }, + React.createElement( + Tooltip, + { + text: 'Unknown error occured' + }, + e => React.createElement(ClearIcon, e) + ) + ) + ) + ) + ) + ], + overlayDOMNode + ) + ); + return ret; + } + } + + const BetaClasses = WebpackModules.find(e => e.beta && !e.channel); + + return class BetterImageViewer extends Plugin { + constructor() { + super(); + XenoLib.changeName(__filename, this.name); + this.handleWHChange = this.handleWHChange.bind(this); + this.showChangelog = this.showChangelog.bind(this); + const oOnStart = this.onStart.bind(this); + this._startFailure = message => { + PluginUpdater.checkForUpdate(this.name, this.version, this._config.info.github_raw); + XenoLib.Notifications.error(`[**${this.name}**] ${message} Please update it, press CTRL + R, or ${GuildStore.getGuild(XenoLib.supportServerId) ? 'go to <#639665366380838924>' : '[join my support server](https://discord.gg/NYvWdN5)'} for further assistance.`, { timeout: 0 }); + }; + this.onStart = () => { + try { + oOnStart(); + } catch (e) { + Logger.stacktrace('Failed to start!', e); + this._startFailure('Failed to start!'); + try { + this.onStop(); + } catch (e) { } + } + }; + try { + ModalStack.closeModal(`${this.name}_DEP_MODAL`); + } catch (e) { } + } + onStart() { + if (!overlayDOMNode) { + overlayDOMNode = document.createElement('div'); + overlayDOMNode.className = 'biv-overlay'; + } + document.querySelector('#app-mount').append(overlayDOMNode); + this.promises = { state: { cancelled: false } }; + if (PluginBrokenFatal) return this._startFailure('Plugin is in a broken state.'); + if (NoImageZoom) this._startFailure('Image zoom is broken.'); + if (this.settings.zoom.enabled && !NoImageZoom && BdApi.Plugins.get('ImageZoom') && BdApi.Plugins.isEnabled('ImageZoom')) XenoLib.Notifications.warning(`[**${this.name}**] Using **ImageZoom** while having the zoom function in **${this.name}** enabled is unsupported! Please disable one or the other.`, { timeout: 15000 }); + if (BdApi.Plugins.get('Better Image Popups') && BdApi.Plugins.isEnabled('Better Image Popups')) XenoLib.Notifications.warning(`[**${this.name}**] Using **Better Image Popups** with **${this.name}** is completely unsupported and will cause issues. **${this.name}** fully supersedes it in terms of features as well, please either disable **Better Image Popups** or delete it to avoid issues.`, { timeout: 0 }); + if (this.settings.zoom.enabled && BdApi.Plugins.get('ImageGallery') && BdApi.Plugins.isEnabled('ImageGallery')) XenoLib.Notifications.warning(`[**${this.name}**] Using **ImageGallery** with **${this.name}** is completely unsupported and will cause issues, mainly, zoom breaks. **${this.name}** fully supersedes it in terms of features as well, please either disable **ImageGallery** or delete it to avoid issues.`, { timeout: 0 }); + this.hiddenSettings = XenoLib.loadData(this.name, 'hidden', { panelWH: 500 }); + this.patchAll(); + Dispatcher.subscribe('MESSAGE_DELETE', this.handleMessageDelete); + Dispatcher.subscribe('MESSAGE_DELETE_BULK', this.handlePurge); + Dispatcher.subscribe('BIV_LENS_WH_CHANGE', this.handleWHChange); + PluginUtilities.addStyle( + this.short + '-CSS', + ` + .BIV-left, + .BIV-right { + position: absolute; + top: 90px; + bottom: 90px; + width: 90px; + background-color: transparent; + display: flex; + justify-content: center; + align-items: center; + transition: all 0.25s ease-in-out; + color: gray; + } + .BIV-disabled { + color: #4d4d4d; + } + .BIV-left.BIV-inactive, + .BIV-right.BIV-inactive { + opacity: 0; + } + .BIV-info.BIV-inactive { + opacity: 0.65; + } + .BIV-left:not(.BIV-disabled):hover, + .BIV-right:not(.BIV-disabled):hover { + background-color: hsla(0, 0%, 49%, 0.2); + color: #fff; + } + .BIV-left { + left: 0; + } + .BIV-right { + right: 0; + } + .BIV-left > svg, + .BIV-right > svg { + width: 30px; + height: 30px; + } + .BIV-info { + position: absolute; + left: 18px; + bottom: 12px; + height: 42px; + transition: opacity 0.35s ease-in-out; + } + .theme-light .BIV-info { + color: white; + } + .BIV-info-extra { + left: unset; + right: 12px; + height: unset; + } + .BIV-info-extra > table { + width: 200px; + } + .BIV-info-extra tr > td:nth-child(2) { + text-align: end; + } + .BIV-info-wrapper { + bottom: 0; + position: absolute; + white-space: nowrap; + } + .BIV-info-wrapper > .BIV-info-wrapper-text { + display: flex; + align-items: center; + } + .BIV-requesting { + display: flex; + margin-left: 5px; + } + .BIV-requesting > svg[name='Nova_Search'], + .BIV-requesting > svg[name='LeftCaret'], + .BIV-requesting > svg[name='RightCaret'] { + width: 16px; + height: 16px; + } + .BIV-zoom-backdrop, + .biv-overlay { + width: 100%; + height: 100%; + position: absolute; + } + .BIV-inactive { + transition: opacity 1s ease-in-out; + } + .BIV-hidden { + opacity: 0; + } + .BIV-info-wrapper .${XenoLib.getClass('header username')} { + max-width: 900px; + overflow-x: hidden; + margin-right: 0.25rem; + } + .biv-overlay { + pointer-events: none; + z-index: 1002; + } + .biv-overlay > * { + pointer-events: all; + } + @keyframes BIV-spin { + 0% { + transform: rotate(0); + } + to { + transform: rotate(1turn); + } + } + .BIV-searching-icon-spin { + animation: BIV-spin 2s linear infinite; + } + .BIV-zoom-lens { + overflow: hidden; + cursor: none; + border: solid #0092ff; + border-width: thin; + } + .BIV-zoom-lens-round { + border-radius: 50%; + border: 2px solid #0092ff; + } + .BIV-zoom-backdrop { + background: rgba(0, 0, 0, 0.4); + } + .BIV-text-bold { + font-weight: 600; + } + .theme-light .BIV-text-bold { + color: white; + } + .${WebpackModules.find(e => Object.keys(e).length === 2 && e.modal && e.inner).modal.split(' ')[0]} > .${WebpackModules.find(e => Object.keys(e).length === 2 && e.modal && e.inner).inner.split(' ')[0]} > .${XenoLib.getSingleClass('imageZoom imageWrapper')} { + display: table; /* lol */ + } + ` + ); + } + + onStop() { + this.promises.state.cancelled = true; + Patcher.unpatchAll(); + Dispatcher.unsubscribe('MESSAGE_DELETE', this.handleMessageDelete); + Dispatcher.unsubscribe('MESSAGE_DELETE_BULK', this.handlePurge); + Dispatcher.unsubscribe('BIV_LENS_WH_CHANGE', this.handleWHChange); + PluginUtilities.removeStyle(this.short + '-CSS'); + if (overlayDOMNode) overlayDOMNode.remove(); + overlayDOMNode = null; + } + + buildSetting(data) { + if (data.type === 'switchbeta') { + data.type = 'switch'; + data.name = [data.name, React.createElement('sup', { className: BetaClasses.beta }, 'BETA')]; + } + return XenoLib.buildSetting(data); + } + + saveSettings(category, setting, value) { + super.saveSettings(category, setting, value); + if (category === 'chat') { + this._MA.forceUpdateAll(); + } + } + + saveHiddenSettings() { + PluginUtilities.saveData(this.name, 'hidden', this.hiddenSettings); + } + + handleMessageDelete(e) { + const { channelId, id: messageId } = e; + stripDeletedMessage(channelId, messageId); + } + handlePurge(e) { + const { channelId, ids: messageIds } = e; + stripPurgedMessages(channelId, messageIds); + } + handleWHChange({ value }) { + this.hiddenSettings.panelWH = value; + this.saveHiddenSettings(); + } + + fetchFilename(url) { + try { + if (url.indexOf('//giphy.com/gifs/') !== -1) url = `https://i.giphy.com/media/${url.match(/-([^-]+)$/)[1]}/giphy.gif`; + const match = url.match(/(?:\/)([^\/]+?)(?:(?:\.)([^.\/?:]+)){0,1}(?:[^\w\/\.]+\w+){0,1}(?:(?:\?[^\/]+){0,1}|(?:\/){0,1})$/); + let name = match[1]; + let extension = match[2] || '.png'; + if (url.indexOf('//media.tenor.co') !== -1) { + extension = name; + name = url.match(/\/\/media.tenor.co\/[^\/]+\/([^\/]+)\//)[1]; + } else if (url.indexOf('//i.giphy.com/media/') !== -1) name = url.match(/\/\/i\.giphy\.com\/media\/([^\/]+)\//)[1]; + return `${name}.${extension}`; + } catch (err) { + Logger.stacktrace('Failed to fetch filename', url, err); + return 'unknown.png'; + } + } + + /* PATCHES */ + + patchAll() { + Utilities.suppressErrors(this.patchMessageAccessories.bind(this), 'MessageAccessories patches')(this.promises.state); + Utilities.suppressErrors(this.patchLazyImageZoomable.bind(this), 'LazyImageZoomable patches')(); + Utilities.suppressErrors(this.patchImageModal.bind(this), 'ImageModal patches')(); + Utilities.suppressErrors(this.patchLazyImage.bind(this), 'LazyImage patches')(); + Utilities.suppressErrors(this.patchImageScaling.bind(this), 'image scaling patches')(); + } + + patchLazyImageZoomable() { + const patchKey = DiscordModules.KeyGenerator(); + const MaskedLink = WebpackModules.getByDisplayName('MaskedLink'); + const renderLinkComponent = props => React.createElement(MaskedLink, props); + const Modals = WebpackModules.getByProps('ModalRoot'); + const ImageModalClasses = WebpackModules.find(m => typeof m.image === 'string' && typeof m.modal === 'string' && !m.content && !m.card) || WebpackModules.getByProps('modal', 'image'); + Patcher.before(WebpackModules.getByDisplayName('LazyImageZoomable').prototype, 'render', (_this, _, ret) => { + if (_this.onZoom.__BIV_patched !== patchKey) { + _this.onZoom = (e, n) => { + let isSearch = e.target; + while (isSearch && typeof isSearch.className === 'string' && isSearch.className.indexOf('searchResult') === -1) isSearch = isSearch.parentElement; + isSearch = isSearch && typeof isSearch.className === 'string' && isSearch.className.indexOf('searchResult') !== -1; + e.preventDefault(); + if (e.currentTarget instanceof HTMLElement) e.currentTarget.blur(); + e = null; + const original = _this.props.original || _this.props.src; + ModalStack.openModal(e => { + try { + return React.createElement( + /* this safety net should prevent any major issues or crashes, in theory + UPDATE: 21.7.2020 the theory was wrong, I'm an idiot and set the state to an invalid one immediately + causing another crash and crashing the entire client! + */ + ErrorCatcher, + { + label: 'Image modal', + onError: (_, level) => { + if (level < 2) XenoLib.Notifications.error(`[${this.name}] Internal error, options will not show. If you repeatedly see this, join my support server, open up console (CTRL + SHIFT + I > click console) and screenshot any errors.`, { timeout: 0 }); + if (level > 1) e.onClose(); + }, + fallback: [ + React.createElement( + Modals.ModalRoot, + { className: ImageModalClasses.modal, ...e, size: Modals.ModalSize.DYNAMIC }, + React.createElement( + ImageModal, + Object.assign( + { + original, + src: _this.props.src, + width: _this.props.width, + height: _this.props.height, + animated: _this.props.animated, + children: _this.props.children, + placeholder: n.placeholder, + isTrusted: TrustedStore.isTrustedDomain(original), + onClickUntrusted: _this.onClickUntrusted, + renderLinkComponent, + className: ImageModalClasses.image, + shouldAnimate: true + }, + e + ) + ) + ) + ] + }, + React.createElement( + Modals.ModalRoot, + { className: ImageModalClasses.modal, ...e, size: Modals.ModalSize.DYNAMIC }, + React.createElement( + RichImageModal, + Object.assign( + { + original, + src: _this.props.src, + width: _this.props.width, + height: _this.props.height, + animated: _this.props.animated, + children: _this.props.children, + placeholder: n.placeholder, + isTrusted: TrustedStore.isTrustedDomain(original), + onClickUntrusted: _this.onClickUntrusted, + renderLinkComponent, + __BIV_data: _this.props.__BIV_data, + __BIV_index: _this.props.__BIV_data ? _this.props.__BIV_data.images.findIndex(m => m.src === _this.props.src) : -1, + __BIV_isSearch: isSearch, + __BIV_settings: this.settings, + className: ImageModalClasses.image, + shouldAnimate: true + }, + e + ) + ) + ) + ); + } catch (err) { + /* juuuuust in case, modal crashes can be brutal */ + Logger.stacktrace('Error creating image modal', err); + e.onClose(); + return null; + } + }); + }; + _this.onZoom.__BIV_patched = patchKey; + } + }); + } + + async patchMessageAccessories(promiseState) { + const selector = `.${XenoLib.getSingleClass('embedWrapper container')}`; + const MessageAccessories = await ReactComponents.getComponentByName('MessageAccessories', selector); + if (!MessageAccessories.selector) MessageAccessories.selector = selector; + if (promiseState.cancelled) return; + Patcher.before(MessageAccessories.component.prototype, 'render', _this => { + _this.__BIV_data = { + images: extractImages(_this.props.message), + messageId: _this.props.message.id + }; + }); + Patcher.before(MessageAccessories.component.prototype, 'renderEmbeds', _this => { + if (!_this.renderEmbed.__BIV_patched) { + const oRenderEmbed = _this.renderEmbed; + _this.renderEmbed = function (e, n) { + const oRenderImageComponent = n.renderImageComponent; + if (!oRenderImageComponent.__BIV_patched) { + n.renderImageComponent = function (a) { + a.__BIV_data = _this.__BIV_data; + a.__BIV_embed = true; + return oRenderImageComponent(a); + }; + n.renderImageComponent.__BIV_patched = true; + } + return oRenderEmbed(e, n); + }; + _this.renderEmbed.__BIV_patched = true; + } + }); + Patcher.after(MessageAccessories.component.prototype, 'renderAttachments', (_this, _, ret) => { + if (!ret) return; + for (let attachment of ret) { + let props = Utilities.getNestedProp(attachment, 'props.children.props'); + if (!props) continue; + const oRenderImageComponent = props.renderImageComponent; + props.renderImageComponent = function (e) { + e.__BIV_data = _this.__BIV_data; + return oRenderImageComponent(e); + }; + attachment = null; + props = null; + } + ret = null; + }); + MessageAccessories.forceUpdateAll(); + this._MA = MessageAccessories; + } + + patchImageModal() { + /* shared code + these patches are for displaying image info + but the same code is shared with RichImageModal + */ + Patcher.after(ImageModal.prototype, 'handleMouseMove', _this => { + if (_this.state.controlsInactive) { + _this.setState({ controlsInactive: false }); + } + if (_this.state.controlsHovered) _this._controlsInactiveDelayedCall.cancel(); + else _this._controlsInactiveDelayedCall.delay(); + }); + /* https://stackoverflow.com/questions/10420352/ */ + function humanFileSize(bytes, si) { + const thresh = si ? 1000 : 1024; + if (Math.abs(bytes) < thresh) return `${bytes} B`; + const units = si ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; + let u = -1; + do { + bytes /= thresh; + ++u; + } while (Math.abs(bytes) >= thresh && u < units.length - 1); + return `${bytes.toFixed(1)} ${units[u]}`; + } + Patcher.after(ImageModal.prototype, 'requestImageInfo', (_this, [props, equalRatio]) => { + const original = (props && props.original) || _this.props.original; + const src = (props && props.src) || _this.props.src; + const width = (props && props.width) || _this.props.width; + const height = (props && props.height) || _this.props.height; + const reqUrl = (() => { + const split = src.split('?')[0]; + const SaveToRedux = BdApi.Plugins.get('SaveToRedux'); + const needsSize = src.substr(src.indexOf('?')).indexOf('size=') !== -1; + try { + if (SaveToRedux) return SaveToRedux.formatURL(original || '', needsSize, '', '', split).url; + } catch (_) { } + return split + (needsSize ? '?size=2048' : ''); + })(); + const max = ImageUtils.zoomFit(width, height); + const ratio = getRatio(width, height, max.width, max.height); + const aa = ImageUtils.getImageSrc(src, width, height, ratio); + _this._headerRequest2 = RequestModule.head(aa, (err, res) => { + if (err || res.statusCode >= 400) return _this.setState({ imageSize: -1 }); + _this.setState({ imageSize: humanFileSize(res.headers['content-length']) }); + if (equalRatio) _this.setState({ originalImageSize: humanFileSize(res.headers['content-length']) }); + }); + if (!equalRatio) { + _this._headerRequest1 = RequestModule.head(reqUrl, (err, res) => { + if (err || res.statusCode >= 400) return _this.setState({ originalImageSize: -1 }); + _this.setState({ originalImageSize: humanFileSize(res.headers['content-length']) }); + }); + } + }); + const RequestModule = require('request'); + Patcher.after(ImageModal.prototype, 'componentDidMount', _this => { + if (!_this.state || _this.state.internalError) return; + const requestImageInfo = XenoLib._.debounce(_this.requestImageInfo.bind(_this), 750, { leading: true }); + _this.requestImageInfo = props => { + const settings = this.settings.ui; + if (!settings.infoResolution && settings.infoScale && settings.infoSize) return; + const width = (props && props.width) || _this.props.width; + const height = (props && props.height) || _this.props.height; + const max = ImageUtils.zoomFit(width, height); + const scaledRatio = getRatio(width, height, max.width, max.height); + const finalRatio = scaledRatio < 1 ? scaledRatio : 1; + if (settings.infoResolution || settings.infoScale) { + _this.setState({ + basicImageInfo: { + width: Math.ceil(width * finalRatio), + height: Math.ceil(height * finalRatio), + ratio: finalRatio + }, + imageSize: null, + originalImageSize: null + }); + } else _this.setState({ imageSize: null, originalImageSize: null }); + if (_this._headerRequest1) _this._headerRequest1.abort(); + if (_this._headerRequest2) _this._headerRequest2.abort(); + if (settings.infoSize) requestImageInfo(props, finalRatio === 1); + }; + _this.requestImageInfo(); + _this._controlsVisibleTimeout = new TimingModule.Timeout(); + _this._controlsVisibleTimeout.start(300, () => _this.setState({ controlsVisible: true })); + _this._controlsInactiveDelayedCall = new TimingModule.DelayedCall(3500, () => _this.setState({ controlsInactive: true })); + _this._controlsInactiveDelayedCall.delay(); + _this.handleMouseMove = XenoLib._.throttle(_this.handleMouseMove.bind(_this), 500); + window.addEventListener('mousemove', _this.handleMouseMove); + Dispatcher.subscribe( + 'BIV_LOAD_FULLRES', + (_this._fullresHandler = () => { + if (_this.state.showFullRes) return; + _this.setState({ + showFullRes: true + }); + }) + ); + }); + Patcher.after(ImageModal.prototype, 'componentWillUnmount', _this => { + if (!_this.state || _this.state.internalError) return; + if (_this._headerRequest1) _this._headerRequest1.abort(); + if (_this._headerRequest2) _this._headerRequest2.abort(); + if (!_this._controlsVisibleTimeout) { + /* since the BdApi is like a child on cocaine, I'll just wrap it in a try catch */ + let reloadFailed = false; + try { + BdApi.Plugins.reload(this.name); + } catch (e) { + try { + pluginModule.reloadPlugin(this.name); + } catch (e) { + reloadFailed = true; + } + } + XenoLib.Notifications.warning(`[**${this.name}**] Something's not right.. ${reloadFailed ? 'Reloading self failed..' : 'Reloading self.'}`); + } + if (_this._controlsVisibleTimeout) _this._controlsVisibleTimeout.stop(); + if (_this._controlsInactiveDelayedCall) _this._controlsInactiveDelayedCall.cancel(); + window.removeEventListener('mousemove', _this.handleMouseMove); + Dispatcher.unsubscribe('BIV_LOAD_FULLRES', _this._fullresHandler); + }); + const renderTableEntry = (val1, val2) => React.createElement('tr', {}, React.createElement('td', {}, val1), React.createElement('td', {}, val2)); + Patcher.after(ImageModal.prototype, 'render', (_this, _, ret) => { + if (!_this.state) + _this.state = { + controlsInactive: false, + controlsVisible: false, + imageSize: null, + originalImageSize: null, + basicImageInfo: null, + showFullRes: false + }; + if (this.settings.ui.loadFull) _this.state.showFullRes = true; + const imageProps = Utilities.getNestedProp(ret, 'props.children.0.props'); + if (imageProps) imageProps.__BIV_full_res = _this.state.showFullRes; + if (_this.state.internalError) return; + const settings = this.settings.ui; + const debug = this.settings.behavior.debug; + if (!settings.infoResolution && settings.infoScale && settings.infoSize && !debug) return; + const { basicImageInfo, imageSize, originalImageSize } = _this.state; + // splice in, otherwise ImageToClipboard freaks out + ret.props.children.splice( + 1, + 0, + /* portals are cool o; */ + ReactDOM.createPortal( + React.createElement( + 'div', + { + className: XenoLib.joinClassNames('BIV-info BIV-info-extra', { 'BIV-hidden': !_this.state.controlsVisible, 'BIV-inactive': _this.state.controlsInactive && !debug }, TextElement.Colors.STANDARD) + }, + React.createElement('table', {}, settings.infoFilename || debug ? React.createElement('tr', {}, React.createElement('td', { colspan: 2 }, this.fetchFilename(_this.props.src))) : null, settings.infoResolution || debug ? renderTableEntry(basicImageInfo ? React.createElement('span', { className: _this.state.showFullRes ? TextElement.Colors.ERROR : undefined }, `${basicImageInfo.width}x${basicImageInfo.height}`) : 'NaNxNaN', `${_this.props.width}x${_this.props.height}`) : null, settings.infoSize || debug ? renderTableEntry(imageSize ? imageSize : 'NaN', originalImageSize ? (originalImageSize === imageSize ? '~' : originalImageSize) : 'NaN') : null, debug ? Object.keys(_this.state).map(key => (!XenoLib._.isObject(_this.state[key]) && key !== 'src' && key !== 'original' && key !== 'placeholder' ? renderTableEntry(key, String(_this.state[key])) : null)) : null) + ), + overlayDOMNode + ) + ); + }); + } + + patchLazyImage() { + if (NoImageZoom) return; + const LazyImage = WebpackModules.getByDisplayName('LazyImage'); + // BETA CODE! + const SectionStore = WebpackModules.find(m => m.getSection && !m.getProps && !m.getStats); + const NO_SIDEBAR = 0.666178623635432; + const SEARCH_SIDEBAR = 0.3601756956193265; + const MEMBERS_SIDEBAR = 0.49048316246120055; + Patcher.instead(LazyImage.prototype, 'handleSidebarChange', (_this, [forced]) => { + if (!this.settings.chat.resize || !SectionStore) return; + const { state } = _this; + if (!currentChannel()) { + state.__BIV_sidebarMultiplier = null; + return; + } + const section = SectionStore.getSection(); + let newMultiplier; + if (section === 'SEARCH') newMultiplier = SEARCH_SIDEBAR; + else if (section !== 'MEMBERS' || (!DiscordModules.SelectedGuildStore.getGuildId() && currentChannel().type !== 'GROUP_DM')) newMultiplier = NO_SIDEBAR; + else newMultiplier = MEMBERS_SIDEBAR; + if (!forced && newMultiplier !== state.__BIV_sidebarMultiplier) _this.setState({ __BIV_sidebarMultiplier: newMultiplier }); + else state.__BIV_sidebarMultiplier = newMultiplier; + }); + Patcher.after(LazyImage.prototype, 'componentDidMount', _this => { + if (typeof _this.props.__BIV_index !== 'undefined' /* || _this.props.__BIV_isVideo */ || (_this.props.className && _this.props.className.indexOf('embedThumbnail') !== -1) || _this.props.__BIV_embed) { + _this.handleSidebarChange = null; + return; + } + _this.handleSidebarChange = _this.handleSidebarChange.bind(_this); + if (SectionStore) SectionStore.addChangeListener(_this.handleSidebarChange); + }); + Patcher.after(LazyImage.prototype, 'componentWillUnmount', _this => { + if (!_this.handleSidebarChange) return; + if (SectionStore) SectionStore.removeChangeListener(_this.handleSidebarChange); + }); + Patcher.before(LazyImage.prototype, 'getRatio', _this => { + if ((!this.settings.chat.resize || !SectionStore) || _this.props.__BIV_embed) return; + if (!_this.handleSidebarChange || typeof _this.props.__BIV_index !== 'undefined' /* || _this.props.__BIV_isVideo */ || (_this.props.className && _this.props.className.indexOf('embedThumbnail') !== -1)) return; + if (typeof _this.state.__BIV_sidebarType === 'undefined') _this.handleSidebarChange(true); + if (_this.state.__BIV_sidebarMultiplier === null) return; + const scale = window.innerWidth / (window.innerWidth * window.devicePixelRatio); + _this.props.maxWidth = Math.max(Math.min(innerWidth * devicePixelRatio * _this.state.__BIV_sidebarMultiplier * (1 + (1 - devicePixelRatio)), _this.props.width * scale), 400); + _this.props.maxHeight = Math.max(Math.min(innerHeight * devicePixelRatio * 0.6777027027027027, _this.props.height * scale), 300); + }); + // BETA CODE! + + Patcher.instead(LazyImage.prototype, 'componentDidUpdate', (_this, [props, state]) => { + /* custom handler, original one caused issues with GIFs not animating */ + const animated = LazyImage.isAnimated(_this.props); + if (animated !== LazyImage.isAnimated(props)) { + if (animated) _this.observeVisibility(); + else _this.unobserveVisibility(); + } else if (state.readyState !== _this.state.readyState && animated) _this.observeVisibility(); + else if (!animated) _this.unobserveVisibility(); + }); + Patcher.instead(LazyImage.prototype, 'getSrc', (_this, [ratio, forcePng], orig) => { + if (_this.props.__BIV_full_res) return _this.props.src; + return orig(ratio, forcePng); + }); + Patcher.after(LazyImage.prototype, 'render', (_this, _, ret) => { + if (!ret) return; + if (_this.props.__BIV_isVideo) return; + /* fix scaling issues for all images */ + if (!this.settings.chat.scale && _this.props.onZoom || _this.props.__BIV_embed) return; + const scale = window.innerWidth / (window.innerWidth * window.devicePixelRatio); + ret.props.width = ret.props.width * scale; + ret.props.height = ret.props.height * scale; + if (_this.state.readyState !== 'READY') return; + if (_this.props.onZoom) return; + if (_this.props.animated && ret.props.children) { + /* dirty */ + try { + ret.props.__BIV_src = ret.props.children({ size: {} }).props.src; + } catch (e) { + return; + } + } + ret.type = Image; + ret.props.__BIV_settings = this.settings.zoom; + ret.props.__BIV_animated = _this.props.animated; + ret.props.__BIV_hiddenSettings = this.hiddenSettings; + }); + Patcher.after(WebpackModules.getByDisplayName('LazyVideo').prototype, 'render', (_, __, ret) => { + if (!ret) return; + ret.props.__BIV_isVideo = true; + }); + } + + patchImageScaling() { + Patcher.instead(WebpackModules.getByProps('zoomFit'), 'zoomFit', (_, [e, t]) => ImageUtils.zoomFit(e, t)); + Patcher.instead(_ImageUtils, 'getImageSrc', (_, args) => ImageUtils.getImageSrc(...args)); + Patcher.before(_ImageUtils, 'getSizedImageSrc', (_, args) => { + const toAdd = window.innerWidth / (window.innerWidth * window.devicePixelRatio); + args[1] *= toAdd; + args[2] *= toAdd; + }); + } + + /* PATCHES */ + + showChangelog(footer) { + XenoLib.showChangelog(`${this.name} has been updated!`, this.version, this._config.changelog); + } + + getSettingsPanel() { + return this.buildSettingsPanel().append(new XenoLib.Settings.PluginFooter(this.showChangelog)).getElement(); + } + + get [Symbol.toStringTag]() { + return 'Plugin'; + } + get name() { + return config.info.name; + } + get short() { + let string = ''; + for (let i = 0, len = config.info.name.length; i < len; i++) { + const char = config.info.name[i]; + if (char === char.toUpperCase()) string += char; + } + return string; + } + get author() { + return config.info.authors.map(author => author.name).join(', '); + } + get version() { + return config.info.version; + } + get description() { + return config.info.description; + } + }; + }; + + /* Finalize */ + + let ZeresPluginLibraryOutdated = false; + let XenoLibOutdated = false; + try { + const i = (i, n) => ((i = i.split('.').map(i => parseInt(i))), (n = n.split('.').map(i => parseInt(i))), !!(n[0] > i[0]) || !!(n[0] == i[0] && n[1] > i[1]) || !!(n[0] == i[0] && n[1] == i[1] && n[2] > i[2])), + n = (n, e) => n && n._config && n._config.info && n._config.info.version && i(n._config.info.version, e), + e = BdApi.Plugins.get('ZeresPluginLibrary'), + o = BdApi.Plugins.get('XenoLib'); + n(e, '1.2.27') && (ZeresPluginLibraryOutdated = !0), n(o, '1.3.35') && (XenoLibOutdated = !0); + } catch (i) { + console.error('Error checking if libraries are out of date', i); + } + + return !global.ZeresPluginLibrary || !global.XenoLib || ZeresPluginLibraryOutdated || XenoLibOutdated + ? class { + constructor() { + this._XL_PLUGIN = true; + this.start = this.load = this.handleMissingLib; + } + getName() { + return this.name.replace(/\s+/g, ''); + } + getAuthor() { + return this.author; + } + getVersion() { + return this.version; + } + getDescription() { + return this.description + ' You are missing libraries for this plugin, please enable the plugin and click Download Now.'; + } + start() { } + stop() { } + handleMissingLib() { + const a = BdApi.findModuleByProps('openModal', 'hasModalOpen'); + if (a && a.hasModalOpen(`${this.name}_DEP_MODAL`)) return; + const b = !global.XenoLib, + c = !global.ZeresPluginLibrary, + d = (b && c) || ((b || c) && (XenoLibOutdated || ZeresPluginLibraryOutdated)), + e = (() => { + let a = ''; + return b || c ? (a += `Missing${XenoLibOutdated || ZeresPluginLibraryOutdated ? ' and outdated' : ''} `) : (XenoLibOutdated || ZeresPluginLibraryOutdated) && (a += `Outdated `), (a += `${d ? 'Libraries' : 'Library'} `), a; + })(), + f = (() => { + let a = `The ${d ? 'libraries' : 'library'} `; + return b || XenoLibOutdated ? ((a += 'XenoLib '), (c || ZeresPluginLibraryOutdated) && (a += 'and ZeresPluginLibrary ')) : (c || ZeresPluginLibraryOutdated) && (a += 'ZeresPluginLibrary '), (a += `required for ${this.name} ${d ? 'are' : 'is'} ${b || c ? 'missing' : ''}${XenoLibOutdated || ZeresPluginLibraryOutdated ? (b || c ? ' and/or outdated' : 'outdated') : ''}.`), a; + })(), + g = BdApi.findModuleByDisplayName('Text'), + h = BdApi.findModuleByDisplayName('ConfirmModal'), + i = () => 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 (!a || !h || !g) return console.error(`Missing components:${(a ? '' : ' ModalStack') + (h ? '' : ' ConfirmationModalComponent') + (g ? '' : 'TextElement')}`), i(); + class j extends BdApi.React.PureComponent { + constructor(a) { + super(a), (this.state = { hasError: !1 }), (this.componentDidCatch = a => (console.error(`Error in ${this.props.label}, screenshot or copy paste the error above to Lighty for help.`), this.setState({ hasError: !0 }), 'function' == typeof this.props.onError && this.props.onError(a))), (this.render = () => (this.state.hasError ? null : this.props.children)); + } + } + let k = !1, + l = !1; + const m = a.openModal( + b => { + if (l) return null; + try { + return BdApi.React.createElement( + j, + { label: 'missing dependency modal', onError: () => (a.closeModal(m), i()) }, + BdApi.React.createElement( + h, + Object.assign( + { + header: e, + children: BdApi.React.createElement(g, { size: g.Sizes.SIZE_16, children: [`${f} Please click Download Now to download ${d ? 'them' : 'it'}.`] }), + red: !1, + confirmText: 'Download Now', + cancelText: 'Cancel', + onCancel: b.onClose, + onConfirm: () => { + if (k) return; + k = !0; + const b = require('request'), + c = require('fs'), + d = require('path'), + e = BdApi.Plugins && BdApi.Plugins.folder ? BdApi.Plugins.folder : window.ContentManager.pluginsFolder, + f = () => { + (global.XenoLib && !XenoLibOutdated) || + b('https://gitdab.com/Dollar3795/LightcordPlugins/raw/branch/master/Plugins/1XenoLib.plugin.js', (b, f, g) => { + try { + if (b || 200 !== f.statusCode) return a.closeModal(m), i(); + c.writeFile(d.join(e, '1XenoLib.plugin.js'), g, () => { }); + } catch (b) { + console.error('Fatal error downloading XenoLib', b), a.closeModal(m), i(); + } + }); + }; + !global.ZeresPluginLibrary || ZeresPluginLibraryOutdated + ? b('https://raw.githubusercontent.com/rauenzi/BDPluginLibrary/master/release/0PluginLibrary.plugin.js', (b, g, h) => { + try { + if (b || 200 !== g.statusCode) return a.closeModal(m), i(); + c.writeFile(d.join(e, '0PluginLibrary.plugin.js'), h, () => { }), f(); + } catch (b) { + console.error('Fatal error downloading ZeresPluginLibrary', b), a.closeModal(m), i(); + } + }) + : f(); + } + }, + b, + { onClose: () => { } } + ) + ) + ); + } catch (b) { + return console.error('There has been an error constructing the modal', b), (l = !0), a.closeModal(m), i(), null; + } + }, + { modalKey: `${this.name}_DEP_MODAL` } + ); + } + get [Symbol.toStringTag]() { + return 'Plugin'; + } + get name() { + return config.info.name; + } + get short() { + let string = ''; + for (let i = 0, len = config.info.name.length; i < len; i++) { + const char = config.info.name[i]; + if (char === char.toUpperCase()) string += char; + } + return string; + } + get author() { + return config.info.authors.map(author => author.name).join(', '); + } + get version() { + return config.info.version; + } + get description() { + return config.info.description; + } + } + : buildPlugin(global.ZeresPluginLibrary.buildPlugin(config)); +})(); + +/*@end@*/ diff --git a/Plugins/BetterTypingUsers.plugin.js b/Plugins/BetterTypingUsers.plugin.js new file mode 100644 index 0000000..0934d58 --- /dev/null +++ b/Plugins/BetterTypingUsers.plugin.js @@ -0,0 +1,510 @@ +//META{"name":"BetterTypingUsers","source":"https://github.com/1Lighty/BetterDiscordPlugins/blob/master/Plugins/BetterTypingUsers/BetterTypingUsers.plugin.js","website":"https://1lighty.github.io/BetterDiscordStuff/?plugin=BetterTypingUsers","authorId":"239513071272329217","invite":"NYvWdN5","donate":"https://paypal.me/lighty13"}*// +/*@cc_on +@if (@_jscript) + + // Offer to self-install for clueless users that try to run this directly. + var shell = WScript.CreateObject('WScript.Shell'); + var fs = new ActiveXObject('Scripting.FileSystemObject'); + var pathPlugins = shell.ExpandEnvironmentStrings('%APPDATA%\\BetterDiscord\\plugins'); + var pathSelf = WScript.ScriptFullName; + // Put the user at ease by addressing them in the first person + shell.Popup('It looks like you\'ve mistakenly tried to run me directly. \n(Don\'t do that!)', 0, 'I\'m a plugin for BetterDiscord', 0x30); + if (fs.GetParentFolderName(pathSelf) === fs.GetAbsolutePathName(pathPlugins)) { + shell.Popup('I\'m in the correct folder already.\nJust reload Discord with Ctrl+R.', 0, 'I\'m already installed', 0x40); + } else if (!fs.FolderExists(pathPlugins)) { + shell.Popup('I can\'t find the BetterDiscord plugins folder.\nAre you sure it\'s even installed?', 0, 'Can\'t install myself', 0x10); + } else if (shell.Popup('Should I copy myself to BetterDiscord\'s plugins folder for you?', 0, 'Do you need some help?', 0x34) === 6) { + fs.CopyFile(pathSelf, fs.BuildPath(pathPlugins, fs.GetFileName(pathSelf)), true); + // Show the user where to put plugins in the future + shell.Exec('explorer ' + pathPlugins); + shell.Popup('I\'m installed!\nJust reload Discord with Ctrl+R.', 0, 'Successfully installed', 0x40); + } + WScript.Quit(); + +@else@*/ +/* + * Copyright © 2019-2020, _Lighty_ + * All rights reserved. + * Code may not be redistributed, modified or otherwise taken without explicit permission. + */ +module.exports = (() => { + /* Setup */ + const config = { + main: 'index.js', + info: { + name: 'BetterTypingUsers', + authors: [ + { + name: 'Lighty', + discord_id: '239513071272329217', + github_username: 'LightyPon', + twitter_username: '' + } + ], + version: '1.0.3', + description: 'Replaces "Several people are typing" with who is actually typing, plus "x others" if it can\'t fit. Number of shown people typing can be changed.', + github: 'https://github.com/1Lighty', + github_raw: 'https://raw.githubusercontent.com/1Lighty/BetterDiscordPlugins/master/Plugins/BetterTypingUsers/BetterTypingUsers.plugin.js' + }, + changelog: [ + { + title: 'RIP BBD on Canary', + type: 'fixed', + items: ['Implemented fixes that allow patches to work properly on canary using Powercord.'] + } + ], + defaultConfig: [ + { + name: 'Max visible typing users', + id: 'maxVisible', + type: 'slider', + value: 5, + min: 3, + max: 20, + markers: Array.from(Array(18), (_, i) => i + 3), + stickToMarkers: true + }, + { + name: 'Previews', + type: 'preview' + } + ], + strings: { + en: { AND_1_OTHER: ' and 1 other', AND_X_OTHERS: ' and ${count} others', AND: ' and ', ARE_TYPING: ' are typing...' }, + de: { AND_1_OTHER: ' und 1 andere', AND_X_OTHERS: ' und ${count} andere', AND: ' und ', ARE_TYPING: ' schreiben...' }, + da: { AND_1_OTHER: ' og 1 anden', AND_X_OTHERS: ' og ${count} andre', AND: ' og ', ARE_TYPING: ' skriver...' }, + es: { AND_1_OTHER: ' y 1 otro', AND_X_OTHERS: ' y otros ${count}', AND: ' y ', ARE_TYPING: ' están escribiendo...' }, + fr: { AND_1_OTHER: ' et 1 autre', AND_X_OTHERS: ' et ${count} autres', AND: ' et ', ARE_TYPING: ' écrivent...' }, + hr: { AND_1_OTHER: ' i 1 drugi', AND_X_OTHERS: ' i ${count} drugih', AND: ' i ', ARE_TYPING: ' pišu...' }, + it: { AND_1_OTHER: ' e 1 altro', AND_X_OTHERS: ' e altri ${count}', AND: ' e ', ARE_TYPING: ' stanno scrivendo...' }, + tr: { AND_1_OTHER: ' ve 1 kişi daha', AND_X_OTHERS: ' ve ${count} kişi daha', AND: ' ve ', ARE_TYPING: ' yazıyor...' } + } + }; + + /* Build */ + const buildPlugin = ([Plugin, Api]) => { + const { ContextMenu, EmulatedTooltip, Toasts, Settings, Popouts, Modals, Utilities, WebpackModules, Filters, DiscordModules, ColorConverter, DOMTools, DiscordClasses, DiscordSelectors, ReactTools, ReactComponents, DiscordAPI, Logger, PluginUpdater, PluginUtilities, DiscordClassModules, Structs } = Api; + const { React, ModalStack, ContextMenuActions, ContextMenuItem, ContextMenuItemsGroup, ReactDOM, ChannelStore, GuildStore, UserStore, DiscordConstants, Dispatcher, GuildMemberStore, GuildActions, SwitchRow, EmojiUtils, RadioGroup, Permissions, TextElement, FlexChild, PopoutOpener, Textbox, RelationshipStore, UserSettingsStore } = DiscordModules; + + const rendererFunctionClass = (() => { + try { + const topContext = require('electron').webFrame.top.context; + if (topContext === window) return null; + return topContext.Function + } catch { + return null; + } + })(); + const originalFunctionClass = Function; + function createSmartPatcher(patcher) { + const createPatcher = patcher => { + return (moduleToPatch, functionName, callback, options = {}) => { + try { + var origDef = moduleToPatch[functionName]; + } catch (_) { + return Logger.error(`Failed to patch ${functionName}`); + } + if (rendererFunctionClass && origDef && !(origDef instanceof originalFunctionClass) && origDef instanceof rendererFunctionClass) window.Function = rendererFunctionClass; + const unpatches = []; + try { + unpatches.push(patcher(moduleToPatch, functionName, callback, options) || DiscordConstants.NOOP); + } catch (err) { + throw err; + } finally { + if (rendererFunctionClass) window.Function = originalFunctionClass; + } + try { + if (origDef && origDef.__isBDFDBpatched && moduleToPatch.BDFDBpatch && typeof moduleToPatch.BDFDBpatch[functionName].originalMethod === 'function') { + /* do NOT patch a patch by ZLIb, that'd be bad and cause double items in context menus */ + if ((Utilities.getNestedProp(ZeresPluginLibrary, 'Patcher.patches') || []).findIndex(e => e.module === moduleToPatch) !== -1 && moduleToPatch.BDFDBpatch[functionName].originalMethod.__originalFunction) return; + unpatches.push(patcher(moduleToPatch.BDFDBpatch[functionName], 'originalMethod', callback, options)); + } + } catch (err) { + Logger.stacktrace('Failed to patch BDFDB patches', err); + } + return function unpatch() { + unpatches.forEach(e => e()); + }; + }; + }; + return Object.assign({}, patcher, { + before: createPatcher(patcher.before), + instead: createPatcher(patcher.instead), + after: createPatcher(patcher.after) + }); + }; + + const Patcher = createSmartPatcher(Api.Patcher); + + const NameUtils = WebpackModules.getByProps('getName'); + + const CUser = WebpackModules.getByPrototypes('getAvatarSource', 'isLocalBot'); + const CChannel = WebpackModules.getByPrototypes('isGroupDM', 'isMultiUserDM'); + const L337 = (() => { + try { + return new CChannel({ id: '1337' }); + } catch (e) { + Logger.stacktrace('Failed to create 1337 channel', e); + } + })(); + + let CTypingUsers = (() => { + try { + const WrappedTypingUsers = WebpackModules.find(m => m.displayName && m.displayName.indexOf('TypingUsers') !== -1); + return new WrappedTypingUsers({ channel: L337 }).render().type; + } catch (e) { + Logger.stacktrace('Failed to get TypingUsers!', e); + return null; + } + })(); + + const ComponentDispatch = (() => { + try { + return WebpackModules.getByProps('ComponentDispatch').ComponentDispatch; + } catch (e) { + Logger.stacktrace('Failed to get ComponentDispatch', e); + } + })(); + + class CTypingUsersPreview extends React.PureComponent { + constructor(props) { + super(props); + this.forceUpdate = this.forceUpdate.bind(this); + const iUsers = UserStore.getUsers(); + for (let i = 0; i < 20; i++) iUsers[(1337 + i).toString()] = new CUser({ username: `User ${i + 1}`, id: (1337 + i).toString(), discriminator: '9999' }); + } + componentDidMount() { + ComponentDispatch.subscribe('BTU_SETTINGS_UPDATED', this.forceUpdate); + } + componentWillUnmount() { + ComponentDispatch.unsubscribe('BTU_SETTINGS_UPDATED', this.forceUpdate); + const iUsers = UserStore.getUsers(); + for (let i = 0; i < 20; i++) delete iUsers[(1337 + i).toString()]; + } + renderTyping(num) { + const typingUsers = {}; + for (let i = 0; i < num; i++) typingUsers[(1337 + i).toString()] = 1; + return React.createElement(CTypingUsers, { + channel: L337, + guildId: '', + isFocused: true, + slowmodeCooldownGuess: 0, + theme: UserSettingsStore.theme, + typingUsers + }); + } + render() { + return React.createElement( + 'div', + { + className: 'BTU-preview' + }, + this.renderTyping(4), + this.renderTyping(6), + this.renderTyping(20) + ); + } + } + + class TypingUsersPreview extends Settings.SettingField { + constructor(name, note) { + super(name, note, null, CTypingUsersPreview); + } + } + + /* since XenoLib is absent from this plugin (since it serves no real purpose), + we can only hope the user doesn't rename the plugin.. + */ + return class BetterTypingUsers extends Plugin { + constructor() { + super(); + try { + WebpackModules.getByProps('openModal', 'hasModalOpen').closeModal(`${this.name}_DEP_MODAL`); + } catch (e) { } + } + onStart() { + this.promises = { state: { cancelled: false } }; + this.patchAll(); + PluginUtilities.addStyle( + this.short + '-CSS', + ` + .BTU-preview > .${WebpackModules.getByProps('slowModeIcon', 'typing').typing.split(' ')[0]} { + position: unset !important; + } + ` + ); + DiscordConstants.MAX_TYPING_USERS = 99; + /* theoretical max is 5 users typing at once.. welp */ + } + + onStop() { + this.promises.state.cancelled = true; + Patcher.unpatchAll(); + PluginUtilities.removeStyle(this.short + '-CSS'); + } + + /* zlib uses reference to defaultSettings instead of a cloned object, which sets settings as default settings, messing everything up */ + loadSettings(defaultSettings) { + return PluginUtilities.loadSettings(this.name, Utilities.deepclone(this.defaultSettings ? this.defaultSettings : defaultSettings)); + } + + buildSetting(data) { + if (data.type === 'preview') return new TypingUsersPreview(data.name, data.note); + return super.buildSetting(data); + } + + saveSettings(_, setting, value) { + super.saveSettings(_, setting, value); + ComponentDispatch.safeDispatch('BTU_SETTINGS_UPDATED'); + } + + filterTypingUsers(typingUsers) { + return Object.keys(typingUsers) + .filter(e => e != DiscordAPI.currentUser.id) + .filter(e => !RelationshipStore.isBlocked(e)) + .map(e => UserStore.getUser(e)) + .filter(e => e != null); + } + + /* PATCHES */ + + patchAll() { + Utilities.suppressErrors(this.patchBetterRoleColors.bind(this), 'BetterRoleColors patch')(); + Utilities.suppressErrors(this.patchTypingUsers.bind(this), 'TypingUsers patch')(this.promises.state); + } + + patchBetterRoleColors() { + const BetterRoleColors = BdApi.Plugins.get('BetterRoleColors'); + if (!BetterRoleColors) return; + /* stop errors */ + /* modify BRCs behavior so it won't unexpectedly try to modify an entry that does not exist + by simply limiting it to the max number of usernames visible in total + */ + Patcher.after(BetterRoleColors, 'filterTypingUsers', (_this, __, ret) => ret.slice(0, this.settings.maxVisible)); + } + + async patchTypingUsers(promiseState) { + const TypingUsers = await ReactComponents.getComponentByName('TypingUsers', DiscordSelectors.Typing.typing); + if (!TypingUsers.selector) TypingUsers.selector = DiscordSelectors.Typing.typing; + const TypingTextClassname = WebpackModules.getByProps('typing', 'text').text.split(' ')[0]; + if (promiseState.cancelled) return; + if (!CTypingUsers) CTypingUsers = typingUsers.component; /* failsafe */ + /* use `instead` so that we modify the return before BetterRoleColors */ + /* Patcher.after(TypingUsers.component.prototype, 'componentDidUpdate', (_this, [props, state], ret) => { + const filtered1 = this.filterTypingUsers(_this.props.typingUsers); + const filtered2 = this.filterTypingUsers(props.typingUsers); + if (filtered1.length !== filtered2.length || _this.state.numLess === state.numLess) { + _this.state.numLess = 0; + _this.triedLess = false; + _this.triedMore = false; + } + }); */ + Patcher.instead(TypingUsers.component.prototype, 'render', (_this, _, orig) => { + /* if (!_this.state) _this.state = { numLess: 0 }; */ + const ret = orig(); + if (!ret) { + /* _this.state.numLess = 0; */ + return ret; + } + const filtered = this.filterTypingUsers(_this.props.typingUsers); + if (filtered.length <= 3) return ret; + /* ret.ref = e => { + _this.__baseRef = e; + if (!e) return; + if (!_this.__textRef) return; + _this.maxWidth = parseInt(getComputedStyle(_this.__baseRef.parentElement).width) - (_this.__textRef.offsetLeft + parseInt(getComputedStyle(_this.__textRef)['margin-left']) - _this.__baseRef.offsetLeft); + if (_this.__textRef.scrollWidth > _this.maxWidth) { + if (_this.triedMore) return; + if (filtered.length - _this.state.numLess <= 3) return; + _this.setState({ numLess: _this.state.numLess + 1 }); + } + }; */ + const typingUsers = Utilities.findInReactTree(ret, e => e && e.props && typeof e.props.className === 'string' && e.props.className.indexOf(TypingTextClassname) !== -1); + if (!typingUsers) return ret; + /* if (typeof _this.state.numLess !== 'number') _this.state.numLess = 0; + typingUsers.ref = e => { + _this.__textRef = e; + }; */ + typingUsers.props.children = []; + /* I don't think this method works for every language..? */ + for (let i = 0; i < filtered.length; i++) { + if (this.settings.maxVisible /* filtered.length - _this.state.numLess */ === i) { + const others = filtered.length - i; + if (others === 1) typingUsers.props.children.push(this.strings.AND_1_OTHER); + else typingUsers.props.children.push(Utilities.formatTString(this.strings.AND_X_OTHERS, { count: others })); + break; + } else if (i === filtered.length - 1) typingUsers.props.children.push(this.strings.AND); + else if (i !== 0) typingUsers.props.children.push(', '); + const name = NameUtils.getName(_this.props.guildId, _this.props.channel.id, filtered[i]); + typingUsers.props.children.push(React.createElement('strong', {}, name)); + } + typingUsers.props.children.push(this.strings.ARE_TYPING); + return ret; + }); + TypingUsers.forceUpdateAll(); + } + + /* PATCHES */ + + getSettingsPanel() { + return this.buildSettingsPanel().getElement(); + } + + get [Symbol.toStringTag]() { + return 'Plugin'; + } + get name() { + return config.info.name; + } + get short() { + let string = ''; + for (let i = 0, len = config.info.name.length; i < len; i++) { + const char = config.info.name[i]; + if (char === char.toUpperCase()) string += char; + } + return string; + } + get author() { + return config.info.authors.map(author => author.name).join(', '); + } + get version() { + return config.info.version; + } + get description() { + return config.info.description; + } + }; + }; + + /* Finalize */ + + let ZeresPluginLibraryOutdated = false; + try { + const a = (c, a) => ((c = c.split('.').map(b => parseInt(b))), (a = a.split('.').map(b => parseInt(b))), !!(a[0] > c[0])) || !!(a[0] == c[0] && a[1] > c[1]) || !!(a[0] == c[0] && a[1] == c[1] && a[2] > c[2]), + b = BdApi.Plugins.get('ZeresPluginLibrary'); + ((b, c) => b && b._config && b._config.info && b._config.info.version && a(b._config.info.version, c))(b, '1.2.27') && (ZeresPluginLibraryOutdated = !0); + } catch (e) { + console.error('Error checking if ZeresPluginLibrary is out of date', e); + } + + return !global.ZeresPluginLibrary || ZeresPluginLibraryOutdated + ? class { + constructor() { + this._config = config; + this.start = this.load = this.handleMissingLib; + } + getName() { + return this.name.replace(/\s+/g, ''); + } + getAuthor() { + return this.author; + } + getVersion() { + return this.version; + } + getDescription() { + return this.description + ' You are missing ZeresPluginLibrary for this plugin, please enable the plugin and click Download Now.'; + } + start() { } + stop() { } + handleMissingLib() { + const a = BdApi.findModuleByProps('openModal', 'hasModalOpen'); + if (a && a.hasModalOpen(`${this.name}_DEP_MODAL`)) return; + const b = !global.ZeresPluginLibrary, + c = ZeresPluginLibraryOutdated ? 'Outdated Library' : 'Missing Library', + d = `The Library ZeresPluginLibrary required for ${this.name} is ${ZeresPluginLibraryOutdated ? 'outdated' : 'missing'}.`, + e = BdApi.findModuleByDisplayName('Text'), + f = BdApi.findModuleByDisplayName('ConfirmModal'), + g = () => BdApi.alert(c, BdApi.React.createElement('span', {}, BdApi.React.createElement('div', {}, d), `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.`, b || ZeresPluginLibraryOutdated ? BdApi.React.createElement('div', {}, BdApi.React.createElement('a', { href: 'https://betterdiscord.net/ghdl?id=2252', target: '_blank' }, 'Click here to download ZeresPluginLibrary')) : null)); + if (!a || !f || !e) return console.error(`Missing components:${(a ? '' : ' ModalStack') + (f ? '' : ' ConfirmationModalComponent') + (e ? '' : 'TextElement')}`), g(); + class h extends BdApi.React.PureComponent { + constructor(a) { + super(a), (this.state = { hasError: !1 }); + } + componentDidCatch(a) { + console.error(`Error in ${this.props.label}, screenshot or copy paste the error above to Lighty for help.`), this.setState({ hasError: !0 }), 'function' == typeof this.props.onError && this.props.onError(a); + } + render() { + return this.state.hasError ? null : this.props.children; + } + } + let i = !1, + j = !1; + const k = a.openModal( + b => { + if (j) return null; + try { + return BdApi.React.createElement( + h, + { + label: 'missing dependency modal', + onError: () => { + a.closeModal(k), g(); + } + }, + BdApi.React.createElement( + f, + Object.assign( + { + header: c, + children: BdApi.React.createElement(e, { size: e.Sizes.SIZE_16, children: [`${d} Please click Download Now to download it.`] }), + red: !1, + confirmText: 'Download Now', + cancelText: 'Cancel', + onCancel: b.onClose, + onConfirm: () => { + if (i) return; + i = !0; + const b = require('request'), + c = require('fs'), + d = require('path'); + b('https://raw.githubusercontent.com/rauenzi/BDPluginLibrary/master/release/0PluginLibrary.plugin.js', (b, e, f) => { + try { + if (b || 200 !== e.statusCode) return a.closeModal(k), g(); + c.writeFile(d.join(BdApi.Plugins && BdApi.Plugins.folder ? BdApi.Plugins.folder : window.ContentManager.pluginsFolder, '0PluginLibrary.plugin.js'), f, () => { }); + } catch (b) { + console.error('Fatal error downloading ZeresPluginLibrary', b), a.closeModal(k), g(); + } + }); + } + }, + b, + { onClose: () => { } } + ) + ) + ); + } catch (b) { + return console.error('There has been an error constructing the modal', b), (j = !0), a.closeModal(k), g(), null; + } + }, + { modalKey: `${this.name}_DEP_MODAL` } + ); + } + get [Symbol.toStringTag]() { + return 'Plugin'; + } + get name() { + return config.info.name; + } + get short() { + let string = ''; + for (let i = 0, len = config.info.name.length; i < len; i++) { + const char = config.info.name[i]; + if (char === char.toUpperCase()) string += char; + } + return string; + } + get author() { + return config.info.authors.map(author => author.name).join(', '); + } + get version() { + return config.info.version; + } + get description() { + return config.info.description; + } + } + : buildPlugin(global.ZeresPluginLibrary.buildPlugin(config)); +})(); + +/*@end@*/ diff --git a/Plugins/BetterUnavailableGuilds.plugin.js b/Plugins/BetterUnavailableGuilds.plugin.js new file mode 100644 index 0000000..6c4fa20 --- /dev/null +++ b/Plugins/BetterUnavailableGuilds.plugin.js @@ -0,0 +1,519 @@ +//META{"name":"BetterUnavailableGuilds","source":"https://github.com/1Lighty/BetterDiscordPlugins/blob/master/Plugins/BetterUnavailableGuilds/","website":"https://1lighty.github.io/BetterDiscordStuff/?plugin=BetterUnavailableGuilds","authorId":"239513071272329217","invite":"NYvWdN5","donate":"https://paypal.me/lighty13"}*// +/*@cc_on +@if (@_jscript) + + // Offer to self-install for clueless users that try to run this directly. + var shell = WScript.CreateObject('WScript.Shell'); + var fs = new ActiveXObject('Scripting.FileSystemObject'); + var pathPlugins = shell.ExpandEnvironmentStrings('%APPDATA%\\BetterDiscord\\plugins'); + var pathSelf = WScript.ScriptFullName; + // Put the user at ease by addressing them in the first person + shell.Popup('It looks like you\'ve mistakenly tried to run me directly. \n(Don\'t do that!)', 0, 'I\'m a plugin for BetterDiscord', 0x30); + if (fs.GetParentFolderName(pathSelf) === fs.GetAbsolutePathName(pathPlugins)) { + shell.Popup('I\'m in the correct folder already.\nJust go to settings, plugins and enable me.', 0, 'I\'m already installed', 0x40); + } else if (!fs.FolderExists(pathPlugins)) { + shell.Popup('I can\'t find the BetterDiscord plugins folder.\nAre you sure it\'s even installed?', 0, 'Can\'t install myself', 0x10); + } else if (shell.Popup('Should I copy myself to BetterDiscord\'s plugins folder for you?', 0, 'Do you need some help?', 0x34) === 6) { + fs.CopyFile(pathSelf, fs.BuildPath(pathPlugins, fs.GetFileName(pathSelf)), true); + // Show the user where to put plugins in the future + shell.Exec('explorer ' + pathPlugins); + shell.Popup('I\'m installed!\nJust go to settings, plugins and enable me!', 0, 'Successfully installed', 0x40); + } + WScript.Quit(); + +@else@*/ +/* + * Copyright © 2020, _Lighty_ + * All rights reserved. + * Code may not be redistributed, modified or otherwise taken without explicit permission. + */ +module.exports = (() => { + /* Setup */ + const config = { + main: 'index.js', + info: { + name: 'BetterUnavailableGuilds', + authors: [ + { + name: 'Lighty', + discord_id: '239513071272329217', + github_username: '1Lighty', + twitter_username: '' + } + ], + version: '0.2.9', + description: 'Force Discord to show server icons of unavailable servers, instead of "1 server is unavailable" and enable interaction with the server (ability to leave the server, move it around, etc).', + github: 'https://github.com/1Lighty', + github_raw: 'https://raw.githubusercontent.com/1Lighty/BetterDiscordPlugins/master/Plugins/BetterUnavailableGuilds/BetterUnavailableGuilds.plugin.js' + }, + changelog: [ + { + title: 'fixed', + type: 'fixed', + items: ['Fixed not restoring the servers on startup.'] + } + ], + defaultConfig: [ + { + type: 'category', + id: 'guilds', + name: 'Guilds', + collapsible: true, + shown: true, + settings: [ + { + type: 'textbox', + name: 'Add guild using data', + note: 'Press enter to add' + }, + { + type: 'guildslist', + name: 'Click to copy guild data to share to people' + } + ] + } + ] + }; + + /* Build */ + const buildPlugin = ([Plugin, Api]) => { + const { Utilities, WebpackModules, DiscordModules, Patcher, PluginUtilities, DiscordAPI, Settings, Toasts } = Api; + const { Dispatcher, GuildStore, React, ModalStack } = DiscordModules; + const GuildAvailabilityStore = WebpackModules.getByProps('unavailableGuilds'); + const _ = WebpackModules.getByProps('bindAll', 'debounce'); + + const FsModule = require('fs'); + // I would say "fuck ED", but it won't even compile on ED due to their piss poor BD/BBD support, lol + const pluginConfigFile = require('path').resolve(BdApi.Plugins.folder, config.info.name + '.config.json'); + + const loadData = (key, defaults) => { + const cloned = _.cloneDeep(defaults); + try { + if (pluginConfigFile) { + if (FsModule.existsSync(pluginConfigFile)) { + return Object.assign(cloned, JSON.parse(FsModule.readFileSync(pluginConfigFile))[key]); + } else { + return cloned; + } + } else { + return Object.assign(cloned, BdApi.loadData(config.info.name, key)); + } + } catch (e) { + return cloned; + } + }; + + const copyToClipboard = WebpackModules.getByProps('copy').copy; /* Possible error in future, TODO: safeguard */ + const GuildIconWrapper = WebpackModules.getByDisplayName('GuildIconWrapper'); + const ListClassModule = WebpackModules.getByProps('listRowContent', 'listAvatar'); + const ListScrollerClassname = WebpackModules.getByProps('listScroller').listScroller; /* Possible error in future, TODO: safeguard */ + const VerticalScroller = WebpackModules.getByDisplayName('VerticalScroller'); + const Clickable = WebpackModules.getByDisplayName('Clickable'); + + /* TODO: proper name for the classes */ + class GL extends React.PureComponent { + renderGuild(guild) { + if (!guild) return null; + return React.createElement( + Clickable, + { + onClick: () => { + copyToClipboard(JSON.stringify({ id: guild.id, icon: guild.icon || undefined, name: guild.name, owner_id: guild.ownerId, joined_at: guild.joinedAt.valueOf() })); + Toasts.success(`Copied ${guild.name}!`); + }, + className: 'BUG-guild-icon' + }, + React.createElement(GuildIconWrapper, { + guild, + showBadge: !0, + className: !guild.icon ? ListClassModule.guildAvatarWithoutIcon : '', + size: GuildIconWrapper.Sizes.LARGE + }) + ); + } + render() { + return React.createElement( + VerticalScroller, + { + fade: true, + className: ListScrollerClassname, + style: { + display: 'flex', + flexDirection: 'row', + flexWrap: 'wrap' + } + }, + Object.values(GuildStore.getGuilds()).map(this.renderGuild) + ); + } + } + + class TB extends DiscordModules.Textbox { + render() { + const ret = super.render(); + const props = Utilities.findInReactTree(ret, e => e && e.onEnterPressed); + props.onKeyDown = e => { + if (e.keyCode !== 13) return; + try { + const parsed = JSON.parse(this.props.value); + ['id', 'name', 'owner_id', 'joined_at'].forEach(prop => { + if (!parsed.hasOwnProperty(prop) || typeof parsed[prop] === 'undefined') throw `Malformed guild data (${prop})`; + }); + if (typeof parsed.name !== 'string' || typeof parsed.owner_id !== 'string' || /\\d+$/.test(parsed.owner_id)) throw 'Malformed guild data'; + this.props.onEnterPressed(parsed); + this.props.onChange(''); + } catch (err) { + Toasts.error(`Failed to parse: ${err.message || err}`); + } + }; + return ret; + } + } + + class Textbox extends Settings.SettingField { + constructor(name, note, onChange, options = {}) { + super(name, note, () => { }, TB, { + onChange: textbox => value => { + textbox.props.value = value; + textbox.forceUpdate(); + }, + value: '', + placeholder: options.placeholder ? options.placeholder : '', + onEnterPressed: onChange + }); + } + } + + class GuildDataCopier extends Settings.SettingField { + constructor(name, note) { + super(name, note, () => { }, GL, {}); + } + } + + return class BetterUnavailableGuilds extends Plugin { + constructor() { + super(); + try { + WebpackModules.getByProps('openModal', 'hasModalOpen').closeModal(`${this.name}_DEP_MODAL`); + } catch (e) { } + this._dispatches = ['CONNECTION_OPEN']; + _.bindAll(this, ['handleGuildStoreChange', 'verifyAllServersCachedInClient', ...this._dispatches]); + // different timings for clients to avoid fighting over a damn config file + this.handleGuildStoreChange = _.throttle(this.handleGuildStoreChange, 15000 + (GLOBAL_ENV.RELEASE_CHANNEL === 'ptb' ? 2500 : GLOBAL_ENV.RELEASE_CHANNEL === 'canary' ? 5000 : 0)); + } + onStart() { + this._guildRecord = loadData('data', { data: {} }).data; + this.verifyAllServersCachedInClient(); + GuildStore.addChangeListener(this.handleGuildStoreChange); + this.handleGuildStoreChange(); + this.patchAll(); + for (const dispatch of this._dispatches) Dispatcher.subscribe(dispatch, this[dispatch]); + PluginUtilities.addStyle( + this.short + '-CSS', + ` + .BUG-guild-icon { + padding: 5px; + } + .BUG-guild-icon:hover { + background-color: var(--background-modifier-hover); + } + ` + ); + } + onStop() { + GuildStore.removeChangeListener(this.handleGuildStoreChange); + Patcher.unpatchAll(); + Dispatcher._computeOrderedActionHandlers('GUILD_DELETE'); + for (const dispatch of this._dispatches) Dispatcher.unsubscribe(dispatch, this[dispatch]); + PluginUtilities.removeStyle(this.short + '-CSS'); + } + + buildSetting(data) { + if (data.type === 'textbox') { + const { name, note } = data; + const setting = new Textbox( + name, + note, + guild => { + if (this.guildRecord[guild.id]) throw 'Guild already exists'; + if (GuildAvailabilityStore.unavailableGuilds.indexOf(guild.id)) throw 'You are not a member of ' + guild.name; + this.guildRecord[guild.id] = { id: guild.id, icon: guild.icon || undefined, name: guild.name, owner_id: guild.owner_id, joined_at: guild.joined_at, default_message_notifications: guild.default_message_notifications }; + this.verifyAllServersCachedInClient(); + Toasts.success('Added!'); + }, + { placeholder: data.placeholder || '' } + ); + return setting; + } else if (data.type === 'guildslist') { + return new GuildDataCopier(data.name, data.note); + } + return super.buildSetting(data); + } + + verifyAllServersCachedInClient() { + if (!DiscordAPI.currentUser) return; /* hhhhhhhh */ + Dispatcher.wait(() => { + this.ensureBDGuildsPreCached(); + this._verifying = true; + const unavailable = _.cloneDeep(GuildAvailabilityStore.unavailableGuilds); + unavailable.forEach(guildId => { + if (!this.guildRecord[guildId] || GuildStore.getGuild(guildId)) return; + Dispatcher.dispatch({ + type: 'GUILD_CREATE', + guild: Object.assign( + { + icon: null, + presences: [], + channels: [], + members: [], + roles: [], + unavailable: true + }, + this.guildRecord[guildId] + ) + }); + /* they're still unavailable, remember? */ + Dispatcher.dispatch({ + type: 'GUILD_UNAVAILABLE', + guildId: guildId + }); + }); + this._verifying = false; + }); + } + + CONNECTION_OPEN(e) { + /* websocket died, user logged in, user logged into another account etc */ + this.verifyAllServersCachedInClient(); + } + + ensureBDGuildsPreCached() { + this.guildRecord['86004744966914048'] = { id: '86004744966914048', icon: '292e7f6bfff2b71dfd13e508a859aedd', name: 'BetterDiscord', owner_id: '81388395867156480', joined_at: Date.now() }; + this.guildRecord['280806472928198656'] = { id: '280806472928198656', icon: 'cbdda04c041699d80689b99c4e5e89dc', name: 'BetterDiscord2', owner_id: '81388395867156480', joined_at: Date.now() }; + } + + ensureDataSettable() { + if (!this._guildRecord[DiscordAPI.currentUser.id]) this._guildRecord[DiscordAPI.currentUser.id] = {}; + if (!this._guildRecord[DiscordAPI.currentUser.id][GLOBAL_ENV.RELEASE_CHANNEL]) { + const curUserShit = this._guildRecord[DiscordAPI.currentUser.id]; + /* transfer the data */ + if (curUserShit['stable']) curUserShit[GLOBAL_ENV.RELEASE_CHANNEL] = _.cloneDeep(curUserShit['stable']); + else if (curUserShit['ptb']) curUserShit[GLOBAL_ENV.RELEASE_CHANNEL] = _.cloneDeep(curUserShit['ptb']); + else if (curUserShit['canary']) curUserShit[GLOBAL_ENV.RELEASE_CHANNEL] = _.cloneDeep(curUserShit['canary']); + else curUserShit[GLOBAL_ENV.RELEASE_CHANNEL] = {}; + } + } + + get guildRecord() { + this.ensureDataSettable(); + const ret = this._guildRecord[DiscordAPI.currentUser.id][GLOBAL_ENV.RELEASE_CHANNEL]; + return ret; + } + set guildRecord(val) { + this.ensureDataSettable(); + return (this._guildRecord[DiscordAPI.currentUser.id][GLOBAL_ENV.RELEASE_CHANNEL] = val); + } + + handleGuildStoreChange() { + if (!DiscordAPI.currentUser) return; /* hhhhhhhh */ + this._guildRecord = loadData('data', { data: {} }).data; + this.verifyAllServersCachedInClient(); + const availableGuilds = Object.values(GuildStore.getGuilds()).map(guild => ({ + id: guild.id, + icon: guild.icon || undefined, + name: guild.name, + owner_id: guild.ownerId, + joined_at: guild.joinedAt ? guild.joinedAt.valueOf() /* int value is fine too */ : 0 /* wut? MasicoreLord experienced a weird bug with joinedAt being undefined */ + })); + let guilds = {}; + GuildAvailabilityStore.unavailableGuilds.forEach(id => this.guildRecord[id] && (guilds[id] = this.guildRecord[id])); + availableGuilds.forEach(guild => (guilds[guild.id] = guild)); + for (const guildId in guilds) guilds[guildId] = _.pickBy(guilds[guildId], e => !_.isUndefined(e)); + if (!_.isEqual(this.guildRecord, guilds)) { + this.guildRecord = guilds; + PluginUtilities.saveData(this.name, 'data', { data: this._guildRecord }); + } + } + + /* PATCHES */ + + patchAll() { + Utilities.suppressErrors(this.patchGuildDelete.bind(this), 'GUILD_DELETE dispatch patch')(); + } + + patchGuildDelete() { + // super sekret (not really) V3/rewrite patch code + for (const id in Dispatcher._dependencyGraph.nodes) { + const node = Dispatcher._dependencyGraph.nodes[id]; + if (!node.actionHandler['GUILD_DELETE']) continue; + Patcher.instead(node.actionHandler, 'GUILD_DELETE', (_, [dispatch], orig) => { + if (!dispatch.guild.unavailable) return orig(dispatch); + }); + } + Dispatcher._computeOrderedActionHandlers('GUILD_DELETE'); + } + + /* PATCHES */ + + getSettingsPanel() { + return this.buildSettingsPanel().getElement(); + } + + get [Symbol.toStringTag]() { + return 'Plugin'; + } + get name() { + return config.info.name; + } + get short() { + let string = ''; + for (let i = 0, len = config.info.name.length; i < len; i++) { + const char = config.info.name[i]; + if (char === char.toUpperCase()) string += char; + } + return string; + } + get author() { + return config.info.authors.map(author => author.name).join(', '); + } + get version() { + return config.info.version; + } + get description() { + return config.info.description; + } + }; + }; + + /* Finalize */ + + let ZeresPluginLibraryOutdated = false; + try { + const a = (c, a) => ((c = c.split('.').map(b => parseInt(b))), (a = a.split('.').map(b => parseInt(b))), !!(a[0] > c[0])) || !!(a[0] == c[0] && a[1] > c[1]) || !!(a[0] == c[0] && a[1] == c[1] && a[2] > c[2]), + b = BdApi.Plugins.get('ZeresPluginLibrary'); + ((b, c) => b && b._config && b._config.info && b._config.info.version && a(b._config.info.version, c))(b, '1.2.27') && (ZeresPluginLibraryOutdated = !0); + } catch (e) { + console.error('Error checking if ZeresPluginLibrary is out of date', e); + } + + return !global.ZeresPluginLibrary || ZeresPluginLibraryOutdated + ? class { + constructor() { + this._config = config; + this.start = this.load = this.handleMissingLib; + } + getName() { + return this.name.replace(/\s+/g, ''); + } + getAuthor() { + return this.author; + } + getVersion() { + return this.version; + } + getDescription() { + return this.description + ' You are missing ZeresPluginLibrary for this plugin, please enable the plugin and click Download Now.'; + } + start() { } + stop() { } + handleMissingLib() { + const a = BdApi.findModuleByProps('openModal', 'hasModalOpen'); + if (a && a.hasModalOpen(`${this.name}_DEP_MODAL`)) return; + const b = !global.ZeresPluginLibrary, + c = ZeresPluginLibraryOutdated ? 'Outdated Library' : 'Missing Library', + d = `The Library ZeresPluginLibrary required for ${this.name} is ${ZeresPluginLibraryOutdated ? 'outdated' : 'missing'}.`, + e = BdApi.findModuleByDisplayName('Text'), + f = BdApi.findModuleByDisplayName('ConfirmModal'), + g = () => BdApi.alert(c, BdApi.React.createElement('span', {}, BdApi.React.createElement('div', {}, d), `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.`, b || ZeresPluginLibraryOutdated ? BdApi.React.createElement('div', {}, BdApi.React.createElement('a', { href: 'https://betterdiscord.net/ghdl?id=2252', target: '_blank' }, 'Click here to download ZeresPluginLibrary')) : null)); + if (!a || !f || !e) return console.error(`Missing components:${(a ? '' : ' ModalStack') + (f ? '' : ' ConfirmationModalComponent') + (e ? '' : 'TextElement')}`), g(); + class h extends BdApi.React.PureComponent { + constructor(a) { + super(a), (this.state = { hasError: !1 }); + } + componentDidCatch(a) { + console.error(`Error in ${this.props.label}, screenshot or copy paste the error above to Lighty for help.`), this.setState({ hasError: !0 }), 'function' == typeof this.props.onError && this.props.onError(a); + } + render() { + return this.state.hasError ? null : this.props.children; + } + } + let i = !1, + j = !1; + const k = a.openModal( + b => { + if (j) return null; + try { + return BdApi.React.createElement( + h, + { + label: 'missing dependency modal', + onError: () => { + a.closeModal(k), g(); + } + }, + BdApi.React.createElement( + f, + Object.assign( + { + header: c, + children: BdApi.React.createElement(e, { size: e.Sizes.SIZE_16, children: [`${d} Please click Download Now to download it.`] }), + red: !1, + confirmText: 'Download Now', + cancelText: 'Cancel', + onCancel: b.onClose, + onConfirm: () => { + if (i) return; + i = !0; + const b = require('request'), + c = require('fs'), + d = require('path'); + b('https://raw.githubusercontent.com/rauenzi/BDPluginLibrary/master/release/0PluginLibrary.plugin.js', (b, e, f) => { + try { + if (b || 200 !== e.statusCode) return a.closeModal(k), g(); + c.writeFile(d.join(BdApi.Plugins && BdApi.Plugins.folder ? BdApi.Plugins.folder : window.ContentManager.pluginsFolder, '0PluginLibrary.plugin.js'), f, () => { }); + } catch (b) { + console.error('Fatal error downloading ZeresPluginLibrary', b), a.closeModal(k), g(); + } + }); + } + }, + b, + { onClose: () => { } } + ) + ) + ); + } catch (b) { + return console.error('There has been an error constructing the modal', b), (j = !0), a.closeModal(k), g(), null; + } + }, + { modalKey: `${this.name}_DEP_MODAL` } + ); + } + get [Symbol.toStringTag]() { + return 'Plugin'; + } + get name() { + return config.info.name; + } + get short() { + let string = ''; + for (let i = 0, len = config.info.name.length; i < len; i++) { + const char = config.info.name[i]; + if (char === char.toUpperCase()) string += char; + } + return string; + } + get author() { + return config.info.authors.map(author => author.name).join(', '); + } + get version() { + return config.info.version; + } + get description() { + return config.info.description; + } + } + : buildPlugin(global.ZeresPluginLibrary.buildPlugin(config)); +})(); + +/*@end@*/ diff --git a/Plugins/CrashRecovery.plugin.js b/Plugins/CrashRecovery.plugin.js new file mode 100644 index 0000000..ac6b96a --- /dev/null +++ b/Plugins/CrashRecovery.plugin.js @@ -0,0 +1,527 @@ +//META{"name":"CrashRecovery","source":"https://github.com/1Lighty/BetterDiscordPlugins/blob/master/Plugins/CrashRecovery/","website":"https://1lighty.github.io/BetterDiscordStuff/?plugin=CrashRecovery","authorId":"239513071272329217","invite":"NYvWdN5","donate":"https://paypal.me/lighty13"}*// +/*@cc_on +@if (@_jscript) + // Offer to self-install for clueless users that try to run this directly. + var shell = WScript.CreateObject('WScript.Shell'); + var fs = new ActiveXObject('Scripting.FileSystemObject'); + var pathPlugins = shell.ExpandEnvironmentStrings('%APPDATA%\\BetterDiscord\\plugins'); + var pathSelf = WScript.ScriptFullName; + // Put the user at ease by addressing them in the first person + shell.Popup('It looks like you\'ve mistakenly tried to run me directly. \n(Don\'t do that!)', 0, 'I\'m a plugin for BetterDiscord', 0x30); + if (fs.GetParentFolderName(pathSelf) === fs.GetAbsolutePathName(pathPlugins)) { + shell.Popup('I\'m in the correct folder already.\nJust go to settings, plugins and enable me.', 0, 'I\'m already installed', 0x40); + } else if (!fs.FolderExists(pathPlugins)) { + shell.Popup('I can\'t find the BetterDiscord plugins folder.\nAre you sure it\'s even installed?', 0, 'Can\'t install myself', 0x10); + } else if (shell.Popup('Should I copy myself to BetterDiscord\'s plugins folder for you?', 0, 'Do you need some help?', 0x34) === 6) { + fs.CopyFile(pathSelf, fs.BuildPath(pathPlugins, fs.GetFileName(pathSelf)), true); + // Show the user where to put plugins in the future + shell.Exec('explorer ' + pathPlugins); + shell.Popup('I\'m installed!\nJust go to settings, plugins and enable me!', 0, 'Successfully installed', 0x40); + } + WScript.Quit(); +@else@*/ +/* + * Copyright © 2019-2020, _Lighty_ + * All rights reserved. + * Code may not be redistributed, modified or otherwise taken without explicit permission. + */ +module.exports = (() => { + /* Setup */ + const config = { + main: 'index.js', + info: { + name: 'CrashRecovery', + authors: [ + { + name: 'Lighty', + discord_id: '239513071272329217', + github_username: '1Lighty', + twitter_username: '' + } + ], + version: '1.0.3', + description: 'In the event that your Discord crashes, the plugin enables you to get Discord back to a working state, without needing to reload at all.', + github: 'https://github.com/1Lighty', + github_raw: 'https://raw.githubusercontent.com/1Lighty/BetterDiscordPlugins/master/Plugins/CrashRecovery/CrashRecovery.plugin.js' + }, + changelog: [ + { + title: 'RIP BBD on Canary', + type: 'fixed', + items: ['Implemented fixes that allow patches to work properly on canary using Powercord.'] + } + ], + defaultConfig: [ + { + name: 'Enable step 3', + note: 'Moves channel switch to a third and last step, otherwise it switches on step 2', + id: 'useThirdStep', + type: 'switch', + value: true + } + ] + }; + + /* Build */ + const buildPlugin = ([Plugin, Api], BasePlugin) => { + const { Logger, Utilities, WebpackModules, DiscordModules, PluginUtilities, ReactTools, PluginUpdater } = Api; + const { React, Dispatcher, FlexChild: Flex, GuildStore } = DiscordModules; + + const Patcher = XenoLib.createSmartPatcher(Api.Patcher); + + const DelayedCall = (WebpackModules.getByProps('DelayedCall') || {}).DelayedCall; + const ElectronDiscordModule = WebpackModules.getByProps('cleanupDisplaySleep') || { cleanupDisplaySleep: DiscordModules.DiscordConstants.NOOP }; + + const ModalStack = WebpackModules.getByProps('openModal'); + + const isPowercord = !!window.powercord; + const BLACKLISTED_BUILTIN_PC_PLUGINS = ['Updater', 'Commands Manager', 'I18n', 'Module Manager', 'Settings'] + const RE_PC_PLUGIN_NAME_FROM_PATH = /[\\\/]plugins[\\\/]([^\\\/]+)/; + + const RE_INVARIANT = /error-decoder.html\?invariant=(\d+)([^\s]*)/; + const INVARIANTS_URL = 'https://raw.githubusercontent.com/facebook/react/master/scripts/error-codes/codes.json'; + const ROOT_FOLDER = isPowercord ? window.powercord.basePath : BdApi.Plugins.folder; + + const path = require('path'); + + return class CrashRecovery extends BasePlugin(Plugin) { + constructor() { + super(); + XenoLib.changeName(__filename, 'CrashRecovery'); + this._startFailure = message => { + PluginUpdater.checkForUpdate(this.name, this.version, this._config.info.github_raw); + XenoLib.Notifications.error(`[**${this.name}**] ${message} Please update it, press CTRL + R, or ${GuildStore.getGuild(XenoLib.supportServerId) ? 'go to <#639665366380838924>' : '[join my support server](https://discord.gg/NYvWdN5)'} for further assistance.`, { timeout: 0 }); + }; + const oOnStart = this.onStart.bind(this); + this.onStart = () => { + try { + oOnStart(); + } catch (e) { + Logger.stacktrace('Failed to start!', e); + this._startFailure('Failed to start!'); + try { + this.onStop(); + } catch (e) { } + } + }; + try { + WebpackModules.getByProps('openModal', 'hasModalOpen').closeModal(`${this.name}_DEP_MODAL`); + } catch (e) { } + } + onStart() { + this.attempts = 0; + this.promises = { state: { cancelled: false } }; + if (!DelayedCall) return this._startFailure('DelayedCall missing, plugin cannot function.'); + if (ElectronDiscordModule.cleanupDisplaySleep === DiscordModules.DiscordConstants.NOOP) XenoLib.Notifications.error(`[**${this.name}**] cleanupDisplaySleep is missing.`); + delete this.onCrashRecoveredDelayedCall; + this.onCrashRecoveredDelayedCall = new DelayedCall(1000, () => { + XenoLib.Notifications.remove(this.notificationId); + this.notificationId = null; + if (this.disabledPlugins) XenoLib.Notifications.danger(`${this.disabledPlugins.map(e => e)} ${this.disabledPlugins.length > 1 ? 'have' : 'has'} been disabled to recover from the crash`, { timeout: 0 }); + if (this.suspectedPlugin) XenoLib.Notifications.danger(`${this.suspectedPlugin} ${this.suspectedPlugin2 !== this.suspectedPlugin && this.suspectedPlugin2 ? 'or ' + this.suspectedPlugin2 : ''} is suspected of causing the crash.`, { timeout: 10000 }); + XenoLib.Notifications.info('Successfully recovered, more info can be found in the console (CTRL + SHIFT + I > console on top). Pass this information to support for further help.', { timeout: 10000 }); + this.disabledPlugins = null; + this.suspectedPlugin = null; + this.suspectedPlugin2 = null; + this.attempts = 0; + const appMount = document.querySelector('#app-mount'); + appMount.append(document.querySelector('.xenoLib-notifications')); + const BIVOverlay = document.querySelector('.biv-overlay'); + if (BIVOverlay) appMount.append(BIVOverlay); + Logger.info('Corrected incorrectly placed containers'); + }); + Error.prepareStackTrace = (error, frames) => { + this._lastStackFrames = frames; + return error.stack + } + Utilities.suppressErrors(this.patchErrorBoundary.bind(this))(this.promises.state); + if (!this.settings.lastErrorMapUpdate || Date.now() - this.settings.lastErrorMapUpdate > 2.628e+9) { + const https = require('https'); + const req = https.request(INVARIANTS_URL, { headers: { 'origin': 'discord.com' } }, res => { + let body = ''; + res.on('data', chunk => { body += chunk; }); + res.on('end', () => { + if (res.statusCode !== 200) return; + try { + this.settings.errorMap = JSON.parse(body); + this.settings.lastErrorMapUpdate = Date.now(); + this.saveSettings(); + } catch { } + }); + }); + req.end(); + } + } + onStop() { + this.promises.state.cancelled = true; + Patcher.unpatchAll(); + if (this.notificationId) XenoLib.Notifications.remove(this.notificationId); + delete Error.prepareStackTrace; + } + + + // Copyright (c) Facebook, Inc. and its affiliates + // https://github.com/facebook/react/blob/master/scripts/jest/setupTests.js#L171 + decodeErrorMessage(message) { + if (!message) return message; + + const [, invariant, argS] = message.match(RE_INVARIANT); + const code = parseInt(invariant, 10); + const args = argS + .split('&') + .filter(s => s.indexOf('args[]=') !== -1) + .map(s => s.substr('args[]='.length)) + .map(decodeURIComponent); + const format = this.settings.errorMap[code]; + if (!format) return message; // ouch + let argIndex = 0; + return format.replace(/%s/g, () => args[argIndex++]); + } + + decodeStacks(stack, componentStack, baseComponentName = 'ErrorBoundary') { + if (!this.settings.errorMap) return { stack, componentStack }; + if (RE_INVARIANT.test(stack)) stack = this.decodeErrorMessage(stack); + // Strip out Discord (and React) only functions + else stack = stack.split('\n') + .filter(l => l.indexOf('discordapp.com/assets') === -1 && l.indexOf('discord.com/assets') === -1) + .join('\n') + .split(ROOT_FOLDER) // transform paths to relative + .join(''); + + // Only show up to the error boundary + const splitComponentStack = componentStack.split('\n').filter(e => e); + const stackEnd = splitComponentStack.findIndex(l => l.indexOf(`in ${baseComponentName}`) !== -1); + if (stackEnd !== -1 && baseComponentName) splitComponentStack.splice(stackEnd + 1, splitComponentStack.length); + componentStack = splitComponentStack.join('\n'); + return { stack, componentStack }; + } + + /* + MUST return either an array with the plugin name + or a string of the plugin name + */ + queryResponsiblePlugins() { + try { + const stack = this._bLastStackFrames + // filter out blank functions (like from console or whatever) + .filter(e => e.getFileName() && (e.getFunctionName() || e.getMethodName())) + // filter out discord functions + .filter(e => e.getFileName().indexOf(ROOT_FOLDER) !== -1) + // convert CallSites to only useful info + .map(e => ({ filename: e.getFileName(), functionName: e.getFunctionName() || e.getMethodName() })) + // filter out ZeresPluginLibrary and ourselves + .filter(({ filename }) => filename.lastIndexOf('0PluginLibrary.plugin.js') !== filename.length - 24 && filename.lastIndexOf(`${this.name}.plugin.js`) !== filename.length - (this.name.length + 10)); + // Filter out MessageLoggerV2 dispatch patch, is a 2 part step + for (let i = 0, len = stack.length; i < len; i++) { + const { filename, functionName } = stack[i]; + if (filename.lastIndexOf('MessageLoggerV2.plugin.js') !== filename.length - 25) continue; + if (functionName !== 'onDispatchEvent') continue; + if (stack[i + 1].functionName !== 'callback') break; + stack.splice(i, 2); + break; + } + const plugins = stack.map(({ filename }) => { + try { + const bdname = path.basename(filename); + if (bdname.indexOf('.plugin.js') === bdname.length - 10) { + const [pluginName] = bdname.split('.plugin.js'); + if (BdApi.Plugins.get(pluginName)) return pluginName; + /* + * go away Zack + */ + for (const path in require.cache) { + const module = require.cache[path]; + if (!module || !module.exports || !module.exports.plugin || module.exports.filename.indexOf(bdname) !== 0) continue; + return module.exports.id; + } + return null; + } + else if (isPowercord) { + const [, pcname] = RE_PC_PLUGIN_NAME_FROM_PATH.exec(filename); + const { pluginManager } = window.powercord; + const plugin = pluginManager.get(pcname); + if (!plugin) return null; + const { name } = plugin.manifest; + if (BLACKLISTED_BUILTIN_PC_PLUGINS.indexOf(name) !== -1) return null; + return pcname; + } + } catch (err) { + Logger.stacktrace('Error fetching plugin') + } + }).filter((name, idx, self) => name && self.indexOf(name) === idx); + const ret = []; + for (let i = 0, len = plugins.length; i < len; i++) { + const name = plugins[i]; + if (this.disabledPlugins && this.disabledPlugins.indexOf(name) !== -1) return name; + ret.push(name); + } + return ret; + } catch (e) { + Logger.stacktrace('query error', e); + return null; + } + } + + cleanupDiscord() { + ElectronDiscordModule.cleanupDisplaySleep(); + Dispatcher.wait(() => { + try { + DiscordModules.ContextMenuActions.closeContextMenu(); + } catch (err) { + Logger.stacktrace('Failed to close all context menus', err); + } + try { + DiscordModules.ModalStack.popAll(); + } catch (err) { + Logger.stacktrace('Failed to pop old modalstack', err); + } + try { + DiscordModules.LayerManager.popAllLayers(); + } catch (err) { + Logger.stacktrace('Failed to pop all layers', err); + } + try { + DiscordModules.PopoutStack.closeAll(); + } catch (err) { + Logger.stacktrace('Failed to close all popouts', err); + } + try { + (ModalStack.modalsApi || ModalStack.useModalsStore).setState(() => ({ default: [] })); /* slow? unsafe? async? */ + } catch (err) { + Logger.stacktrace('Failed to pop new modalstack'); + } + try { + if (!this.settings.useThirdStep) DiscordModules.NavigationUtils.transitionTo('/channels/@me'); + } catch (err) { + Logger.stacktrace('Failed to transition to home'); + } + }); + } + + handleCrash(_this, stack, componentStack, isRender = false) { + this._bLastStackFrames = this._lastStackFrames; + try { + const decoded = this.decodeStacks(stack, componentStack); + Logger.error('HEY OVER HERE! Show this to the plugin developer or in the support server!\nPrettified stacktraces, stack:\n', decoded.stack, '\nComponent stack:\n', decoded.componentStack); + } catch (err) { + Logger.stacktrace('Failed decoding stack!', err); + } + this.onCrashRecoveredDelayedCall.cancel(); + if (!isRender) { + _this.setState({ + error: { stack } + }); + } + if (!this.notificationId) { + this.notificationId = XenoLib.Notifications.danger('Crash detected, attempting recovery', { timeout: 0, loading: true }); + } + const responsiblePlugins = this.queryResponsiblePlugins(); + if (responsiblePlugins && !Array.isArray(responsiblePlugins)) { + XenoLib.Notifications.update(this.notificationId, { content: `Failed to recover from crash, ${responsiblePlugins} is not stopping properly`, loading: false }); + return; + } + if (!this.attempts) { + this.cleanupDiscord(); + if (responsiblePlugins) this.suspectedPlugin = responsiblePlugins.shift(); + } + if (this.setStateTimeout) return; + if (this.attempts >= 10 || ((this.settings.useThirdStep ? this.attempts >= 3 : this.attempts >= 2) && (!responsiblePlugins || !responsiblePlugins[0]))) { + XenoLib.Notifications.update(this.notificationId, { content: 'Failed to recover from crash', loading: false }); + return; + } + if (this.attempts === 1) XenoLib.Notifications.update(this.notificationId, { content: 'Failed, trying again' }); + else if (this.settings.useThirdStep && this.attempts === 2) { + Dispatcher.wait(() => DiscordModules.NavigationUtils.transitionTo('/channels/@me')); + XenoLib.Notifications.update(this.notificationId, { content: `Failed, switching channels` }); + } else if (this.attempts >= 2) { + const [name] = responsiblePlugins; + try { + if (BdApi.Plugins.get(name)) BdApi.Plugins.disable(name); + else if (isPowercord) window.powercord.pluginManager.disable(name); + } catch (e) { } + XenoLib.Notifications.update(this.notificationId, { content: `Failed, suspecting ${name} for recovery failure` }); + if (!this.disabledPlugins) this.disabledPlugins = []; + this.disabledPlugins.push(name); + } + this.setStateTimeout = setTimeout(() => { + this.setStateTimeout = null; + this.attempts++; + this.onCrashRecoveredDelayedCall.delay(); + _this.setState({ + error: null, + info: null + }); + }, 1000); + } + + /* PATCHES */ + + patchErrorBoundary() { + const ErrorBoundary = WebpackModules.getByDisplayName('ErrorBoundary'); + Patcher.instead(ErrorBoundary.prototype, 'componentDidCatch', (_this, [{ stack }, { componentStack }], orig) => { + this.handleCrash(_this, stack, componentStack); + }); + Patcher.after(ErrorBoundary.prototype, 'render', (_this, _, ret) => { + if (!_this.state.error) return; + if (!this.notificationId) { + this.handleCrash(_this, _this.state.error.stack, _this.state.info.componentStack, true); + } + /* better be safe than sorry! */ + if (!_this.state.customPageError) { + ret.props.action = React.createElement( + XenoLib.ReactComponents.ErrorBoundary, + { label: 'ErrorBoundary patch', onError: () => _this.setState({ customPageError: true /* sad day.. */ }) }, + React.createElement( + Flex, + { + grow: 0, + direction: Flex.Direction.HORIZONTAL + }, + React.createElement( + XenoLib.ReactComponents.Button, + { + size: XenoLib.ReactComponents.ButtonOptions.ButtonSizes.LARGE, + style: { + marginRight: 20 + }, + onClick: () => { + this.attempts = 0; + this.disabledPlugins = null; + XenoLib.Notifications.update(this.notificationId, { content: 'If you say so.. trying again', loading: true }); + _this.setState({ + error: null, + info: null + }); + } + }, + 'Recover' + ), + React.createElement( + XenoLib.ReactComponents.Button, + { + size: XenoLib.ReactComponents.ButtonOptions.ButtonSizes.LARGE, + style: { + marginRight: 20 + }, + onClick: () => window.location.reload(true) + }, + 'Reload' + ) + ) + ); + } + ret.props.note = [ + React.createElement('div', {}, 'Discord has crashed!'), + this.suspectedPlugin ? React.createElement('div', {}, this.suspectedPlugin, this.suspectedPlugin2 && this.suspectedPlugin2 !== this.suspectedPlugin ? [' or ', this.suspectedPlugin2] : false, ' is likely responsible for the crash') : this.suspectedPlugin2 ? React.createElement('div', {}, this.suspectedPlugin2, ' is likely responsible for the crash') : React.createElement('div', {}, 'Plugin responsible for crash is unknown'), + this.disabledPlugins && this.disabledPlugins.length + ? React.createElement( + 'div', + {}, + this.disabledPlugins.map((e, i) => `${i === 0 ? '' : ', '}${e}`), + this.disabledPlugins.length > 1 ? ' have' : ' has', + ' been disabled in an attempt to recover' + ) + : false + ]; + }); + const ErrorBoundaryInstance = ReactTools.getOwnerInstance(document.querySelector(`.${XenoLib.getSingleClass('errorPage')}`) || document.querySelector('#app-mount > svg:first-of-type'), { include: ['ErrorBoundary'] }); + ErrorBoundaryInstance.state.customPageError = false; + ErrorBoundaryInstance.forceUpdate(); + } + showChangelog = () => XenoLib.showChangelog(`${this.name} has been updated!`, this.version, this._config.changelog); + getSettingsPanel = () => + this.buildSettingsPanel() + .append(new XenoLib.Settings.PluginFooter(() => this.showChangelog())) + .getElement(); + }; + }; + + /* Finalize */ + + /* shared getters */ + const BasePlugin = cl => + class extends cl { + constructor() { + super(); + Object.defineProperties(this, { + name: { get: () => config.info.name }, + short: { get: () => config.info.name.split('').reduce((acc, char) => acc + (char === char.toUpperCase() ? char : '')) }, + author: { get: () => config.info.authors.map(author => author.name).join(', ') }, + version: { get: () => config.info.version }, + description: { get: () => config.info.description } + }); + } + }; + + /* this new lib loader is lit */ + let ZeresPluginLibraryOutdated = false; + let XenoLibOutdated = false; + try { + const a = (c, a) => ((c = c.split('.').map(b => parseInt(b))), (a = a.split('.').map(b => parseInt(b))), !!(a[0] > c[0])) || !!(a[0] == c[0] && a[1] > c[1]) || !!(a[0] == c[0] && a[1] == c[1] && a[2] > c[2]), + b = (b, c) => ((b && b._config && b._config.info && b._config.info.version && a(b._config.info.version, c))), + c = BdApi.Plugins.get('ZeresPluginLibrary'), + d = BdApi.Plugins.get('XenoLib'); + b(c, '1.2.27') && (ZeresPluginLibraryOutdated = !0), b(d, '1.3.35') && (XenoLibOutdated = !0); + } catch (a) { + console.error('Error checking if libraries are out of date', a); + } + + /* to anyone asking "why are you checking if x is out of date", well you see, sometimes, for whatever reason + the libraries are sometimes not updating for people. Either it doesn't check for an update, or the request + for some odd reason just fails. Yet, plugins update just fine with the same domain. + */ + return !global.ZeresPluginLibrary || !global.XenoLib || ZeresPluginLibraryOutdated || XenoLibOutdated + ? class extends BasePlugin(class { }) { + constructor() { + super(); + this._XL_PLUGIN = true; + this.getName = () => this.name.replace(/\s+/g, ''); + this.getAuthor = () => this.author; + this.getVersion = () => this.version; + this.getDescription = () => this.description + global.BetterDiscordConfig ? '' : ' You are missing libraries for this plugin, please enable the plugin and click Download Now.'; + this.start = this.load = this.handleMissingLib; + } + start() { } + stop() { } + handleMissingLib() { + const a = !!window.powercord && -1 !== (window.bdConfig && window.bdConfig.dataPath || "").indexOf("bdCompat") && "function" == typeof BdApi.__getPluginConfigPath, b = BdApi.findModuleByProps("openModal", "hasModalOpen"); + if (b && b.hasModalOpen(`${this.name}_DEP_MODAL`)) return; + const c = !global.XenoLib, d = !global.ZeresPluginLibrary, e = c && d || (c || d) && (XenoLibOutdated || ZeresPluginLibraryOutdated), + f = (() => { let a = ""; return c || d ? a += `Missing${XenoLibOutdated || ZeresPluginLibraryOutdated ? " and outdated" : ""} ` : (XenoLibOutdated || ZeresPluginLibraryOutdated) && (a += `Outdated `), a += `${e ? "Libraries" : "Library"} `, a })(), + g = (() => { + let a = `The ${e ? "libraries" : "library"} `; return c || XenoLibOutdated ? (a += "XenoLib ", (d || ZeresPluginLibraryOutdated) && (a += "and ZeresPluginLibrary ")) : (d || ZeresPluginLibraryOutdated) && (a += "ZeresPluginLibrary "), + a += `required for ${this.name} ${e ? "are" : "is"} ${c || d ? "missing" : ""}${XenoLibOutdated || ZeresPluginLibraryOutdated ? c || d ? " and/or outdated" : "outdated" : ""}.`, a + })(), h = BdApi.findModuleByDisplayName("Text"), i = BdApi.findModuleByDisplayName("ConfirmModal"), + j = () => BdApi.alert(f, BdApi.React.createElement("span", { style: { color: "white" } }, BdApi.React.createElement("div", {}, g), `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.`, + d || ZeresPluginLibraryOutdated ? BdApi.React.createElement("div", {}, BdApi.React.createElement("a", { href: "https://betterdiscord.net/ghdl?id=2252", target: "_blank" }, "Click here to download ZeresPluginLibrary")) : null, c || 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 (global.ohgodohfuck) return; if (!b || !i || !h) return console.error(`Missing components:${(b ? "" : " ModalStack") + (i ? "" : " ConfirmationModalComponent") + (h ? "" : "TextElement")}`), + j(); class k extends BdApi.React.PureComponent { + constructor(a) { + super(a), this.state = { hasError: !1 }, this.componentDidCatch = a => (console.error(`Error in ${this.props.label}, screenshot or copy paste the error above to Lighty for help.`), this.setState({ hasError: !0 }), "function" == typeof this.props.onError && this.props.onError(a)), + this.render = () => this.state.hasError ? null : this.props.children + } + } let m = !1; const n = b.openModal(c => { + if (m) return null; try { + return BdApi.React.createElement(k, { label: "missing dependency modal", onError: () => (b.closeModal(n), j()) }, BdApi.React.createElement(i, + Object.assign({ + header: f, children: BdApi.React.createElement(h, { size: h.Sizes.SIZE_16, children: [`${g} Please click Download Now to download ${e ? "them" : "it"}.`] }), red: !1, confirmText: "Download Now", cancelText: "Cancel", onCancel: c.onClose, onConfirm: () => { + const c = require("request"), d = require("fs"), + e = require("path"), f = BdApi.Plugins && BdApi.Plugins.folder ? BdApi.Plugins.folder : window.ContentManager.pluginsFolder, g = () => { + global.XenoLib && !XenoLibOutdated || c("https://gitdab.com/Dollar3795/LightcordPlugins/raw/branch/master/Plugins/1XenoLib.plugin.js", (c, g, h) => { + try { + if (c || 200 !== g.statusCode) return b.closeModal(n), + j(); d.writeFile(e.join(f, "1XenoLib.plugin.js"), h, () => { BdApi.isSettingEnabled("fork-ps-5") && !a || BdApi.Plugins.reload(this.getName()) }) + } catch (a) { console.error("Fatal error downloading XenoLib", a), b.closeModal(n), j() } + }) + }; !global.ZeresPluginLibrary || ZeresPluginLibraryOutdated ? c("https://raw.githubusercontent.com/rauenzi/BDPluginLibrary/master/release/0PluginLibrary.plugin.js", + (a, c, h) => { try { if (a || 200 !== c.statusCode) return b.closeModal(n), j(); d.writeFile(e.join(f, "0PluginLibrary.plugin.js"), h, () => { }), g() } catch (a) { console.error("Fatal error downloading ZeresPluginLibrary", a), b.closeModal(n), j() } }) : g() + } + }, c, { onClose: () => { } }))) + } catch (a) { return console.error("There has been an error constructing the modal", a), m = !0, b.closeModal(n), j(), null } + }, { modalKey: `${this.name}_DEP_MODAL` }); + } + } + : buildPlugin(global.ZeresPluginLibrary.buildPlugin(config), BasePlugin); +})(); + +/*@end@*/