├── .gitignore ├── Plugins ├── 1XenoLib.plugin.js └── MessageLoggerV2 │ ├── CHANGELOG.md │ ├── MessageLoggerV2.plugin.js │ └── README.md ├── README.md └── download ├── README.md └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode -------------------------------------------------------------------------------- /Plugins/1XenoLib.plugin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @name XenoLib 3 | * @description Simple library to complement plugins with shared code without lowering performance. Also adds needed buttons to some plugins. 4 | * @author 1Lighty 5 | * @authorId 239513071272329217 6 | * @version 2.0 7 | * @invite NYvWdN5 8 | * @source https://github.com/Davilarek/MessageLoggerV2-fixed/blob/master/Plugins/1XenoLib.plugin.js 9 | * @updateUrl https://raw.githubusercontent.com/Davilarek/MessageLoggerV2-fixed/master/Plugins/1XenoLib.plugin.js 10 | */ 11 | /*@cc_on 12 | @if (@_jscript) 13 | 14 | // Offer to self-install for clueless users that try to run this directly. 15 | var shell = WScript.CreateObject('WScript.Shell'); 16 | var fs = new ActiveXObject('Scripting.FileSystemObject'); 17 | var pathPlugins = shell.ExpandEnvironmentStrings('%APPDATA%\\BetterDiscord\\plugins'); 18 | var pathSelf = WScript.ScriptFullName; 19 | // Put the user at ease by addressing them in the first person 20 | 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); 21 | if (fs.GetParentFolderName(pathSelf) === fs.GetAbsolutePathName(pathPlugins)) { 22 | shell.Popup('I\'m in the correct folder already.', 0, 'I\'m already installed', 0x40); 23 | } else if (!fs.FolderExists(pathPlugins)) { 24 | shell.Popup('I can\'t find the BetterDiscord plugins folder.\nAre you sure it\'s even installed?', 0, 'Can\'t install myself', 0x10); 25 | } else if (shell.Popup('Should I copy myself to BetterDiscord\'s plugins folder for you?', 0, 'Do you need some help?', 0x34) === 6) { 26 | fs.CopyFile(pathSelf, fs.BuildPath(pathPlugins, '1XenoLib.plugin.js'), true); 27 | // Show the user where to put plugins in the future 28 | shell.Exec('explorer ' + pathPlugins); 29 | shell.Popup('I\'m installed!', 0, 'Successfully installed', 0x40); 30 | } 31 | WScript.Quit(); 32 | 33 | @else@*/ 34 | /* 35 | * Copyright © 2019-2022, _Lighty_ 36 | * All rights reserved. 37 | * Code may not be redistributed, modified or otherwise taken without explicit permission. 38 | */ 39 | 40 | // eslint-disable-next-line no-undef 41 | if (window.__XL_waitingForWatcherTimeout && !window.__XL_assumingZLibLoaded) clearTimeout(window.__XL_waitingForWatcherTimeout); 42 | 43 | function _extractMeta(code/* : string */)/* : BDPluginManifest */ { 44 | const [firstLine] = code.split('\n'); 45 | if (firstLine.indexOf('//META') !== -1) return _parseOldMeta(code); 46 | if (firstLine.indexOf('/**') !== -1) return _parseNewMeta(code); 47 | throw new /* ErrorNoStack */Error('No or invalid plugin META header'); 48 | } 49 | 50 | function _parseOldMeta(code/* : string */)/* : BDPluginManifest */ { 51 | const [meta] = code.split('\n'); 52 | const rawMeta = meta.substring(meta.indexOf('//META') + 6, meta.indexOf('*//')); 53 | try { 54 | const parsed = JSON.parse(rawMeta); 55 | if (!parsed.name) throw 'ENONAME'; 56 | parsed.format = 'json'; 57 | return parsed; 58 | } catch (err) { 59 | if (err === 'ENONAME') throw new /* ErrorNoStack */Error('Plugin META header missing name property'); 60 | throw new /* ErrorNoStack */Error('Plugin META header could not be parsed'); 61 | } 62 | } 63 | 64 | function _parseNewMeta(code/* : string */)/* : BDPluginManifest */ { 65 | const ret = {}; 66 | let key = ''; 67 | let value = ''; 68 | try { 69 | const jsdoc = code.substr(code.indexOf('/**') + 3, code.indexOf('*/') - code.indexOf('/**') - 3); 70 | for (let i = 0, lines = jsdoc.split(/[^\S\r\n]*?(?:\r\n|\n)[^\S\r\n]*?\*[^\S\r\n]?/); i < lines.length; i++) { 71 | const line = lines[i]; 72 | if (!line.length) continue; 73 | if (line[0] !== '@' || line[1] === ' ') { 74 | value += ` ${line.replace('\\n', '\n').replace(/^\\@/, '@')}`; 75 | continue; 76 | } 77 | if (key && value) ret[key] = value.trim(); 78 | const spaceSeperator = line.indexOf(' '); 79 | key = line.substr(1, spaceSeperator - 1); 80 | value = line.substr(spaceSeperator + 1); 81 | } 82 | ret[key] = value.trim(); 83 | ret.format = 'jsdoc'; 84 | } catch (err) { 85 | throw new /* ErrorNoStack */Error(`Plugin META header could not be parsed ${err}`); 86 | } 87 | if (!ret.name) throw new /* ErrorNoStack */Error('Plugin META header missing name property'); 88 | return ret; 89 | } 90 | 91 | module.exports = (() => { 92 | const canUseAstraNotifAPI = !!(global.Astra && Astra.n11s && Astra.n11s.n11sApi); 93 | // 1 day interval in milliseconds 94 | const USER_COUNTER_INTERVAL = 1000 * 60 * 60 * 24 * 1; 95 | /* Setup */ 96 | const config = { 97 | main: 'index.js', 98 | info: { 99 | name: 'XenoLib', 100 | authors: [ 101 | { 102 | name: 'Lighty', 103 | discord_id: '239513071272329217', 104 | github_username: '1Lighty', 105 | twitter_username: '' 106 | }, 107 | // { 108 | // name: 'Davilarek', 109 | // discord_id: '456226577798135808', 110 | // github_username: 'Davilarek', 111 | // twitter_username: '' 112 | // } 113 | /* I barely changed anything here */ 114 | ], 115 | version: '2.0', // don't interfere 116 | description: 'Simple library to complement plugins with shared code without lowering performance. Also adds needed buttons to some plugins.', 117 | // github: 'https://github.com/1Lighty', 118 | github_raw: 'https://raw.githubusercontent.com/Davilarek/MessageLoggerV2-fixed/master/Plugins/1XenoLib.plugin.js' 119 | }, 120 | changelog: [ 121 | { 122 | title: 'Fixed', 123 | type: 'fixed', 124 | items: [] 125 | } 126 | ], 127 | defaultConfig: [ 128 | canUseAstraNotifAPI ? {} : { 129 | type: 'category', 130 | id: 'notifications', 131 | name: 'Notification settings', 132 | collapsible: true, 133 | shown: true, 134 | settings: [ 135 | { 136 | name: 'Notification position', 137 | id: 'position', 138 | type: 'position', 139 | value: 'topRight' 140 | }, 141 | { 142 | name: 'Notifications use backdrop-filter', 143 | id: 'backdrop', 144 | type: 'switch', 145 | value: true 146 | }, 147 | { 148 | name: 'Background color', 149 | id: 'backdropColor', 150 | type: 'color', 151 | value: '#3e4346', 152 | options: { 153 | defaultColor: '#3e4346' 154 | } 155 | }, 156 | { 157 | name: 'Timeout resets to 0 when hovered', 158 | id: 'timeoutReset', 159 | type: 'switch', 160 | value: true 161 | } 162 | ] 163 | }, 164 | { 165 | type: 'category', 166 | id: 'addons', 167 | name: 'AddonCard settings', 168 | collapsible: true, 169 | shown: false, 170 | settings: [ 171 | { 172 | name: 'Add extra buttons to specific plugins', 173 | note: 'Disabling this will move the buttons to the bottom of plugin settings (if available)', 174 | id: 'extra', 175 | type: 'switch', 176 | value: true 177 | } 178 | ] 179 | }, 180 | { 181 | type: 'category', 182 | id: 'userCounter', 183 | name: 'User counter settings', 184 | collapsible: true, 185 | shown: false, 186 | settings: [ 187 | { 188 | name: 'Enable user counter', 189 | id: 'enabled', 190 | note: 'Only active after 3 days of enabling this setting', 191 | type: 'switch', 192 | value: true 193 | }, 194 | { 195 | id: 'enableTime', 196 | type: 'timeStatus', 197 | value: 0, 198 | after: 'User counter will be active ', 199 | active: 'User counter is currently active', 200 | inactive: 'User counter is currently inactive', 201 | time: USER_COUNTER_INTERVAL 202 | }, 203 | { 204 | id: 'lastSubmission', 205 | type: 'timeStatus', 206 | value: 0, 207 | after: 'Next user counter submission will be ', 208 | active: 'User counter submission will be submitted on next load', 209 | inactive: 'User counter submissions are inactive', 210 | time: USER_COUNTER_INTERVAL 211 | } 212 | ] 213 | } 214 | ] 215 | }; 216 | 217 | /* Build */ 218 | const buildPlugin = ([Plugin, Api]) => { 219 | const start = performance.now(); 220 | const { Settings, Modals, Utilities, WebpackModules, DiscordModules, ColorConverter, DiscordClasses, ReactTools, ReactComponents, Logger, PluginUpdater, PluginUtilities, Structs } = Api; 221 | const { React, ModalStack, ContextMenuActions, ReactDOM, ChannelStore, GuildStore, UserStore, DiscordConstants, PrivateChannelActions, LayerManager, InviteActions, FlexChild, Titles, Changelog: ChangelogModal, SelectedChannelStore, SelectedGuildStore, Moment } = DiscordModules; 222 | 223 | if (window.__XL_waitingForWatcherTimeout) clearTimeout(window.__XL_waitingForWatcherTimeout); 224 | 225 | try { 226 | //PluginUpdater.checkForUpdate(config.info.name, config.info.version, config.info.github_raw); 227 | } catch (err) { 228 | } 229 | 230 | let CancelledAsync = false; 231 | const DefaultLibrarySettings = {}; 232 | 233 | for (let s = 0; s < config.defaultConfig.length; s++) { 234 | const current = config.defaultConfig[s]; 235 | if (current.type === 'category') { 236 | DefaultLibrarySettings[current.id] = {}; 237 | for (let s = 0; s < current.settings.length; s++) { 238 | const subCurrent = current.settings[s]; 239 | DefaultLibrarySettings[current.id][subCurrent.id] = subCurrent.value; 240 | } 241 | } else DefaultLibrarySettings[current.id] = current.value; 242 | } 243 | const XenoLib = {}; 244 | 245 | if (global.XenoLib) try { 246 | global.XenoLib.shutdown(); 247 | XenoLib._lazyContextMenuListeners = global.XenoLib._lazyContextMenuListeners || []; 248 | } catch (e) { } 249 | if (!XenoLib._lazyContextMenuListeners) XenoLib._lazyContextMenuListeners = []; 250 | XenoLib.shutdown = () => { 251 | try { 252 | Logger.log('Unpatching all'); 253 | Patcher.unpatchAll(); 254 | } catch (e) { 255 | Logger.stacktrace('Failed to unpatch all', e); 256 | } 257 | CancelledAsync = true; 258 | PluginUtilities.removeStyle('XenoLib-CSS'); 259 | try { 260 | const notifWrapper = document.querySelector('.xenoLib-notifications'); 261 | if (notifWrapper) { 262 | ReactDOM.unmountComponentAtNode(notifWrapper); 263 | notifWrapper.remove(); 264 | } 265 | } catch (e) { 266 | Logger.stacktrace('Failed to unmount Notifications component', e); 267 | } 268 | }; 269 | 270 | XenoLib._ = XenoLib.DiscordUtils = window._ || WebpackModules.getByProps('bindAll', 'debounce'); 271 | 272 | XenoLib.loadData = (name, key, defaultData, returnNull) => { 273 | try { 274 | return XenoLib._.mergeWith(defaultData ? Utilities.deepclone(defaultData) : {}, BdApi.getData(name, key), (_, b) => { 275 | if (XenoLib._.isArray(b)) return b; 276 | }); 277 | } catch (err) { 278 | Logger.err(name, 'Unable to load data: ', err); 279 | if (returnNull) return null; 280 | return Utilities.deepclone(defaultData); 281 | } 282 | }; 283 | 284 | // replica of zeres deprecated DiscordAPI 285 | XenoLib.DiscordAPI = { 286 | get userId() { 287 | const user = UserStore.getCurrentUser(); 288 | return user && user.id; 289 | }, 290 | get channelId() { 291 | return SelectedChannelStore.getChannelId(); 292 | }, 293 | get guildId() { 294 | return SelectedGuildStore.getGuildId(); 295 | }, 296 | get user() { 297 | return UserStore.getCurrentUser(); 298 | }, 299 | get channel() { 300 | return ChannelStore.getChannel(this.channelId); 301 | }, 302 | get guild() { 303 | return GuildStore.getGuild(this.guildId); 304 | } 305 | }; 306 | 307 | XenoLib.getClass = (arg, thrw) => { 308 | try { 309 | const args = arg.split(' '); 310 | return WebpackModules.getByProps(...args)[args[args.length - 1]]; 311 | } catch (e) { 312 | if (thrw) throw e; 313 | if (XenoLib.DiscordAPI.userId === '239513071272329217' && !XenoLib.getClass.__warns[arg] || Date.now() - XenoLib.getClass.__warns[arg] > 1000 * 60) { 314 | Logger.warn(`Failed to get class with props ${arg}`, e); 315 | XenoLib.getClass.__warns[arg] = Date.now(); 316 | } 317 | return ''; 318 | } 319 | }; 320 | XenoLib.getSingleClass = (arg, thrw) => { 321 | try { 322 | return XenoLib.getClass(arg, thrw).split(' ')[0]; 323 | } catch (e) { 324 | if (thrw) throw e; 325 | if (XenoLib.DiscordAPI.userId === '239513071272329217' && !XenoLib.getSingleClass.__warns[arg] || Date.now() - XenoLib.getSingleClass.__warns[arg] > 1000 * 60) { 326 | Logger.warn(`Failed to get class with props ${arg}`, e); 327 | XenoLib.getSingleClass.__warns[arg] = Date.now(); 328 | } 329 | return ''; 330 | } 331 | }; 332 | XenoLib.getClass.__warns = {}; 333 | XenoLib.getSingleClass.__warns = {}; 334 | 335 | const NOOP = () => {}; 336 | const NOOP_NULL = () => null; 337 | 338 | const originalFunctionClass = Function; 339 | XenoLib.createSmartPatcher = patcher => { 340 | const createPatcher = patcher => (moduleToPatch, functionName, callback, options = {}) => { 341 | try { 342 | var origDef = moduleToPatch[functionName]; 343 | } catch (err) { 344 | return Logger.error(`Failed to patch ${functionName}`, err); 345 | } 346 | if (origDef && typeof origDef === 'function' && origDef.constructor !== originalFunctionClass) window.Function = origDef.constructor; 347 | 348 | const unpatches = []; 349 | try { 350 | unpatches.push(patcher(moduleToPatch, functionName, callback, options) || NOOP); 351 | } catch (err) { 352 | throw err; 353 | } finally { 354 | window.Function = originalFunctionClass; 355 | } 356 | try { 357 | if (origDef && origDef.__isBDFDBpatched && moduleToPatch.BDFDBpatch && typeof moduleToPatch.BDFDBpatch[functionName].originalMethod === 'function') { 358 | /* do NOT patch a patch by ZLIb, that'd be bad and cause double items in context menus */ 359 | if ((Utilities.getNestedProp(ZeresPluginLibrary, 'Patcher.patches') || []).findIndex(e => e.module === moduleToPatch) !== -1 && moduleToPatch.BDFDBpatch[functionName].originalMethod.__originalFunction) return; 360 | unpatches.push(patcher(moduleToPatch.BDFDBpatch[functionName], 'originalMethod', callback, options)); 361 | } 362 | } catch (err) { 363 | Logger.stacktrace('Failed to patch BDFDB patches', err); 364 | } 365 | return function unpatch() { 366 | unpatches.forEach(e => e()); 367 | }; 368 | }; 369 | return { 370 | ...patcher, before: createPatcher(patcher.before), 371 | instead: createPatcher(patcher.instead), 372 | after: createPatcher(patcher.after) 373 | }; 374 | }; 375 | 376 | const Patcher = XenoLib.createSmartPatcher(Api.Patcher); 377 | 378 | const LibrarySettings = XenoLib.loadData(config.info.name, 'settings', DefaultLibrarySettings); 379 | LibrarySettings.addons.extra = false; 380 | LibrarySettings.userCounter.enabled = false; 381 | LibrarySettings.userCounter.lastSubmission = 0; 382 | LibrarySettings.userCounter.enableTime = 0; 383 | PluginUtilities.saveSettings(this.name, LibrarySettings); 384 | 385 | try { 386 | // 1 week before the API will be enabled. 387 | if (LibrarySettings.userCounter.enabled) { 388 | const { enableTime } = LibrarySettings.userCounter; 389 | let changed = false; 390 | if (enableTime) { 391 | if ((Date.now() - enableTime > USER_COUNTER_INTERVAL) && (Date.now() - LibrarySettings.userCounter.lastSubmission > USER_COUNTER_INTERVAL)) { 392 | LibrarySettings.userCounter.lastSubmission = Date.now(); 393 | changed = true; 394 | // require('https').get('https://astranika.com/api/analytics/submit', res => { 395 | // res.on('error', () => {}); 396 | // }); 397 | } 398 | } else { 399 | LibrarySettings.userCounter.enableTime = Date.now(); 400 | changed = true; 401 | } 402 | if (changed) PluginUtilities.saveSettings(config.info.name, LibrarySettings); 403 | } 404 | } catch (err) { 405 | Logger.stacktrace('Failed to load user counter', err); 406 | } 407 | 408 | PluginUtilities.addStyle( 409 | 'XenoLib-CSS', 410 | ` 411 | .xenoLib-color-picker .xenoLib-button { 412 | min-height: 38px; 413 | width: 34px; 414 | white-space: nowrap; 415 | position: relative; 416 | transition: background-color .2s ease-in-out,color .2s ease-in-out,width .2s ease-in-out; 417 | overflow: hidden; 418 | margin: 4px 4px 4px 0; 419 | padding: 2px 20px; 420 | border-radius: 2px; 421 | } 422 | 423 | .xenoLib-color-picker .xenoLib-button:hover { 424 | width: 128px; 425 | } 426 | 427 | .xenoLib-color-picker .xenoLib-button .xl-text-1SHFy0 { 428 | opacity: 0; 429 | transform: translate3d(200%,0,0); 430 | 431 | } 432 | .xenoLib-color-picker .xenoLib-button:hover .xl-text-1SHFy0 { 433 | opacity: 1; 434 | transform: translateZ(0); 435 | } 436 | .xenoLib-button-icon { 437 | left: 50%; 438 | top: 50%; 439 | position: absolute; 440 | margin-left: -12px; 441 | margin-top: -8px; 442 | width: 24px; 443 | height: 24px; 444 | opacity: 1; 445 | transform: translateZ(0); 446 | transition: opacity .2s ease-in-out,transform .2s ease-in-out,-webkit-transform .2s ease-in-out; 447 | } 448 | .xenoLib-button-icon.xenoLib-revert > svg { 449 | width: 24px; 450 | height: 24px; 451 | } 452 | .xenoLib-button-icon.xenoLib-revert { 453 | margin-top: -12px; 454 | } 455 | .xenoLib-button:hover .xenoLib-button-icon { 456 | opacity: 0; 457 | transform: translate3d(-200%,0,0); 458 | } 459 | .xenoLib-notifications { 460 | position: absolute; 461 | color: white; 462 | width: 100%; 463 | min-height: 100%; 464 | display: flex; 465 | flex-direction: column; 466 | z-index: 1000; 467 | pointer-events: none; 468 | font-size: 14px; 469 | } 470 | .xenoLib-notification { 471 | min-width: 200px; 472 | overflow: hidden; 473 | } 474 | .xenoLib-notification-content-wrapper { 475 | padding: 22px 20px 0 20px; 476 | } 477 | .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 { 478 | padding: 0 20px 20px 20px; 479 | } 480 | .xenoLib-notification-content { 481 | padding: 12px; 482 | overflow: hidden; 483 | background: #474747; 484 | pointer-events: all; 485 | position: relative; 486 | width: 20vw; 487 | white-space: break-spaces; 488 | min-width: 330px; 489 | } 490 | .xenoLib-notification-loadbar { 491 | position: absolute; 492 | bottom: 0; 493 | left: 0px; 494 | width: auto; 495 | background-image: linear-gradient(130deg,var(--grad-one),var(--grad-two)); 496 | height: 5px; 497 | } 498 | .xenoLib-notification-loadbar-user { 499 | animation: fade-loadbar-animation 1.5s ease-in-out infinite; 500 | } 501 | @keyframes fade-loadbar-animation { 502 | 0% { 503 | filter: brightness(75%) 504 | } 505 | 50% { 506 | filter: brightness(100%) 507 | } 508 | to { 509 | filter: brightness(75%) 510 | } 511 | } 512 | .xenoLib-notification-loadbar-striped:before { 513 | content: ""; 514 | position: absolute; 515 | width: 100%; 516 | height: 100%; 517 | border-radius: 5px; 518 | background: linear-gradient( 519 | -20deg, 520 | transparent 35%, 521 | var(--bar-color) 35%, 522 | var(--bar-color) 70%, 523 | transparent 70% 524 | ); 525 | animation: shift 1s linear infinite; 526 | background-size: 60px 100%; 527 | box-shadow: inset 0 0px 1px rgba(0, 0, 0, 0.2), 528 | inset 0 -2px 1px rgba(0, 0, 0, 0.2); 529 | } 530 | @keyframes shift { 531 | to { 532 | background-position: 60px 100%; 533 | } 534 | } 535 | .xenoLib-notification-close { 536 | float: right; 537 | padding: 0; 538 | height: unset; 539 | opacity: .7; 540 | } 541 | .xenLib-notification-counter { 542 | float: right; 543 | margin-top: 2px; 544 | } 545 | .option-xenoLib { 546 | position: absolute; 547 | width: 24%; 548 | height: 24%; 549 | margin: 6px; 550 | border-radius: 3px; 551 | opacity: .6; 552 | background-color: #72767d; 553 | cursor: pointer; 554 | overflow: hidden; 555 | text-indent: -999em; 556 | font-size: 0; 557 | line-height: 0; 558 | } 559 | .selected-xenoLib.option-xenoLib { 560 | background-color: var(--brand-experiment); 561 | border-color: var(--brand-experiment); 562 | box-shadow: 0 2px 0 rgba(0,0,0,.3); 563 | opacity: 1; 564 | } 565 | .topLeft-xenoLib { 566 | top: 0; 567 | left: 0; 568 | } 569 | .topRight-xenoLib { 570 | top: 0; 571 | right: 0; 572 | } 573 | .bottomLeft-xenoLib { 574 | bottom: 0; 575 | left: 0; 576 | } 577 | .bottomRight-xenoLib { 578 | bottom: 0; 579 | right: 0; 580 | } 581 | .topMiddle-xenoLib { 582 | top: 0; 583 | left: 0; 584 | right: 0; 585 | margin-left: auto; 586 | margin-right: auto; 587 | } 588 | .bottomMiddle-xenoLib { 589 | bottom: 0; 590 | left: 0; 591 | right: 0; 592 | margin-left: auto; 593 | margin-right: auto; 594 | } 595 | .xenoLib-centering-topLeft, .xenoLib-centering-bottomLeft { 596 | align-items: flex-start; 597 | } 598 | .xenoLib-centering-topMiddle, .xenoLib-centering-bottomMiddle { 599 | align-items: center; 600 | } 601 | .xenoLib-centering-topRight, .xenoLib-centering-bottomRight { 602 | align-items: flex-end; 603 | } 604 | .xenoLib-centering-bottomLeft, .xenoLib-centering-bottomMiddle, .xenoLib-centering-bottomRight { 605 | flex-direction: column-reverse; 606 | bottom: 0; 607 | } 608 | .xenoLib-position-wrapper { 609 | box-sizing: border-box; 610 | position: relative; 611 | background-color: rgba(0,0,0,.1); 612 | padding-bottom: 56.25%; 613 | border-radius: 8px; 614 | border: 2px solid var(--brand-experiment); 615 | } 616 | .xenoLib-position-hidden-input { 617 | opacity: 0; 618 | position: absolute; 619 | top: 0; 620 | cursor: pointer; 621 | } 622 | .XL-chl-p img{ 623 | width: unset !important; 624 | } 625 | .xenoLib-error-text { 626 | padding-top: 5px; 627 | } 628 | 629 | .xenoLib-multiInput { 630 | display: -webkit-box; 631 | display: -ms-flexbox; 632 | display: flex; 633 | -webkit-box-align: center; 634 | -ms-flex-align: center; 635 | align-items: center; 636 | font-size: 16px; 637 | -webkit-box-sizing: border-box; 638 | box-sizing: border-box; 639 | width: 100%; 640 | border-radius: 3px; 641 | color: var(--text-normal); 642 | background-color: var(--deprecated-text-input-bg); 643 | border: 1px solid var(--deprecated-text-input-border); 644 | -webkit-transition: border-color .2s ease-in-out; 645 | transition: border-color .2s ease-in-out; 646 | } 647 | .xenoLib-multiInput.xenoLib-multiInput-focused { 648 | border-color: var(--text-link); 649 | } 650 | .xenoLib-multiInput.xenoLib-multiInput-error { 651 | border-color: hsl(359,calc(var(--saturation-factor, 1)*82.6%),59.4%); 652 | } 653 | .xenoLib-multiInputFirst { 654 | -webkit-box-flex: 1; 655 | -ms-flex-positive: 1; 656 | flex-grow: 1 657 | } 658 | .xenoLib-multiInputField { 659 | border: none; 660 | background-color: transparent 661 | } 662 | ` 663 | ); 664 | 665 | { 666 | const hasOwn = {}.hasOwnProperty; 667 | 668 | XenoLib.joinClassNames = function classNames(...args/* : VariableClassNamesArgs */)/* : string */ { 669 | const classes = []; 670 | for (let i = 0, len = args.length; i < len; i++) { 671 | const arg = args[i]; 672 | if (!arg) continue; 673 | const argType = typeof arg; 674 | if (argType === 'string' || argType === 'number') classes.push(arg); 675 | else if (Array.isArray(arg)) { 676 | if (arg.length) { 677 | const inner = classNames(...arg); 678 | if (inner) classes.push(inner); 679 | } 680 | // eslint-disable-next-line curly 681 | } else if (argType === 'object') { 682 | if (arg.toString === Object.prototype.toString) for (const key in arg/* as any */) { 683 | if (hasOwn.call(arg, key) && arg[key]) classes.push(key); 684 | } 685 | else classes.push(arg.toString()); 686 | } 687 | } 688 | 689 | return classes.join(' '); 690 | }; 691 | } 692 | 693 | XenoLib.authorId = '239513071272329217'; 694 | XenoLib.supportServerId = '389049952732446731'; 695 | 696 | /* try { 697 | const getUserAsync = WebpackModules.getByProps('getUser', 'acceptAgreements').getUser; 698 | const requestUser = () => 699 | getUserAsync(XenoLib.authorId) 700 | .then(user => (XenoLib.author = user)) 701 | .catch(() => setTimeout(requestUser, 1 * 60 * 1000)); 702 | if (UserStore.getUser(XenoLib.authorId)) XenoLib.author = UserStore.getUser(XenoLib.authorId); 703 | else requestUser(); 704 | } catch (e) { 705 | Logger.stacktrace('Failed to grab author object', e); 706 | } */ 707 | 708 | XenoLib.ReactComponents = {}; 709 | 710 | XenoLib.ReactComponents.ErrorBoundary = class XLErrorBoundary extends React.PureComponent { 711 | constructor(props) { 712 | super(props); 713 | this.state = { hasError: false }; 714 | } 715 | componentDidCatch(err, inf) { 716 | Logger.err(`Error in ${this.props.label}, screenshot or copy paste the error above to Lighty for help.`); 717 | this.setState({ hasError: true }); 718 | if (typeof this.props.onError === 'function') this.props.onError(err); 719 | } 720 | render() { 721 | if (this.state.hasError) return null; 722 | return this.props.children; 723 | } 724 | }; 725 | 726 | /* —————————————— Copyright (c) 2022 1Lighty, All rights reserved —————————————— 727 | * 728 | * A utility from Astra 729 | * 730 | * ————————————————————————————————————————————————————————————————————————————— */ 731 | function fakeRenderHook(executor/* : () => void */, options/* : { 732 | preExecutor?(): void 733 | postExecutor?(): void 734 | useCallback?(...args: any[]): any 735 | useContext?(...args: any[]): any 736 | useDebugValue?(...args: any[]): any 737 | useDeferredValue?(...args: any[]): any 738 | useEffect?(...args: any[]): any 739 | useImperativeHandle?(...args: any[]): any 740 | useLayoutEffect?(...args: any[]): any 741 | useMemo?(...args: any[]): any 742 | useMutableSource?(...args: any[]): any 743 | useOpaqueIdentifier?(...args: any[]): any 744 | useReducer?(...args: any[]): any 745 | useRef?(...args: any[]): any 746 | useState?(...args: any[]): any 747 | useTransition?(...args: any[]): any 748 | } */ = {})/* : void */ { 749 | // @ts-ignore 750 | const ReactDispatcher = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher.current; 751 | const oUseCallback = ReactDispatcher.useCallback; 752 | const oUseContext = ReactDispatcher.useContext; 753 | const oUseDebugValue = ReactDispatcher.useDebugValue; 754 | const oUseDeferredValue = ReactDispatcher.useDeferredValue; 755 | const oUseEffect = ReactDispatcher.useEffect; 756 | const oUseImperativeHandle = ReactDispatcher.useImperativeHandle; 757 | const oUseLayoutEffect = ReactDispatcher.useLayoutEffect; 758 | const oUseMemo = ReactDispatcher.useMemo; 759 | const oUseMutableSource = ReactDispatcher.useMutableSource; 760 | const oUseOpaqueIdentifier = ReactDispatcher.useOpaqueIdentifier; 761 | const oUseReducer = ReactDispatcher.useReducer; 762 | const oUseRef = ReactDispatcher.useRef; 763 | const oUseState = ReactDispatcher.useState; 764 | const oUseTransition = ReactDispatcher.useTransition; 765 | 766 | ReactDispatcher.useCallback = options.useCallback || (() => () => {}); 767 | ReactDispatcher.useContext = options.useContext || (context => context._currentValue); 768 | ReactDispatcher.useDebugValue = options.useDebugValue || (() => {}); 769 | ReactDispatcher.useDeferredValue = options.useDeferredValue || (val => val); 770 | ReactDispatcher.useEffect = options.useEffect || (() => {}); 771 | ReactDispatcher.useImperativeHandle = options.useImperativeHandle || (() => {}); 772 | ReactDispatcher.useLayoutEffect = options.useLayoutEffect || (() => {}); 773 | ReactDispatcher.useMemo = options.useMemo || (memo => memo()); 774 | ReactDispatcher.useMutableSource = options.useMutableSource || (() => {}); 775 | ReactDispatcher.useOpaqueIdentifier = options.useOpaqueIdentifier || (() => rand()); 776 | ReactDispatcher.useReducer = options.useReducer || ((_, val) => [val, () => {}]); 777 | ReactDispatcher.useRef = options.useRef || (() => ({ current: null })); 778 | ReactDispatcher.useState = options.useState || (() => [null, () => {}]); 779 | ReactDispatcher.useTransition = options.useTransition || (() => [() => {}, true]); 780 | 781 | if (typeof options.preExecutor === 'function') options.preExecutor(); 782 | 783 | let ret/* : any */ = null; 784 | try { 785 | ret = executor(); 786 | } catch (err) { 787 | Logger.error('Error rendering functional component', err); 788 | } 789 | 790 | if (typeof options.postExecutor === 'function') options.postExecutor(); 791 | ReactDispatcher.useCallback = oUseCallback; 792 | ReactDispatcher.useContext = oUseContext; 793 | ReactDispatcher.useDebugValue = oUseDebugValue; 794 | ReactDispatcher.useDeferredValue = oUseDeferredValue; 795 | ReactDispatcher.useEffect = oUseEffect; 796 | ReactDispatcher.useImperativeHandle = oUseImperativeHandle; 797 | ReactDispatcher.useLayoutEffect = oUseLayoutEffect; 798 | ReactDispatcher.useMemo = oUseMemo; 799 | ReactDispatcher.useMutableSource = oUseMutableSource; 800 | ReactDispatcher.useOpaqueIdentifier = oUseOpaqueIdentifier; 801 | ReactDispatcher.useReducer = oUseReducer; 802 | ReactDispatcher.useRef = oUseRef; 803 | ReactDispatcher.useState = oUseState; 804 | ReactDispatcher.useTransition = oUseTransition; 805 | 806 | return ret; 807 | } 808 | 809 | XenoLib.fakeRenderHook = fakeRenderHook; 810 | 811 | const deprecateFunction = (name, advice, ret = undefined) => () => (Logger.warn(`XenoLib.${name} is deprecated! ${advice}`), ret); 812 | 813 | XenoLib.patchContext = deprecateFunction('patchContext', 'Do manual patching of context menus instead.'); 814 | 815 | const CTXMenu = WebpackModules.getByProps('default', 'MenuStyle'); 816 | 817 | class ContextMenuWrapper extends React.PureComponent { 818 | constructor(props) { 819 | super(props); 820 | this.handleOnClose = this.handleOnClose.bind(this); 821 | } 822 | handleOnClose() { 823 | ContextMenuActions.closeContextMenu(); 824 | if (this.props.target instanceof HTMLElement) this.props.target.focus(); 825 | } 826 | render() { 827 | return React.createElement(CTXMenu.default, { onClose: this.handleOnClose, id: 'xenolib-context' }, this.props.menu); 828 | } 829 | } 830 | XenoLib.createSharedContext = (element, menuCreation) => { 831 | if (element.__XenoLib_ContextMenus) element.__XenoLib_ContextMenus.push(menuCreation); 832 | else { 833 | element.__XenoLib_ContextMenus = [menuCreation]; 834 | const oOnContextMenu = element.props.onContextMenu; 835 | 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()), ..._ })))); 836 | } 837 | }; 838 | 839 | const contextMenuItems = WebpackModules.find(m => m.MenuRadioItem && !m.default); 840 | XenoLib.unpatchContext = deprecateFunction('unpatchContext', 'Manual patching needs manual unpatching'); 841 | XenoLib.createContextMenuItem = (label, action, id, options = {}) => (!contextMenuItems ? null : React.createElement(contextMenuItems.MenuItem, { label, id, action: () => (!options.noClose && ContextMenuActions.closeContextMenu(), action()), ...options })); 842 | XenoLib.createContextMenuSubMenu = (label, children, id, options = {}) => (!contextMenuItems ? null : React.createElement(contextMenuItems.MenuItem, { label, children, id, ...options })); 843 | XenoLib.createContextMenuGroup = (children, options) => (!contextMenuItems ? null : React.createElement(contextMenuItems.MenuGroup, { children, ...options })); 844 | 845 | 846 | const lazyContextMenu = WebpackModules.getByProps('openContextMenuLazy'); 847 | const ConnectedContextMenus = WebpackModules.getByDisplayName('ConnectedContextMenus'); 848 | if (lazyContextMenu && ConnectedContextMenus) { 849 | try { 850 | const ContextMenus = fakeRenderHook(() => ConnectedContextMenus({}).type); 851 | Patcher.instead(ContextMenus.prototype, 'componentDidMount', (_this, _, orig) => { 852 | if (!_this.props.isOpen || !_this.props.renderLazy) return orig(); 853 | const olRenderLazy = _this.props.renderLazy; 854 | _this.props.renderLazy = async () => { 855 | _this.props.renderLazy = olRenderLazy; 856 | const ret = await olRenderLazy(); 857 | if (typeof ret === 'function') try { 858 | const ctxEl = ret(); 859 | let { type } = ctxEl; 860 | let typeOverriden = false; 861 | const deepAnalyticsWrapper = type.toString().search(/\)\(\w\)\.AnalyticsLocationProvider,/) !== -1; 862 | const analyticsWrapper = deepAnalyticsWrapper || type.toString().includes('.CONTEXT_MENU).AnalyticsLocationProvider'); 863 | if (type.toString().includes('objectType') || analyticsWrapper) fakeRenderHook(() => { 864 | const ret = type(ctxEl.props); 865 | if (ret.type.displayName !== 'AnalyticsContext' && !analyticsWrapper) return; 866 | ({ type } = ret.props.children); 867 | typeOverriden = true; 868 | if (deepAnalyticsWrapper) { 869 | const deeperRet = type(ret.props.children.props); 870 | if (deeperRet?.props?.children?.type) ({ type } = deeperRet.props.children); 871 | } 872 | }, { 873 | useState: () => [[], () => {}], 874 | useCallback: e => e 875 | }); 876 | 877 | let changed = false; 878 | for (const { menuNameOrFilter, callback, multi, patchedModules } of [...XenoLib._lazyContextMenuListeners]) { 879 | if (typeof menuNameOrFilter === 'string' && menuNameOrFilter !== type.displayName && (!typeOverriden || menuNameOrFilter !== ctxEl.type.displayName)) continue; 880 | if (typeof menuNameOrFilter === 'function' && !menuNameOrFilter(type) && (!typeOverriden || !menuNameOrFilter(ctxEl.type))) continue; 881 | if (multi && patchedModules.indexOf(type) !== -1) continue; 882 | changed = callback(ctxEl.type) || changed; 883 | if (multi) { 884 | patchedModules.push(type); 885 | continue; 886 | } 887 | XenoLib._lazyContextMenuListeners = XenoLib._lazyContextMenuListeners.filter(l => l.callback !== callback); 888 | } 889 | if (changed) requestAnimationFrame(() => { 890 | olRenderLazy().then(r => _this.setState({ render: r })); 891 | }); 892 | 893 | } catch (err) { 894 | Logger.error('Error rendering lazy context menu', err); 895 | } 896 | return ret; 897 | }; 898 | orig(); 899 | _this.props.renderLazy = olRenderLazy; 900 | }); 901 | } catch (err) { 902 | Logger.err('Error patching ContextMenus', err); 903 | } 904 | XenoLib.listenLazyContextMenu = (menuNameOrFilter, callback, multi) => { 905 | XenoLib._lazyContextMenuListeners = XenoLib._lazyContextMenuListeners || []; 906 | if (!Array.isArray(XenoLib._lazyContextMenuListeners)) XenoLib._lazyContextMenuListeners = []; 907 | XenoLib._lazyContextMenuListeners.push({ menuNameOrFilter, callback, multi, patchedModules: [] }); 908 | return () => { 909 | XenoLib._lazyContextMenuListeners = XenoLib._lazyContextMenuListeners.filter(l => l.callback !== callback); 910 | }; 911 | }; 912 | } else XenoLib.listenLazyContextMenu = (menuNameOrFilter, callback, multi) => { 913 | callback(); 914 | }; 915 | 916 | 917 | try { 918 | // XenoLib.ReactComponents.ButtonOptions = WebpackModules.getByProps('BorderColors'); 919 | XenoLib.ReactComponents.ButtonOptions = ZeresPluginLibrary.DiscordModules.ButtonData; 920 | XenoLib.ReactComponents.ButtonOptions.ButtonSizes = XenoLib.ReactComponents.ButtonOptions.Sizes; 921 | XenoLib.ReactComponents.Button = XenoLib.ReactComponents.ButtonOptions; 922 | } catch (e) { 923 | Logger.stacktrace('Error getting Button component', e); 924 | } 925 | 926 | const path = require('path'); 927 | const isBBDBeta = typeof window.BDModules !== 'undefined' && !window.require && typeof window.BetterDiscordConfig !== 'undefined' && path.normalize(__dirname).replace(/[\\\/]/g, '/').toLowerCase().indexOf('rd_bd/plugins') !== -1; 928 | // why zere? 929 | if (isBBDBeta) Object.assign(window, require('timers')); 930 | 931 | function patchAddonCardAnyway(manualPatch) { 932 | try { 933 | if (patchAddonCardAnyway.patched) return; 934 | patchAddonCardAnyway.patched = true; 935 | const LinkClassname = XenoLib.joinClassNames(XenoLib.getClass('anchorUnderlineOnHover anchor'), XenoLib.getClass('anchor anchorUnderlineOnHover'), 'bda-author'); 936 | const handlePatch = (_this, _, ret) => { 937 | if (!_this.props.addon || !_this.props.addon.plugin || typeof _this.props.addon.plugin.getAuthor().indexOf('Lighty') === -1) return; 938 | const settingsProps = Utilities.findInReactTree(ret, e => e && e.className === 'plugin-settings'); 939 | if (settingsProps) delete settingsProps.id; 940 | const author = Utilities.findInReactTree(ret, e => e && e.props && typeof e.props.className === 'string' && e.props.className.indexOf('bda-author') !== -1); 941 | if (!author || typeof author.props.children !== 'string' || author.props.children.indexOf('Lighty') === -1) return; 942 | const onClick = () => { 943 | if (XenoLib.DiscordAPI.userId === XenoLib.authorId) return; 944 | PrivateChannelActions.ensurePrivateChannel(XenoLib.DiscordAPI.userId, XenoLib.authorId).then(() => { 945 | PrivateChannelActions.openPrivateChannel(XenoLib.DiscordAPI.userId, XenoLib.authorId); 946 | LayerManager.popLayer(); 947 | }); 948 | }; 949 | if (author.props.children === 'Lighty') { 950 | author.type = 'a'; 951 | author.props.className = LinkClassname; 952 | author.props.onClick = onClick; 953 | } else { 954 | const idx = author.props.children.indexOf('Lighty'); 955 | const pre = author.props.children.slice(0, idx); 956 | const post = author.props.children.slice(idx + 6); 957 | author.props.children = [ 958 | pre, 959 | React.createElement( 960 | 'a', 961 | { 962 | className: LinkClassname, 963 | onClick 964 | }, 965 | 'Lighty' 966 | ), 967 | post 968 | ]; 969 | delete author.props.onClick; 970 | author.props.className = 'bda-author'; 971 | author.type = 'span'; 972 | } 973 | let footerProps = Utilities.findInReactTree(ret, e => e && e.props && typeof e.props.className === 'string' && e.props.className.indexOf('bda-links') !== -1); 974 | if (!footerProps) return; 975 | footerProps = footerProps.props; 976 | if (!Array.isArray(footerProps.children)) footerProps.children = [footerProps.children]; 977 | const findLink = name => Utilities.findInReactTree(footerProps.children, e => e && e.props && e.props.children === name); 978 | const websiteLink = findLink('Website'); 979 | const sourceLink = findLink('Source'); 980 | const supportServerLink = findLink('Support Server'); 981 | footerProps.children = []; 982 | if (websiteLink) { 983 | const { href } = websiteLink.props; 984 | delete websiteLink.props.href; 985 | delete websiteLink.props.target; 986 | websiteLink.props.onClick = () => window.open(href); 987 | footerProps.children.push(websiteLink); 988 | } 989 | if (sourceLink) { 990 | const { href } = sourceLink.props; 991 | delete sourceLink.props.href; 992 | delete sourceLink.props.target; 993 | sourceLink.props.onClick = () => window.open(href); 994 | footerProps.children.push(websiteLink ? ' | ' : null, sourceLink); 995 | } 996 | 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')); 997 | footerProps.children.push(' | ', supportServerLink || React.createElement('a', { className: 'bda-link bda-link-website', onClick: () => (LayerManager.popLayer(), InviteActions.acceptInviteAndTransitionToInviteChannel('NYvWdN5')) }, 'Support Server')); 998 | 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')); 999 | footerProps = null; 1000 | }; 1001 | async function patchRewriteCard() { 1002 | const component = [...ReactComponents.components.entries()].find(([_, e]) => e.component && e.component.prototype && e.component.prototype.reload && e.component.prototype.showSettings); 1003 | const AddonCard = component ? component[1] : await ReactComponents.getComponent('AddonCard', '.bda-slist > .ui-switch-item', e => e.prototype && e.prototype.reload && e.prototype.showSettings); 1004 | if (CancelledAsync) return; 1005 | const ContentColumn = await ReactComponents.getComponent('ContentColumn', '.content-column'); 1006 | class PatchedAddonCard extends AddonCard.component { 1007 | render() { 1008 | const ret = super.render(); 1009 | try { 1010 | /* did I mention I am Lighty? */ 1011 | handlePatch(this, undefined, ret); 1012 | } catch (err) { 1013 | Logger.stacktrace('AddonCard patch', err); 1014 | } 1015 | return ret; 1016 | } 1017 | } 1018 | let firstRender = true; 1019 | Patcher.after(ContentColumn.component.prototype, 'render', (_, __, ret) => { 1020 | if (!LibrarySettings.addons.extra) return; 1021 | const list = Utilities.findInReactTree(ret, e => e && typeof e.className === 'string' && e.className.indexOf('bd-addon-list') !== -1); 1022 | if (Utilities.getNestedProp(list, 'children.0.props.children.type') !== AddonCard.component) return; 1023 | for (const item of list.children) { 1024 | const card = Utilities.getNestedProp(item, 'props.children'); 1025 | if (!card) continue; 1026 | card.type = PatchedAddonCard; 1027 | } 1028 | if (!firstRender) return; 1029 | ret.key = DiscordModules.KeyGenerator(); 1030 | firstRender = false; 1031 | }); 1032 | if (manualPatch) return; 1033 | ContentColumn.forceUpdateAll(); 1034 | AddonCard.forceUpdateAll(); 1035 | } 1036 | patchRewriteCard(); 1037 | } catch (e) { 1038 | Logger.stacktrace('Failed to patch V2C_*Card or AddonCard (BBD rewrite)', e); 1039 | } 1040 | } 1041 | if (LibrarySettings.addons.extra) patchAddonCardAnyway(); 1042 | 1043 | try { 1044 | XenoLib.ReactComponents.PluginFooter = class XLPluginFooter extends React.PureComponent { 1045 | render() { 1046 | if (LibrarySettings.addons.extra) return null; 1047 | return React.createElement( 1048 | 'div', 1049 | { 1050 | style: { 1051 | display: 'flex' 1052 | } 1053 | }, 1054 | React.createElement( 1055 | XenoLib.ReactComponents.Button, 1056 | { 1057 | style: { 1058 | flex: '2 1 auto' 1059 | }, 1060 | onClick: this.props.showChangelog 1061 | }, 1062 | 'Changelog' 1063 | ), 1064 | React.createElement( 1065 | XenoLib.ReactComponents.Button, 1066 | { 1067 | style: { 1068 | flex: '2 1 auto' 1069 | }, 1070 | 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 }))) 1071 | }, 1072 | 'Donate' 1073 | ), 1074 | React.createElement( 1075 | XenoLib.ReactComponents.Button, 1076 | { 1077 | style: { 1078 | flex: '2 1 auto' 1079 | }, 1080 | onClick: () => (LayerManager.popLayer(), InviteActions.acceptInviteAndTransitionToInviteChannel('NYvWdN5')) 1081 | }, 1082 | 'Support server' 1083 | ) 1084 | ); 1085 | } 1086 | }; 1087 | } catch (err) { 1088 | Logger.stacktrace('Error creating plugin footer'); 1089 | XenoLib.ReactComponents.PluginFooter = NOOP_NULL; 1090 | } 1091 | 1092 | const TextElement = WebpackModules.getByDisplayName('Text') || WebpackModules.find(e => e.Text?.displayName === 'Text')?.Text; 1093 | 1094 | /* shared between FilePicker and ColorPicker */ 1095 | const MultiInputClassname = 'xenoLib-multiInput'; 1096 | const MultiInputFirstClassname = 'xenoLib-multiInputFirst'; 1097 | const MultiInputFieldClassname = 'xenoLib-multiInputField'; 1098 | const ErrorMessageClassname = XenoLib.joinClassNames('xenoLib-error-text', XenoLib.getClass('errorMessage'), Utilities.getNestedProp(TextElement, 'Colors.ERROR')); 1099 | const ErrorClassname = XenoLib.joinClassNames('xenoLib-multiInput-error', XenoLib.getClass('input error')); 1100 | 1101 | try { 1102 | class DelayedCall { 1103 | constructor(delay, callback) { 1104 | this.delay = delay; 1105 | this.callback = callback; 1106 | this.timeout = null; 1107 | } 1108 | 1109 | delay() { 1110 | clearTimeout(this.timeout); 1111 | this.timeout = setTimeout(this.callback, this.delay); 1112 | } 1113 | } 1114 | const FsModule = require('fs'); 1115 | /** 1116 | * @interface 1117 | * @name module:FilePicker 1118 | * @property {string} path 1119 | * @property {string} placeholder 1120 | * @property {Function} onChange 1121 | * @property {object} properties 1122 | * @property {bool} nullOnInvalid 1123 | * @property {bool} saveOnEnter 1124 | */ 1125 | XenoLib.ReactComponents.FilePicker = class FilePicker extends React.PureComponent { 1126 | constructor(props) { 1127 | super(props); 1128 | this.state = { 1129 | multiInputFocused: false, 1130 | path: props.path, 1131 | error: null 1132 | }; 1133 | XenoLib._.bindAll(this, ['handleOnBrowse', 'handleChange', 'checkInvalidDir']); 1134 | this.handleKeyDown = XenoLib._.throttle(this.handleKeyDown.bind(this), 500); 1135 | this.delayedCallVerifyPath = new DelayedCall(500, () => this.checkInvalidDir()); 1136 | } 1137 | checkInvalidDir(doSave) { 1138 | FsModule.access(this.state.path, FsModule.constants.W_OK, error => { 1139 | const invalid = (error && error.message.match(/.*: (.*), access '/)[1]) || null; 1140 | this.setState({ error: invalid }); 1141 | if (this.props.saveOnEnter && !doSave) return; 1142 | if (invalid) this.props.onChange(this.props.nullOnInvalid ? null : ''); 1143 | else this.props.onChange(this.state.path); 1144 | }); 1145 | } 1146 | handleOnBrowse() { 1147 | DiscordNative.fileManager.showOpenDialog({ title: this.props.title, properties: this.props.properties }).then(({ filePaths: [path] }) => { 1148 | if (path) this.handleChange(path); 1149 | }); 1150 | } 1151 | handleChange(path) { 1152 | this.setState({ path }); 1153 | this.delayedCallVerifyPath.delay(); 1154 | } 1155 | handleKeyDown(e) { 1156 | if (!this.props.saveOnEnter || e.which !== DiscordConstants.KeyboardKeys.ENTER) return; 1157 | this.checkInvalidDir(true); 1158 | } 1159 | render() { 1160 | const n = {}; 1161 | n['xenoLib-multiInput-focused'] = this.state.multiInputFocused; 1162 | n[ErrorClassname] = !!this.state.error; 1163 | return React.createElement( 1164 | 'div', 1165 | { className: DiscordClasses.BasicInputs.inputWrapper, style: { width: '100%' } }, 1166 | React.createElement( 1167 | 'div', 1168 | { className: XenoLib.joinClassNames(MultiInputClassname, n) }, 1169 | React.createElement(DiscordModules.Textbox, { 1170 | value: this.state.path, 1171 | placeholder: this.props.placeholder, 1172 | onChange: this.handleChange, 1173 | onFocus: () => this.setState({ multiInputFocused: true }), 1174 | onBlur: () => this.setState({ multiInputFocused: false }), 1175 | onKeyDown: this.handleKeyDown, 1176 | autoFocus: false, 1177 | className: MultiInputFirstClassname, 1178 | inputClassName: MultiInputFieldClassname 1179 | }), 1180 | 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') 1181 | ), 1182 | !!this.state.error && React.createElement('div', { className: ErrorMessageClassname, style: { color: 'hsl(359,calc(var(--saturation-factor, 1)*82.6%),59.4%)' } }, 'Error: ', this.state.error) 1183 | ); 1184 | } 1185 | }; 1186 | } catch (e) { 1187 | Logger.stacktrace('Failed to create FilePicker component', e); 1188 | } 1189 | 1190 | /** 1191 | * @param {string} name - name label of the setting 1192 | * @param {string} note - help/note to show underneath or above the setting 1193 | * @param {string} value - current hex color 1194 | * @param {callable} onChange - callback to perform on setting change, callback receives hex string 1195 | * @param {object} [options] - object of options to give to the setting 1196 | * @param {boolean} [options.disabled=false] - should the setting be disabled 1197 | * @param {Array} [options.colors=presetColors] - preset list of colors 1198 | * @author Zerebos, from his library ZLibrary 1199 | */ 1200 | const FormItem = WebpackModules.getByDisplayName('FormItem'); 1201 | 1202 | const ColorPickerComponent = (_ => { 1203 | try { 1204 | return null; 1205 | return fakeRenderHook(() => { 1206 | const GSRED = WebpackModules.getByDisplayName('GuildSettingsRolesEditDisplay'); 1207 | const ret = GSRED({ role: { id: '' }, guild: { id: '' } }); 1208 | const cpfi = Utilities.findInReactTree(ret, e => e && e.type && e.type.displayName === 'ColorPickerFormItem').type; 1209 | const ret2 = cpfi({ role: { color: '' } }); 1210 | const ColorPicker = Utilities.findInReactTree(ret2, e => e && e.props && e.props.colors).type; 1211 | return ColorPicker; 1212 | }); 1213 | } catch (err) { 1214 | Logger.stacktrace('Failed to get lazy colorpicker, unsurprisingly', err); 1215 | return _ => null; 1216 | } 1217 | })(); 1218 | 1219 | const ModalStuff = WebpackModules.getByProps('ModalRoot'); 1220 | 1221 | class ColorPickerModal extends React.PureComponent { 1222 | constructor(props) { 1223 | super(props); 1224 | this.state = { value: props.value }; 1225 | XenoLib._.bindAll(this, ['handleChange']); 1226 | } 1227 | handleChange(value) { 1228 | this.setState({ value }); 1229 | this.props.onChange(ColorConverter.int2hex(value)); 1230 | } 1231 | render() { 1232 | return React.createElement( 1233 | ModalStuff.ModalRoot, 1234 | { tag: 'form', onSubmit: this.handleSubmit, size: '', transitionState: this.props.transitionState }, 1235 | React.createElement( 1236 | ModalStuff.ModalContent, 1237 | {}, 1238 | React.createElement( 1239 | FormItem, 1240 | { className: XenoLib.joinClassNames(DiscordClasses.Margins.marginTop20.value, DiscordClasses.Margins.marginBottom20.value) }, 1241 | React.createElement(ColorPickerComponent, { 1242 | defaultColor: this.props.defaultColor, 1243 | 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], 1244 | value: this.state.value, 1245 | onChange: this.handleChange 1246 | }) 1247 | ) 1248 | ) 1249 | ); 1250 | } 1251 | } 1252 | const NewModalStack = WebpackModules.getByProps('openModal', 'hasModalOpen'); 1253 | 1254 | const ExtraButtonClassname = 'xenoLib-button'; 1255 | const TextClassname = 'xl-text-1SHFy0'; 1256 | 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' })); 1257 | 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' })); 1258 | class ColorPicker extends React.PureComponent { 1259 | constructor(props) { 1260 | super(props); 1261 | this.state = { 1262 | error: null, 1263 | value: props.value, 1264 | multiInputFocused: false 1265 | }; 1266 | XenoLib._.bindAll(this, ['handleChange', 'handleColorPicker', 'handleReset']); 1267 | } 1268 | handleChange(value) { 1269 | if (!value.length) this.state.error = 'You must input a hex string'; 1270 | else if (!ColorConverter.isValidHex(value)) this.state.error = 'Invalid hex string'; 1271 | else this.state.error = null; 1272 | 1273 | this.setState({ value }); 1274 | this.props.onChange(!value.length || !ColorConverter.isValidHex(value) ? this.props.defaultColor : value); 1275 | } 1276 | handleColorPicker() { 1277 | const modalId = NewModalStack.openModal(e => React.createElement(XenoLib.ReactComponents.ErrorBoundary, { label: 'color picker modal', onError: () => NewModalStack.closeModal(modalId) }, React.createElement(ColorPickerModal, { ...e, defaultColor: ColorConverter.hex2int(this.props.defaultColor), value: ColorConverter.hex2int(this.props.value), onChange: this.handleChange }))); 1278 | } 1279 | handleReset() { 1280 | this.handleChange(this.props.defaultColor); 1281 | } 1282 | render() { 1283 | const n = {}; 1284 | n['xenoLib-multiInput-focused'] = this.state.multiInputFocused; 1285 | n[ErrorClassname] = !!this.state.error; 1286 | return React.createElement( 1287 | 'div', 1288 | { className: XenoLib.joinClassNames(DiscordClasses.BasicInputs.inputWrapper.value, 'xenoLib-color-picker'), style: { width: '100%' } }, 1289 | React.createElement( 1290 | 'div', 1291 | { className: XenoLib.joinClassNames(MultiInputClassname, n) }, 1292 | React.createElement('div', { 1293 | className: XenoLib.ReactComponents.Button.Sizes.SMALL, 1294 | style: { 1295 | backgroundColor: this.state.value, 1296 | height: 38 1297 | } 1298 | }), 1299 | React.createElement(DiscordModules.Textbox, { 1300 | value: this.state.value, 1301 | placeholder: 'Hex color', 1302 | onChange: this.handleChange, 1303 | onFocus: () => this.setState({ multiInputFocused: true }), 1304 | onBlur: () => this.setState({ multiInputFocused: false }), 1305 | autoFocus: false, 1306 | className: MultiInputFirstClassname, 1307 | inputClassName: MultiInputFieldClassname 1308 | }), 1309 | React.createElement( 1310 | XenoLib.ReactComponents.Button, 1311 | { 1312 | onClick: this.handleColorPicker, 1313 | color: (!!this.state.error && XenoLib.ReactComponents.ButtonOptions.ButtonColors.RED) || XenoLib.ReactComponents.ButtonOptions.ButtonColors.GREY, 1314 | look: XenoLib.ReactComponents.ButtonOptions.ButtonLooks.GHOST, 1315 | size: XenoLib.ReactComponents.Button.Sizes.MIN, 1316 | className: ExtraButtonClassname 1317 | }, 1318 | React.createElement('span', { className: TextClassname }, 'Color picker'), 1319 | React.createElement( 1320 | 'span', 1321 | { 1322 | className: 'xenoLib-button-icon' 1323 | }, 1324 | DropperIcon 1325 | ) 1326 | ), 1327 | React.createElement( 1328 | XenoLib.ReactComponents.Button, 1329 | { 1330 | onClick: this.handleReset, 1331 | color: (!!this.state.error && XenoLib.ReactComponents.ButtonOptions.ButtonColors.RED) || XenoLib.ReactComponents.ButtonOptions.ButtonColors.GREY, 1332 | look: XenoLib.ReactComponents.ButtonOptions.ButtonLooks.GHOST, 1333 | size: XenoLib.ReactComponents.Button.Sizes.MIN, 1334 | className: ExtraButtonClassname 1335 | }, 1336 | React.createElement('span', { className: TextClassname }, 'Reset'), 1337 | React.createElement( 1338 | 'span', 1339 | { 1340 | className: 'xenoLib-button-icon xenoLib-revert' 1341 | }, 1342 | ClockReverseIcon 1343 | ) 1344 | ) 1345 | ), 1346 | !!this.state.error && React.createElement('div', { className: ErrorMessageClassname, style: { color: 'hsl(359,calc(var(--saturation-factor, 1)*82.6%),59.4%)' } }, 'Error: ', this.state.error) 1347 | ); 1348 | } 1349 | } 1350 | XenoLib.Settings = {}; 1351 | XenoLib.Settings.FilePicker = class FilePickerSettingField extends Settings.SettingField { 1352 | constructor(name, note, value, onChange, options = { properties: ['openDirectory', 'createDirectory'], placeholder: 'Path to folder', defaultPath: '' }) { 1353 | super(name, note, onChange, XenoLib.ReactComponents.FilePicker || class b { }, { 1354 | onChange: reactElement => path => { 1355 | this.onChange(path ? path : options.defaultPath); 1356 | }, 1357 | path: value, 1358 | nullOnInvalid: true, 1359 | ...options 1360 | }); 1361 | } 1362 | }; 1363 | XenoLib.Settings.ColorPicker = class ColorPickerSettingField extends Settings.SettingField { 1364 | constructor(name, note, value, onChange, options = {}) { 1365 | super(name, note, onChange, ColorPicker, { 1366 | disabled: !!options.disabled, 1367 | onChange: reactElement => color => { 1368 | this.onChange(color); 1369 | }, 1370 | defaultColor: typeof options.defaultColor !== 'undefined' ? options.defaultColor : ColorConverter.int2hex(DiscordConstants.DEFAULT_ROLE_COLOR), 1371 | value 1372 | }); 1373 | } 1374 | }; 1375 | 1376 | XenoLib.Settings.PluginFooter = class PluginFooterField extends Settings.SettingField { 1377 | constructor(showChangelog) { 1378 | super('', '', NOOP, XenoLib.ReactComponents.PluginFooter, { 1379 | showChangelog 1380 | }); 1381 | } 1382 | }; 1383 | 1384 | XenoLib.changeName = (currentName, newName) => { 1385 | try { 1386 | const path = require('path'); 1387 | const fs = require('fs'); 1388 | const pluginsFolder = path.dirname(currentName); 1389 | const pluginName = path.basename(currentName).match(/^[^\.]+/)[0]; 1390 | if (pluginName === newName) return true; 1391 | const wasEnabled = BdApi.Plugins && BdApi.Plugins.isEnabled ? BdApi.Plugins.isEnabled(pluginName) : global.pluginCookie && pluginCookie[pluginName]; 1392 | fs.accessSync(currentName, fs.constants.W_OK | fs.constants.R_OK); 1393 | const files = fs.readdirSync(pluginsFolder); 1394 | files.forEach(file => { 1395 | if (!file.startsWith(pluginName) || file.startsWith(newName) || file.indexOf('.plugin.js') !== -1) return; 1396 | fs.renameSync(path.resolve(pluginsFolder, file), path.resolve(pluginsFolder, `${newName}${file.match(new RegExp(`^${pluginName}(.*)`))[1]}`)); 1397 | }); 1398 | fs.renameSync(currentName, path.resolve(pluginsFolder, `${newName}.plugin.js`)); 1399 | XenoLib.Notifications.success(`[**XenoLib**] \`${pluginName}\` file has been renamed to \`${newName}\``); 1400 | if ((!BdApi.Plugins || !BdApi.Plugins.isEnabled || !BdApi.Plugins.enable) && (!global.pluginCookie || !global.pluginModule)) BdApi.showConfirmationModal('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).'); 1401 | else { 1402 | if (!wasEnabled) return; 1403 | setTimeout(() => (BdApi.Plugins && BdApi.Plugins.enable ? BdApi.Plugins.enable(newName) : pluginModule.enablePlugin(newName)), 1000); /* /shrug */ 1404 | } 1405 | } catch (e) { 1406 | Logger.stacktrace('There has been an issue renaming a plugin', e); 1407 | } 1408 | }; 1409 | 1410 | const FancyParser = (() => { 1411 | const Markdown = WebpackModules.getByProps('astParserFor', 'parse'); 1412 | try { 1413 | const { default: DeepClone } = WebpackModules.find(m => { 1414 | if (!m.default || m.useVirtualizedAnchor || typeof m.default !== 'function') return false; 1415 | const toString = m.default.toString(); 1416 | return toString.indexOf('/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(') !== -1 && toString.search(/\w\({},\w\[\w\],{},\w\[\w\]\)/) !== -1; 1417 | }); 1418 | // SOOO much more extra code with zeres lib compared to Astra, maybe I just can't figure out how to use it effectively 1419 | const ReactParserRules = WebpackModules.find(m => typeof m === 'function' && (m = m.toString()) && (m.toString().replace(/\n/g, '').search(/^function\(\w\){return \w\({},\w,{link:\(0,\w.default\)\(\w\),emoji:\(\w=\w,\w=\w\.emojiTooltipPosition,\w=void 0===\w?\w/) !== -1)); 1420 | const FANCY_PANTS_PARSER_RULES = DeepClone([WebpackModules.getByProps('RULES').RULES, ReactParserRules({}), { mention: WebpackModules.find(e => e.Z && e.Z.react).Z }]); 1421 | const { defaultRules } = WebpackModules.getByProps('defaultParse'); 1422 | FANCY_PANTS_PARSER_RULES.image = defaultRules.image; 1423 | FANCY_PANTS_PARSER_RULES.link = defaultRules.link; 1424 | return Markdown.reactParserFor(FANCY_PANTS_PARSER_RULES); 1425 | } catch (e) { 1426 | //Logger.stacktrace('Failed to create special parser', e); 1427 | try { 1428 | return Markdown.parse; 1429 | } catch (e) { 1430 | Logger.stacktrace('Failed to even get basic parser', e); 1431 | return e => e; 1432 | } 1433 | } 1434 | })(); 1435 | 1436 | const AnchorClasses = WebpackModules.getByProps('anchor', 'anchorUnderlineOnHover') || {}; 1437 | const EmbedVideo = (() => { 1438 | return NOOP_NULL; 1439 | try { 1440 | return WebpackModules.getByProps('EmbedVideo').EmbedVideo; 1441 | } catch (e) { 1442 | Logger.stacktrace('Failed to get EmbedVideo!', e); 1443 | return NOOP_NULL; 1444 | } 1445 | })(); 1446 | const VideoComponent = (() => { 1447 | return NOOP_NULL; 1448 | try { 1449 | const ret = new (WebpackModules.getByDisplayName('MediaPlayer'))({}).render(); 1450 | const vc = Utilities.findInReactTree(ret, e => e && e.props && typeof e.props.className === 'string' && e.props.className.indexOf('video-2HW4jD') !== -1); 1451 | return vc.type; 1452 | } catch (e) { 1453 | Logger.stacktrace('Failed to get the video component', e); 1454 | return NOOP_NULL; 1455 | } 1456 | })(); 1457 | const ComponentRenderers = WebpackModules.getByProps('renderVideoComponent') || {}; 1458 | /* MY CHANGELOG >:C */ 1459 | XenoLib.showChangelog = (title, version, changelog, footer, showDisclaimer) => { 1460 | return; 1461 | const ChangelogClasses = DiscordClasses.Changelog; 1462 | const items = []; 1463 | let isFistType = true; 1464 | for (let i = 0; i < changelog.length; i++) { 1465 | const item = changelog[i]; 1466 | switch (item.type) { 1467 | case 'image': 1468 | items.push(React.createElement('img', { alt: '', src: item.src, width: item.width || 451, height: item.height || 254 })); 1469 | continue; 1470 | case 'video': 1471 | items.push(React.createElement(VideoComponent, { src: item.src, poster: item.thumbnail, width: item.width || 451, height: item.height || 254, loop: item.loop || !0, muted: item.muted || !0, autoPlay: item.autoplay || !0, className: ChangelogClasses.video })); 1472 | continue; 1473 | case 'youtube': 1474 | 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 || NOOP_NULL, renderImageComponent: ComponentRenderers.renderImageComponent || NOOP_NULL, renderLinkComponent: ComponentRenderers.renderMaskedLinkComponent || NOOP_NULL })); 1475 | continue; 1476 | case 'description': 1477 | items.push(React.createElement('p', {}, FancyParser(item.content))); 1478 | continue; 1479 | default: 1480 | const logType = ChangelogClasses[item.type] || ChangelogClasses.added; 1481 | items.push(React.createElement('h1', { className: XenoLib.joinClassNames(logType.value, { [ChangelogClasses.marginTop.value]: item.marginTop || isFistType }) }, item.title)); 1482 | items.push(React.createElement( 1483 | 'ul', 1484 | { className: 'XL-chl-p' }, 1485 | item.items.map(e => 1486 | React.createElement( 1487 | 'li', 1488 | {}, 1489 | React.createElement( 1490 | 'p', 1491 | {}, 1492 | Array.isArray(e) 1493 | ? e.map(e => 1494 | (Array.isArray(e) 1495 | ? React.createElement( 1496 | 'ul', 1497 | {}, 1498 | e.map(e => React.createElement('li', {}, FancyParser(e))) 1499 | ) 1500 | : FancyParser(e))) 1501 | : FancyParser(e) 1502 | ) 1503 | )) 1504 | )); 1505 | isFistType = false; 1506 | } 1507 | } 1508 | const renderFooter = () => ['Need support? ', React.createElement('a', { className: XenoLib.joinClassNames(AnchorClasses.anchor, AnchorClasses.anchorUnderlineOnHover), onClick: () => Modals.showConfirmationModal('Please confirm', 'Are you sure you want to join my support server?', { confirmText: 'Yes', cancelText: 'Nope', onConfirm: () => (LayerManager.popLayer(), ModalStack.pop(), NewModalStack.closeAllModals(), 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'), '!', showDisclaimer ? '\nBy using these plugins, you agree to being part of the anonymous user counter, unless disabled in settings.' : '']; 1509 | 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, variant: 'text-xs/normal', className: ChangelogClasses.date }, `Version ${version}`)), renderFooter: () => React.createElement(FlexChild.Child, { gro: 1, shrink: 1 }, React.createElement(TextElement, { size: TextElement?.Sizes?.SIZE_12, variant: 'text-xs/normal' }, footer ? (typeof footer === 'string' ? FancyParser(footer) : footer) : renderFooter())), children: items, ...props }))); 1510 | }; 1511 | 1512 | /* https://github.com/react-spring/zustand 1513 | * MIT License 1514 | * 1515 | * Copyright (c) 2019 Paul Henschel 1516 | * 1517 | * Permission is hereby granted, free of charge, to any person obtaining a copy 1518 | * of this software and associated documentation files (the "Software"), to deal 1519 | * in the Software without restriction, including without limitation the rights 1520 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 1521 | * copies of the Software, and to permit persons to whom the Software is 1522 | * furnished to do so, subject to the following conditions: 1523 | * 1524 | * The above copyright notice and this permission notice shall be included in all 1525 | * copies or substantial portions of the Software. 1526 | * 1527 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 1528 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 1529 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 1530 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 1531 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 1532 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 1533 | * SOFTWARE. 1534 | */ 1535 | XenoLib.zustand = createState => { 1536 | let state; 1537 | const listeners = new Set(); 1538 | const setState = partial => { 1539 | const partialState = typeof partial === 'function' ? partial(state) : partial; 1540 | if (partialState !== state) { 1541 | state = { ...state, ...partialState }; 1542 | listeners.forEach(listener => listener()); 1543 | } 1544 | }; 1545 | const getState = () => state; 1546 | const getSubscriber = (listener, selector, equalityFn) => { 1547 | if (selector === void 0) selector = getState; 1548 | if (equalityFn === void 0) equalityFn = Object.is; 1549 | return { currentSlice: selector(state), equalityFn, errored: false, listener, selector, unsubscribe: function unsubscribe() { } }; 1550 | }; 1551 | const subscribe = function subscribe(subscriber) { 1552 | function listener() { 1553 | // Selector or equality function could throw but we don't want to stop 1554 | // the listener from being called. 1555 | // https://github.com/react-spring/zustand/pull/37 1556 | try { 1557 | const newStateSlice = subscriber.selector(state); 1558 | if (!subscriber.equalityFn(subscriber.currentSlice, newStateSlice)) subscriber.listener((subscriber.currentSlice = newStateSlice)); 1559 | } catch (error) { 1560 | subscriber.errored = true; 1561 | subscriber.listener(null, error); 1562 | } 1563 | } 1564 | 1565 | listeners.add(listener); 1566 | return () => listeners.delete(listener); 1567 | }; 1568 | const apiSubscribe = (listener, selector, equalityFn) => subscribe(getSubscriber(listener, selector, equalityFn)); 1569 | const destroy = () => listeners.clear(); 1570 | const useStore = (selector, equalityFn) => { 1571 | if (selector === void 0) selector = getState; 1572 | if (equalityFn === void 0) equalityFn = Object.is; 1573 | const forceUpdate = React.useReducer(c => c + 1, 0)[1]; 1574 | const subscriberRef = React.useRef(); 1575 | if (!subscriberRef.current) { 1576 | subscriberRef.current = getSubscriber(forceUpdate, selector, equalityFn); 1577 | subscriberRef.current.unsubscribe = subscribe(subscriberRef.current); 1578 | } 1579 | const subscriber = subscriberRef.current; 1580 | let newStateSlice; 1581 | let hasNewStateSlice = false; // The selector or equalityFn need to be called during the render phase if 1582 | // they change. We also want legitimate errors to be visible so we re-run 1583 | // them if they errored in the subscriber. 1584 | if (subscriber.selector !== selector || subscriber.equalityFn !== equalityFn || subscriber.errored) { 1585 | // Using local variables to avoid mutations in the render phase. 1586 | newStateSlice = selector(state); 1587 | hasNewStateSlice = !equalityFn(subscriber.currentSlice, newStateSlice); 1588 | } // Syncing changes in useEffect. 1589 | React.useLayoutEffect(() => { 1590 | if (hasNewStateSlice) subscriber.currentSlice = newStateSlice; 1591 | subscriber.selector = selector; 1592 | subscriber.equalityFn = equalityFn; 1593 | subscriber.errored = false; 1594 | }); 1595 | React.useLayoutEffect(() => subscriber.unsubscribe, []); 1596 | return hasNewStateSlice ? newStateSlice : subscriber.currentSlice; 1597 | }; 1598 | const api = { setState, getState, subscribe: apiSubscribe, destroy }; 1599 | state = createState(setState, getState, api); 1600 | return [useStore, api]; 1601 | }; 1602 | /* NOTIFICATIONS START */ 1603 | let UPDATEKEY = {}; 1604 | const notificationEvents = canUseAstraNotifAPI ? null : new (require('events').EventEmitter)(); 1605 | if (!canUseAstraNotifAPI) notificationEvents.dispatch = data => notificationEvents.emit(data.type, data); 1606 | try { 1607 | if (canUseAstraNotifAPI) { 1608 | const defaultOptions = { 1609 | loading: false, 1610 | progress: -1, 1611 | channelId: undefined, 1612 | timeout: 3500, 1613 | color: '#2196f3', 1614 | onLeave: NOOP 1615 | }; 1616 | const utils = { 1617 | success(content, options = {}) { 1618 | return this.show(content, { color: '#43b581', ...options }); 1619 | }, 1620 | info(content, options = {}) { 1621 | return this.show(content, { color: '#4a90e2', ...options }); 1622 | }, 1623 | warning(content, options = {}) { 1624 | return this.show(content, { color: '#ffa600', ...options }); 1625 | }, 1626 | danger(content, options = {}) { 1627 | return this.show(content, { color: '#f04747', ...options }); 1628 | }, 1629 | error(content, options = {}) { 1630 | return this.danger(content, options); 1631 | }, 1632 | /** 1633 | * @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/) 1634 | * @param {object} options 1635 | * @param {string} [options.channelId] Channel ID if content is a string which gets formatted, and you want to mention a role for example. 1636 | * @param {Number} [options.timeout] Set to 0 to keep it permanently until user closes it, or if you want a progress bar 1637 | * @param {Boolean} [options.loading] Makes the bar animate differently instead of fading in and out slowly 1638 | * @param {Number} [options.progress] 0-100, -1 sets it to 100%, setting it to 100% closes the notification automatically 1639 | * @param {string} [options.color] Bar color 1640 | * @param {string} [options.allowDuplicates] By default, notifications that are similar get grouped together, use true to disable that 1641 | * @param {function} [options.onLeave] Callback when notification is leaving 1642 | * @return {Number} - Notification ID. Store this if you plan on force closing it, changing its content or want to set the progress 1643 | */ 1644 | show(content, options = {}) { 1645 | const { timeout, loading, progress, color, allowDuplicates, onLeave, channelId, onClick, onContext, onMiddleClick } = Object.assign(Utilities.deepclone(defaultOptions), options); 1646 | return Astra.n11s.show(content instanceof HTMLElement ? ReactTools.createWrappedElement(content) : content, { 1647 | timeout, 1648 | loading, 1649 | progress, 1650 | color, 1651 | allowDuplicates, 1652 | onClick, 1653 | onContext, 1654 | onMiddleClick, 1655 | onClose: onLeave, 1656 | markdownOptions: { channelId } 1657 | }); 1658 | }, 1659 | remove(id) { 1660 | Astra.n11s.remove(id); 1661 | }, 1662 | /** 1663 | * @param {Number} id Notification ID 1664 | * @param {object} options 1665 | * @param {string} [options.channelId] Channel ID if content is a string which gets formatted, and you want to mention a role for example. 1666 | * @param {Boolean} [options.loading] Makes the bar animate differently instead of fading in and out slowly 1667 | * @param {Number} [options.progress] 0-100, -1 sets it to 100%, setting it to 100% closes the notification automatically 1668 | * @param {string} [options.color] Bar color 1669 | * @param {function} [options.onLeave] Callback when notification is leaving 1670 | */ 1671 | update(id, options) { 1672 | const obj = {}; 1673 | for (const key of ['content', 'timeout', 'loading', 'progress', 'color', 'onClick', 'onContext', 'onMiddleClick']) if (typeof options[key] !== 'undefined') obj[key] = options[key]; 1674 | if (options.onLeave) obj.onClose = options.onLeave; 1675 | if (options.channelId) obj.markdownOptions = { channelId: options.channelId }; 1676 | Astra.n11s.update(id, obj); 1677 | }, 1678 | exists(id) { 1679 | return Astra.n11s.exists(id); 1680 | } 1681 | }; 1682 | XenoLib.Notifications = utils; 1683 | } else { 1684 | const DeepEqualityCheck = (content1, content2) => { 1685 | if (typeof content1 !== typeof content2) return false; 1686 | const isCNT1HTML = content1 instanceof HTMLElement; 1687 | const isCNT2HTML = content2 instanceof HTMLElement; 1688 | if (isCNT1HTML !== isCNT2HTML) return false; 1689 | else if (isCNT1HTML) return content1.isEqualNode(content2); 1690 | if (content1 !== content2) if (Array.isArray(content1)) { 1691 | if (content1.length !== content2.length) return false; 1692 | for (const [index, item] of content1.entries()) if (!DeepEqualityCheck(item, content2[index])) return false; 1693 | 1694 | } else if (typeof content1 === 'object') { 1695 | if (content1.type) { 1696 | if (typeof content1.type !== typeof content2.type) return false; 1697 | if (content1.type !== content2.type) return false; 1698 | } 1699 | if (typeof content1.props !== typeof content2.props) return false; 1700 | if (content1.props) { 1701 | if (Object.keys(content1.props).length !== Object.keys(content2.props).length) return false; 1702 | for (const prop in content1.props) if (!DeepEqualityCheck(content1.props[prop], content2.props[prop])) return false; 1703 | 1704 | } 1705 | } else return false; 1706 | 1707 | return true; 1708 | }; 1709 | const [useStore, api] = XenoLib.zustand(e => ({ data: [] })); 1710 | const defaultOptions = { 1711 | loading: false, 1712 | progress: -1, 1713 | channelId: undefined, 1714 | timeout: 3500, 1715 | color: '#2196f3', 1716 | onLeave: NOOP 1717 | }; 1718 | const utils = { 1719 | success(content, options = {}) { 1720 | return this.show(content, { color: '#43b581', ...options }); 1721 | }, 1722 | info(content, options = {}) { 1723 | return this.show(content, { color: '#4a90e2', ...options }); 1724 | }, 1725 | warning(content, options = {}) { 1726 | return this.show(content, { color: '#ffa600', ...options }); 1727 | }, 1728 | danger(content, options = {}) { 1729 | return this.show(content, { color: '#f04747', ...options }); 1730 | }, 1731 | error(content, options = {}) { 1732 | return this.danger(content, options); 1733 | }, 1734 | /** 1735 | * @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/) 1736 | * @param {object} options 1737 | * @param {string} [options.channelId] Channel ID if content is a string which gets formatted, and you want to mention a role for example. 1738 | * @param {Number} [options.timeout] Set to 0 to keep it permanently until user closes it, or if you want a progress bar 1739 | * @param {Boolean} [options.loading] Makes the bar animate differently instead of fading in and out slowly 1740 | * @param {Number} [options.progress] 0-100, -1 sets it to 100%, setting it to 100% closes the notification automatically 1741 | * @param {string} [options.color] Bar color 1742 | * @param {string} [options.allowDuplicates] By default, notifications that are similar get grouped together, use true to disable that 1743 | * @param {function} [options.onLeave] Callback when notification is leaving 1744 | * @return {Number} - Notification ID. Store this if you plan on force closing it, changing its content or want to set the progress 1745 | */ 1746 | show(content, options = {}) { 1747 | let id = null; 1748 | options = Object.assign(Utilities.deepclone(defaultOptions), options); 1749 | api.setState(state => { 1750 | if (!options.allowDuplicates) { 1751 | const notif = state.data.find(n => DeepEqualityCheck(n.content, content) && n.timeout === options.timeout && !n.leaving); 1752 | if (notif) { 1753 | id = notif.id; 1754 | notificationEvents.dispatch({ type: 'XL_NOTIFS_DUPLICATE', id: notif.id }); 1755 | return state; 1756 | } 1757 | } 1758 | if (state.data.length >= 100) return state; 1759 | do id = Math.floor(4294967296 * Math.random()); 1760 | while (state.data.findIndex(n => n.id === id) !== -1); 1761 | return { data: [].concat(state.data, [{ content, ...options, id }]) }; 1762 | }); 1763 | return id; 1764 | }, 1765 | remove(id) { 1766 | notificationEvents.dispatch({ type: 'XL_NOTIFS_REMOVE', id }); 1767 | }, 1768 | /** 1769 | * @param {Number} id Notification ID 1770 | * @param {object} options 1771 | * @param {string} [options.channelId] Channel ID if content is a string which gets formatted, and you want to mention a role for example. 1772 | * @param {Boolean} [options.loading] Makes the bar animate differently instead of fading in and out slowly 1773 | * @param {Number} [options.progress] 0-100, -1 sets it to 100%, setting it to 100% closes the notification automatically 1774 | * @param {string} [options.color] Bar color 1775 | * @param {function} [options.onLeave] Callback when notification is leaving 1776 | */ 1777 | update(id, options) { 1778 | delete options.id; 1779 | api.setState(state => { 1780 | const idx = state.data.findIndex(n => n.id === id); 1781 | if (idx === -1) return state; 1782 | state.data[idx] = Object.assign(state.data[idx], options); 1783 | return state; 1784 | }); 1785 | notificationEvents.dispatch({ type: 'XL_NOTIFS_UPDATE', id, ...options }); 1786 | }, 1787 | exists(id) { 1788 | return api.getState().data.findIndex(e => e.id === id && !e.leaving) !== -1; 1789 | } 1790 | }; 1791 | XenoLib.Notifications = utils; 1792 | const ReactSpring = (() => { 1793 | const olfilter = Array.prototype.filter 1794 | Array.prototype.filter = function (callbackFn, thisArg) { 1795 | return []; 1796 | } 1797 | try { 1798 | return WebpackModules.getByProps('useTransition') 1799 | } finally { 1800 | Array.prototype.filter = olfilter; 1801 | } 1802 | })(); 1803 | 1804 | function hex2int(hex) { 1805 | return parseInt(hex, 16); 1806 | } 1807 | 1808 | function int2rgba(int, alpha) { 1809 | const r = int >> 16 & 255; 1810 | const g = int >> 8 & 255; 1811 | const b = int & 255; 1812 | return `rgba(${r}, ${g}, ${b}, ${alpha})`; 1813 | } 1814 | 1815 | const BadgesModule = WebpackModules.getByProps('NumberBadge'); 1816 | 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' })); 1817 | class Notification extends React.PureComponent { 1818 | constructor(props) { 1819 | super(props); 1820 | this.state = { 1821 | closeFast: false /* close button pressed, XL_NOTIFS_REMOVE dispatch */, 1822 | offscreen: false /* don't do anything special if offscreen, not timeout */, 1823 | counter: 1 /* how many times this notification was shown */, 1824 | resetBar: false /* reset bar to 0 in the event counter goes up */, 1825 | hovered: false, 1826 | leaving: true /* prevent hover events from messing up things */, 1827 | loading: props.loading /* loading animation, enable progress */, 1828 | progress: props.progress /* -1 means undetermined */, 1829 | content: props.content, 1830 | contentParsed: this.parseContent(props.content, props.channelId), 1831 | color: props.color 1832 | }; 1833 | this._contentRef = null; 1834 | this._ref = null; 1835 | this._animationCancel = NOOP; 1836 | this._oldOffsetHeight = 0; 1837 | this._initialProgress = !this.props.timeout ? (this.state.loading && this.state.progress !== -1 ? this.state.progress : 100) : 0; 1838 | XenoLib._.bindAll(this, ['closeNow', 'handleDispatch', '_setContentRef', 'onMouseEnter', 'onMouseLeave']); 1839 | this.handleResizeEvent = XenoLib._.throttle(this.handleResizeEvent.bind(this), 100); 1840 | this.resizeObserver = new ResizeObserver(this.handleResizeEvent); 1841 | this._timeout = props.timeout; 1842 | } 1843 | componentDidMount() { 1844 | this._unsubscribe = api.subscribe(_ => this.checkOffScreen()); 1845 | window.addEventListener('resize', this.handleResizeEvent); 1846 | notificationEvents.on('XL_NOTIFS_DUPLICATE', this.handleDispatch); 1847 | notificationEvents.on('XL_NOTIFS_REMOVE', this.handleDispatch); 1848 | notificationEvents.on('XL_NOTIFS_UPDATE', this.handleDispatch); 1849 | notificationEvents.on('XL_NOTIFS_ANIMATED', this.handleDispatch); 1850 | notificationEvents.on('XL_NOTIFS_SETTINGS_UPDATE', this.handleDispatch); 1851 | } 1852 | componentWillUnmount() { 1853 | this._unsubscribe(); 1854 | window.window.removeEventListener('resize', this.handleResizeEvent); 1855 | notificationEvents.off('XL_NOTIFS_DUPLICATE', this.handleDispatch); 1856 | notificationEvents.off('XL_NOTIFS_REMOVE', this.handleDispatch); 1857 | notificationEvents.off('XL_NOTIFS_UPDATE', this.handleDispatch); 1858 | notificationEvents.off('XL_NOTIFS_ANIMATED', this.handleDispatch); 1859 | notificationEvents.off('XL_NOTIFS_SETTINGS_UPDATE', this.handleDispatch); 1860 | this.resizeObserver.disconnect(); 1861 | this.resizeObserver = null; /* no mem leaks plz */ 1862 | this._ref = null; 1863 | this._contentRef = null; 1864 | } 1865 | handleDispatch(e) { 1866 | if (this.state.leaving || this.state.closeFast) return; 1867 | if (e.type === 'XL_NOTIFS_SETTINGS_UPDATE') { 1868 | if (e.key !== UPDATEKEY) return; 1869 | this._animationCancel(); 1870 | this.forceUpdate(); 1871 | return; 1872 | } 1873 | if (e.type === 'XL_NOTIFS_ANIMATED') this.checkOffScreen(); 1874 | if (e.id !== this.props.id) return; 1875 | const { content, channelId, loading, progress, color } = e; 1876 | const { content: curContent, channelId: curChannelId, loading: curLoading, progress: curProgress, color: curColor } = this.state; 1877 | switch (e.type) { 1878 | case 'XL_NOTIFS_REMOVE': 1879 | this.closeNow(); 1880 | break; 1881 | case 'XL_NOTIFS_DUPLICATE': 1882 | this._animationCancel(); 1883 | this.setState({ counter: this.state.counter + 1, resetBar: !!this.props.timeout, closeFast: false }); 1884 | break; 1885 | case 'XL_NOTIFS_UPDATE': 1886 | /* if (!this.state.initialAnimDone) { 1887 | this.state.content = content || curContent; 1888 | this.state.channelId = channelId || curChannelId; 1889 | this.state.contentParsed = this.parseContent(content || curContent, channelId || curChannelId); 1890 | if (typeof loading !== 'undefined') this.state.loading = loading; 1891 | if (typeof progress !== 'undefined') this.state.progress = progress; 1892 | this.state.color = color || curColor; 1893 | return; 1894 | }*/ 1895 | if (this.state.initialAnimDone) this._animationCancel(); 1896 | this.setState({ 1897 | content: content || curContent, 1898 | channelId: channelId || curChannelId, 1899 | contentParsed: this.parseContent(content || curContent, channelId || curChannelId), 1900 | loading: typeof loading !== 'undefined' ? loading : curLoading, 1901 | progress: typeof progress !== 'undefined' ? progress : curProgress, 1902 | color: color || curColor 1903 | }); 1904 | break; 1905 | } 1906 | } 1907 | parseContent(content, channelId) { 1908 | if (typeof content === 'string') return FancyParser(content, true, { channelId }); 1909 | else if (content instanceof Element) return ReactTools.createWrappedElement(content); 1910 | return content; 1911 | } 1912 | checkOffScreen() { 1913 | if (this.state.leaving || !this._contentRef) return; 1914 | const bcr = this._contentRef.getBoundingClientRect(); 1915 | if (Math.floor(bcr.bottom) - 1 > Structs.Screen.height || Math.ceil(bcr.top) + 1 < 0) { 1916 | if (!this.state.offscreen) { 1917 | this._animationCancel(); 1918 | this.setState({ offscreen: true }); 1919 | } 1920 | } else if (this.state.offscreen) { 1921 | this._animationCancel(); 1922 | this.setState({ offscreen: false }); 1923 | } 1924 | } 1925 | closeNow() { 1926 | if (this.state.closeFast) return; 1927 | this.resizeObserver.disconnect(); 1928 | this._animationCancel(); 1929 | api.setState(state => { 1930 | const dt = state.data.find(m => m.id === this.props.id); 1931 | if (dt) dt.leaving = true; 1932 | return { data: state.data }; 1933 | }); 1934 | this.setState({ closeFast: true }); 1935 | } 1936 | handleResizeEvent() { 1937 | if (this._oldOffsetHeight !== this._contentRef.offsetHeight) { 1938 | this._animationCancel(); 1939 | this.forceUpdate(); 1940 | } 1941 | } 1942 | _setContentRef(ref) { 1943 | if (this._contentRef) { 1944 | this._contentRef.removeEventListener('mouseenter', this.onMouseEnter); 1945 | this._contentRef.removeEventListener('mouseleave', this.onMouseLeave); 1946 | } 1947 | if (!ref) return; 1948 | ref.addEventListener('mouseenter', this.onMouseEnter); 1949 | ref.addEventListener('mouseleave', this.onMouseLeave); 1950 | this._contentRef = ref; 1951 | this.resizeObserver.observe(ref); 1952 | } 1953 | onMouseEnter(e) { 1954 | if (this.state.leaving || !this.props.timeout || this.state.closeFast) return; 1955 | this._animationCancel(); 1956 | if (this._startProgressing) this._timeout -= Date.now() - this._startProgressing; 1957 | this.setState({ hovered: true }); 1958 | } 1959 | onMouseLeave(e) { 1960 | if (this.state.leaving || !this.props.timeout || this.state.closeFast) return; 1961 | this._animationCancel(); 1962 | this.setState({ hovered: false }); 1963 | } 1964 | render() { 1965 | const config = { duration: 200 }; 1966 | if (this._contentRef) this._oldOffsetHeight = this._contentRef.offsetHeight; 1967 | return React.createElement( 1968 | ReactSpring.Spring, 1969 | { 1970 | native: true, 1971 | from: { opacity: 0, height: 0, progress: this._initialProgress, loadbrightness: 1 }, 1972 | to: async (next, cancel) => { 1973 | this.state.leaving = false; 1974 | this._animationCancel = cancel; 1975 | if (this.state.offscreen) { 1976 | if (this.state.closeFast) { 1977 | this.state.leaving = true; 1978 | await next({ opacity: 0, height: 0 }); 1979 | api.setState(state => ({ data: state.data.filter(n => n.id !== this.props.id) })); 1980 | return; 1981 | } 1982 | await next({ opacity: 1, height: this._contentRef.offsetHeight, loadbrightness: 1 }); 1983 | if (this.props.timeout) await next({ progress: 0 }); 1984 | else 1985 | if (this.state.loading && this.state.progress !== -1) await next({ progress: 0 }); 1986 | else await next({ progress: 100 }); 1987 | 1988 | 1989 | return; 1990 | } 1991 | const isSettingHeight = this._ref.offsetHeight !== this._contentRef.offsetHeight; 1992 | await next({ opacity: 1, height: this._contentRef.offsetHeight }); 1993 | if (isSettingHeight) notificationEvents.dispatch({ type: 'XL_NOTIFS_ANIMATED' }); 1994 | this.state.initialAnimDone = true; 1995 | if (this.state.resetBar || (this.state.hovered && LibrarySettings.notifications.timeoutReset)) { 1996 | await next({ progress: 0 }); /* shit gets reset */ 1997 | this.state.resetBar = false; 1998 | } 1999 | 2000 | if (!this.props.timeout && !this.state.closeFast) { 2001 | if (!this.state.loading) await next({ progress: 100 }); 2002 | else { 2003 | await next({ loadbrightness: 1 }); 2004 | if (this.state.progress === -1) await next({ progress: 100 }); 2005 | else await next({ progress: this.state.progress }); 2006 | } 2007 | if (this.state.progress < 100 || !this.state.loading) return; 2008 | } 2009 | if (this.state.hovered && !this.state.closeFast) return; 2010 | if (!this.state.closeFast && !LibrarySettings.notifications.timeoutReset) this._startProgressing = Date.now(); 2011 | await next({ progress: 100 }); 2012 | if (this.state.hovered && !this.state.closeFast) return; /* race condition: notif is hovered, but it continues and closes! */ 2013 | this.state.leaving = true; 2014 | if (!this.state.closeFast) api.setState(state => { 2015 | const dt = state.data.find(m => m.id === this.props.id); 2016 | if (dt) dt.leaving = true; 2017 | return { data: state.data }; 2018 | }); 2019 | 2020 | this.props.onLeave(); 2021 | await next({ opacity: 0, height: 0 }); 2022 | api.setState(state => ({ data: state.data.filter(n => n.id !== this.props.id) })); 2023 | }, 2024 | config: key => { 2025 | if (key === 'progress') { 2026 | let duration = this._timeout; 2027 | if (this.state.closeFast || !this.props.timeout || this.state.resetBar || this.state.hovered) duration = 150; 2028 | if (this.state.offscreen) duration = 0; /* don't animate at all */ 2029 | return { duration }; 2030 | } 2031 | if (key === 'loadbrightness') return { duration: 750 }; 2032 | return config; 2033 | } 2034 | }, 2035 | e => React.createElement( 2036 | ReactSpring.animated.div, 2037 | { 2038 | style: { 2039 | height: e.height, 2040 | opacity: e.opacity 2041 | }, 2042 | className: 'xenoLib-notification', 2043 | ref: e => e && (this._ref = e) 2044 | }, 2045 | React.createElement( 2046 | 'div', 2047 | { 2048 | className: 'xenoLib-notification-content-wrapper', 2049 | ref: this._setContentRef, 2050 | style: { 2051 | '--grad-one': this.state.color, 2052 | '--grad-two': ColorConverter.lightenColor(this.state.color, 20), 2053 | '--bar-color': ColorConverter.darkenColor(this.state.color, 30) 2054 | }, 2055 | onClick: e => { 2056 | if (!this.props.onClick) return; 2057 | if (e.target && e.target.getAttribute('role') === 'button') return; 2058 | this.props.onClick(e); 2059 | this.closeNow(); 2060 | }, 2061 | onContextMenu: e => { 2062 | if (!this.props.onContext) return; 2063 | this.props.onContext(e); 2064 | this.closeNow(); 2065 | }, 2066 | onMouseUp: e => { 2067 | if (!this.props.onMiddleClick || e.button !== 1) return; 2068 | if (e.target && e.target.getAttribute('role') === 'button') return; 2069 | this.props.onMiddleClick(e); 2070 | this.closeNow(); 2071 | } 2072 | }, 2073 | React.createElement( 2074 | 'div', 2075 | { 2076 | className: 'xenoLib-notification-content', 2077 | style: { 2078 | backdropFilter: LibrarySettings.notifications.backdrop ? 'blur(5px)' : undefined, 2079 | background: int2rgba(hex2int(LibrarySettings.notifications.backdropColor), LibrarySettings.notifications.backdrop ? 0.3 : 1.0), 2080 | border: LibrarySettings.notifications.backdrop ? 'none' : undefined 2081 | }, 2082 | ref: e => { 2083 | if (!LibrarySettings.notifications.backdrop || !e) return; 2084 | e.style.setProperty('backdrop-filter', e.style.backdropFilter, 'important'); 2085 | e.style.setProperty('background', e.style.background, 'important'); 2086 | e.style.setProperty('border', e.style.border, 'important'); 2087 | } 2088 | }, 2089 | React.createElement(ReactSpring.animated.div, { 2090 | 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 }), 2091 | style: { right: e.progress.to(e => `${100 - e}%`), filter: e.loadbrightness.to(e => `brightness(${e * 100}%)`) } 2092 | }), 2093 | React.createElement( 2094 | XenoLib.ReactComponents.Button, 2095 | { 2096 | look: XenoLib.ReactComponents.Button.Looks.BLANK, 2097 | size: XenoLib.ReactComponents.Button.Sizes.NONE, 2098 | onClick: e => { 2099 | e.preventDefault(); 2100 | e.stopPropagation(); 2101 | this.closeNow(); 2102 | }, 2103 | onContextMenu: e => { 2104 | e.preventDefault(); 2105 | e.stopPropagation(); 2106 | const state = api.getState(); 2107 | state.data.forEach(notif => utils.remove(notif.id)); 2108 | }, 2109 | className: 'xenoLib-notification-close' 2110 | }, 2111 | CloseButton 2112 | ), 2113 | /*this.state.counter > 1 && BadgesModule.NumberBadge({ count: this.state.counter, className: 'xenLib-notification-counter', color: '#2196f3' }),*/ 2114 | this.state.counter > 1, 2115 | this.state.contentParsed 2116 | ) 2117 | ) 2118 | ) 2119 | ); 2120 | } 2121 | } 2122 | function NotificationsWrapper(e) { 2123 | const notifications = useStore(e => e.data); 2124 | 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(); 2125 | } 2126 | NotificationsWrapper.displayName = 'XenoLibNotifications'; 2127 | const DOMElement = document.createElement('div'); 2128 | document.querySelector('#app-mount').appendChild(DOMElement); // fucking incompetent powercord needs me to append it first 2129 | DOMElement.className = XenoLib.joinClassNames('xenoLib-notifications', `xenoLib-centering-${LibrarySettings.notifications.position}`); 2130 | ReactDOM.render(React.createElement(NotificationsWrapper, {}), DOMElement); 2131 | } 2132 | } catch (e) { 2133 | Logger.stacktrace('There has been an error loading the Notifications system, fallback object has been put in place to prevent errors', e); 2134 | XenoLib.Notifications = { 2135 | success(content, options = {}) { }, 2136 | info(content, options = {}) { }, 2137 | warning(content, options = {}) { }, 2138 | danger(content, options = {}) { }, 2139 | error(content, options = {}) { }, 2140 | show(content, options = {}) { }, 2141 | remove(id) { }, 2142 | update(id, options) { } 2143 | }; 2144 | } 2145 | /* NOTIFICATIONS END */ 2146 | 2147 | global.XenoLib = XenoLib; 2148 | 2149 | const notifLocations = ['topLeft', 'topMiddle', 'topRight', 'bottomLeft', 'bottomMiddle', 'bottomRight']; 2150 | const notifLocationClasses = [ 2151 | 'topLeft-xenoLib option-xenoLib', 2152 | 'topMiddle-xenoLib option-xenoLib', 2153 | 'topRight-xenoLib option-xenoLib', 2154 | 'bottomLeft-xenoLib option-xenoLib', 2155 | 'bottomMiddle-xenoLib option-xenoLib', 2156 | 'bottomRight-xenoLib option-xenoLib' 2157 | ]; 2158 | const PositionSelectorWrapperClassname = 'xenoLib-position-wrapper'; 2159 | const PositionSelectorSelectedClassname = 'selected-xenoLib'; 2160 | const PositionSelectorHiddenInputClassname = 'xenoLib-position-hidden-input'; 2161 | const FormText = WebpackModules.getByDisplayName('FormText'); 2162 | class NotificationPosition extends React.PureComponent { 2163 | constructor(props) { 2164 | super(props); 2165 | this.state = { 2166 | position: props.position 2167 | }; 2168 | } 2169 | componentDidMount() { 2170 | this._notificationId = XenoLib.Notifications.show('Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur lacinia justo eget libero ultrices mollis.', { timeout: 0 }); 2171 | } 2172 | componentWillUnmount() { 2173 | XenoLib.Notifications.remove(this._notificationId); 2174 | } 2175 | getSelected() { 2176 | switch (this.state.position) { 2177 | case 'topLeft': 2178 | return 'Top Left'; 2179 | case 'topMiddle': 2180 | return 'Top Middle'; 2181 | case 'topRight': 2182 | return 'Top Right'; 2183 | case 'bottomLeft': 2184 | return 'Bottom Left'; 2185 | case 'bottomMiddle': 2186 | return 'Bottom Middle'; 2187 | case 'bottomRight': 2188 | return 'Bottom Right'; 2189 | default: 2190 | return 'Unknown'; 2191 | } 2192 | } 2193 | render() { 2194 | return React.createElement( 2195 | 'div', 2196 | {}, 2197 | React.createElement( 2198 | 'div', 2199 | { 2200 | className: PositionSelectorWrapperClassname 2201 | }, 2202 | notifLocations.map((e, i) => React.createElement( 2203 | 'label', 2204 | { 2205 | className: XenoLib.joinClassNames(notifLocationClasses[i], { [PositionSelectorSelectedClassname]: this.state.position === e }) 2206 | }, 2207 | React.createElement('input', { 2208 | type: 'radio', 2209 | name: 'xenolib-notif-position-selector', 2210 | value: e, 2211 | onChange: () => { 2212 | this.props.onChange(e); 2213 | this.setState({ position: e }); 2214 | }, 2215 | className: PositionSelectorHiddenInputClassname 2216 | }) 2217 | )) 2218 | ), 2219 | React.createElement( 2220 | FormText, 2221 | { 2222 | type: FormText.Types.DESCRIPTION, 2223 | className: DiscordClasses.Margins.marginTop8 2224 | }, 2225 | this.getSelected() 2226 | ) 2227 | ); 2228 | } 2229 | } 2230 | 2231 | class NotificationPositionField extends Settings.SettingField { 2232 | constructor(name, note, onChange, value) { 2233 | super(name, note, onChange, NotificationPosition, { 2234 | position: value, 2235 | onChange: reactElement => position => { 2236 | this.onChange(position); 2237 | } 2238 | }); 2239 | } 2240 | } 2241 | 2242 | class RadioGroupWrapper extends React.PureComponent { 2243 | render() { 2244 | return React.createElement(DiscordModules.RadioGroup, this.props); 2245 | } 2246 | } 2247 | 2248 | class RadioGroup extends Settings.SettingField { 2249 | constructor(name, note, defaultValue, values, onChange, options = {}) { 2250 | super(name, note, onChange, RadioGroupWrapper, { 2251 | noteOnTop: true, 2252 | disabled: !!options.disabled, 2253 | options: values, 2254 | onChange: reactElement => option => { 2255 | reactElement.props.value = option.value; 2256 | reactElement.forceUpdate(); 2257 | this.onChange(option.value); 2258 | }, 2259 | value: defaultValue 2260 | }); 2261 | } 2262 | } 2263 | 2264 | 2265 | class SwitchItemWrapper extends React.PureComponent { 2266 | render() { 2267 | return React.createElement(DiscordModules.SwitchRow, this.props); 2268 | } 2269 | } 2270 | 2271 | class Switch extends Settings.SettingField { 2272 | constructor(name, note, isChecked, onChange, options = {}) { 2273 | super(name, note, onChange); 2274 | this.disabled = !!options.disabled; 2275 | this.value = !!isChecked; 2276 | } 2277 | 2278 | onAdded() { 2279 | const reactElement = ReactDOM.render(React.createElement(SwitchItemWrapper, { 2280 | children: this.name, 2281 | note: this.note, 2282 | disabled: this.disabled, 2283 | hideBorder: false, 2284 | value: this.value, 2285 | onChange: e => { 2286 | reactElement.props.value = e; 2287 | reactElement.forceUpdate(); 2288 | this.onChange(e); 2289 | } 2290 | }), this.getElement()); 2291 | } 2292 | } 2293 | 2294 | class TimerWrapper extends React.PureComponent { 2295 | constructor(...args) { 2296 | super(...args); 2297 | this.moment = Moment(this.props.value + this.props.time); 2298 | } 2299 | componentDidUpdate() { 2300 | this.moment = Moment(this.props.value + this.props.time); 2301 | } 2302 | componentDidMount() { 2303 | const { moment } = this; 2304 | const vv = moment.clone().seconds(0).add(1, 'm').diff(moment); 2305 | this.timer = setInterval(() => { 2306 | clearInterval(this.timer); 2307 | this.timer = setInterval(() => this.forceUpdate(), 60 * 1000); 2308 | this.forceUpdate(); 2309 | }, vv); 2310 | } 2311 | componentWillUnmount() { 2312 | if (this.timer) clearInterval(this.timer); 2313 | } 2314 | render() { 2315 | const { value, after, active, inactive, time } = this.props; 2316 | const future = (value + time); 2317 | return React.createElement(TextElement, {}, value ? Date.now() > future ? active : `${after}${this.moment.fromNow()}` : inactive); 2318 | } 2319 | } 2320 | 2321 | class Timer extends Settings.SettingField { 2322 | constructor(name, note, value, onChange, after, active, inactive, time) { 2323 | super(name, note, onChange, TimerWrapper, { after, active, inactive, time, value }); 2324 | } 2325 | } 2326 | 2327 | XenoLib.buildSetting = function buildSetting(data) { 2328 | const { name, note, type, value, onChange, id } = data; 2329 | let setting = null; 2330 | if (type == 'color') setting = new XenoLib.Settings.ColorPicker(name, note, value, onChange, { disabled: data.disabled, defaultColor: value }); 2331 | else if (type == 'dropdown') setting = new Settings.Dropdown(name, note, value, data.options, onChange); 2332 | else if (type == 'file') setting = new Settings.FilePicker(name, note, onChange); 2333 | else if (type == 'keybind') setting = new Settings.Keybind(name, note, value, onChange); 2334 | else if (type == 'radio') setting = new RadioGroup(name, note, value, data.options, onChange, { disabled: data.disabled }); 2335 | else if (type == 'slider') setting = new Settings.Slider(name, note, data.min, data.max, value, onChange, data); 2336 | else if (type == 'switch') setting = new Switch(name, note, value, onChange, { disabled: data.disabled }); 2337 | else if (type == 'textbox') setting = new Settings.Textbox(name, note, value, onChange, { placeholder: data.placeholder || '' }); 2338 | if (id) setting.id = id; 2339 | return setting; 2340 | }; 2341 | 2342 | /* 2343 | * Function versionComparator from 0PluginLibrary as defaultComparator, required copyright notice: 2344 | * 2345 | * Copyright 2018 Zachary Rauen 2346 | * 2347 | * 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: 2348 | * 2349 | * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 2350 | */ 2351 | XenoLib.versionComparator = (currentVersion, remoteVersion) => { 2352 | currentVersion = currentVersion.split(".").map((e) => {return parseInt(e);}); 2353 | remoteVersion = remoteVersion.split(".").map((e) => {return parseInt(e);}); 2354 | 2355 | if (remoteVersion[0] > currentVersion[0]) return true; 2356 | else if (remoteVersion[0] == currentVersion[0] && remoteVersion[1] > currentVersion[1]) return true; 2357 | else if (remoteVersion[0] == currentVersion[0] && remoteVersion[1] == currentVersion[1] && remoteVersion[2] > currentVersion[2]) return true; 2358 | return false; 2359 | } 2360 | 2361 | /* 2362 | * Function extractVersion from 0PluginLibrary as defaultVersioner, required copyright notice: 2363 | * 2364 | * Copyright 2018 Zachary Rauen 2365 | * 2366 | * 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: 2367 | * 2368 | * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 2369 | */ 2370 | XenoLib.extractVersion = (content) => { 2371 | const remoteVersion = content.match(/['"][0-9]+\.[0-9]+\.[0-9]+['"]/i); 2372 | if (!remoteVersion) return "0.0.0"; 2373 | return remoteVersion.toString().replace(/['"]/g, ""); 2374 | } 2375 | 2376 | return class CXenoLib extends Plugin { 2377 | constructor() { 2378 | super(); 2379 | this.settings = LibrarySettings; 2380 | const _zerecantcode_path = require('path'); 2381 | const theActualFileNameZere = _zerecantcode_path.join(__dirname, _zerecantcode_path.basename(__filename)); 2382 | XenoLib.changeName(theActualFileNameZere, '1XenoLib'); /* prevent user from changing libs filename */ 2383 | try { 2384 | NewModalStack.closeModal(`${this.name}_DEP_MODAL`); 2385 | } catch (e) { } 2386 | } 2387 | load() { 2388 | if (super.load) super.load(); 2389 | try { 2390 | if (!BdApi.Plugins) return; /* well shit what now */ 2391 | if (!BdApi.isSettingEnabled) return; 2392 | const list = BdApi.Plugins.getAll().filter(k => k._XL_PLUGIN || (k.instance && k.instance._XL_PLUGIN)).map(k => k.instance || k); 2393 | for (let p = 0; p < list.length; p++) try { 2394 | BdApi.Plugins.reload(list[p].getName()); 2395 | } catch (e) { 2396 | try { 2397 | Logger.stacktrace(`Failed to reload plugin ${list[p].getName()}`, e); 2398 | } catch (e) { 2399 | Logger.error('Failed telling you about failing to reload a plugin', list[p], e); 2400 | } 2401 | } 2402 | 2403 | const pluginsDir = (BdApi.Plugins && BdApi.Plugins.folder) || (window.ContentManager && window.ContentManager.pluginsFolder); 2404 | const PLUGINS_LIST = ['BetterImageViewer', 'BetterTypingUsers', 'BetterUnavailableGuilds', 'CrashRecovery', 'InAppNotifications', 'MessageLoggerV2', 'MultiUploads', 'SaveToRedux', 'UnreadBadgesRedux']; 2405 | const fs = require('fs'); 2406 | const path = require('path'); 2407 | 2408 | const pluginsToCheck = []; 2409 | 2410 | let alreadyFoundZLib = false; 2411 | 2412 | for (const file of fs.readdirSync(pluginsDir)) { 2413 | if (file.indexOf('.js') !== file.length - 3) continue; 2414 | 2415 | try { 2416 | const { name } = _extractMeta(fs.readFileSync(path.join(pluginsDir, file), 'utf8')); 2417 | if (PLUGINS_LIST.indexOf(name) === -1) { 2418 | switch (name) { 2419 | case 'XenoLib': { 2420 | if (file !== path.basename(__filename)) fs.unlinkSync(path.join(pluginsDir, file)); 2421 | break; 2422 | } 2423 | case 'ZeresPluginLibrary': { 2424 | if (alreadyFoundZLib) fs.unlinkSync(path.join(pluginsDir, file)); 2425 | else alreadyFoundZLib = true; 2426 | } 2427 | } 2428 | continue; 2429 | } 2430 | if (~pluginsToCheck.findIndex(e => e.name === name)) { 2431 | fs.unlinkSync(path.join(pluginsDir, file)); 2432 | continue; 2433 | } 2434 | pluginsToCheck.push({ name, file }); 2435 | } catch (e) { } 2436 | } 2437 | setTimeout(() => { 2438 | try { 2439 | const https = require('https'); 2440 | for (const { name, file } of pluginsToCheck) { 2441 | // eslint-disable-next-line no-undef 2442 | const isPluginEnabled = BdApi.Plugins.isEnabled(name); 2443 | let plugin = BdApi.Plugins.get(name); 2444 | if (plugin && plugin.instance) plugin = plugin.instance; 2445 | // eslint-disable-next-line no-loop-func 2446 | const req = https.request(`https://raw.githubusercontent.com/Davilarek/MessageLoggerV2-fixed/master/Plugins/${name}/${name}.plugin.js`, { headers: { origin: 'discord.com' } }, res => { 2447 | let body = ''; 2448 | // eslint-disable-next-line no-void 2449 | res.on('data', chunk => ((body += chunk), void 0)); 2450 | res.on('end', () => { 2451 | try { 2452 | if (res.statusCode !== 200) return /* XenoLib.Notifications.error(`Failed to check for updates for ${name}`, { timeout: 0 }) */; 2453 | if (plugin && (name === 'MessageLoggerV2' || Utilities.getNestedProp(plugin, '_config.info.version')) && !XenoLib.versionComparator(name === 'MessageLoggerV2' ? plugin.getVersion() : plugin._config.info.version, XenoLib.extractVersion(body))) return; 2454 | const newFile = `${name}.plugin.js`; 2455 | fs.unlinkSync(path.join(pluginsDir, file)); 2456 | // avoid BDs watcher being shit as per usual 2457 | setTimeout(() => { 2458 | try { 2459 | fs.writeFileSync(path.join(pluginsDir, newFile), body); 2460 | if (window.pluginModule && window.pluginModule.loadPlugin) { 2461 | BdApi.Plugins.reload(name); 2462 | if (newFile !== file) window.pluginModule.loadPlugin(name); 2463 | // eslint-disable-next-line curly 2464 | } else if (BdApi.version ? !BdApi.isSettingEnabled('settings', 'addons', 'autoReload') : !BdApi.isSettingEnabled('fork-ps-5')) { 2465 | // eslint-disable-next-line no-negated-condition 2466 | if (newFile !== file) { 2467 | // eslint-disable-next-line no-undef 2468 | BdApi.showConfirmationModal('Hmm', 'You must reload in order to finish plugin installation', { onConfirm: () => location.reload() }); 2469 | isPluginEnabled = false; 2470 | } else BdApi.Plugins.reload(name); 2471 | } 2472 | if (isPluginEnabled) setTimeout(() => BdApi.Plugins.enable(name), 3000); 2473 | } catch (e) { } 2474 | }, 1000); 2475 | } catch (e) { } 2476 | }); 2477 | }); 2478 | req.on('error', _ => XenoLib.Notifications.error(`Failed to check for updates for ${name}`, { timeout: 0 })); 2479 | } 2480 | } catch (e) { } 2481 | }, 3000); 2482 | } catch (err) { 2483 | Logger.log('Failed to execute load', err); 2484 | } 2485 | const end = performance.now(); 2486 | Logger.log(`Loaded in ${Math.round(end - start)}ms`); 2487 | } 2488 | buildSetting(data) { 2489 | if (data.type === 'position') { 2490 | const setting = new NotificationPositionField(data.name, data.note, data.onChange, data.value); 2491 | if (data.id) setting.id = data.id; 2492 | return setting; 2493 | } else if (data.type === 'color') { 2494 | const setting = new XenoLib.Settings.ColorPicker(data.name, data.note, data.value, data.onChange, data.options); 2495 | if (data.id) setting.id = data.id; 2496 | return setting; 2497 | } else if (data.type === 'timeStatus') { 2498 | const setting = new Timer(data.name, data.note, data.value, data.onChange, data.after, data.active, data.inactive, data.time); 2499 | if (data.id) setting.id = data.id; 2500 | return setting; 2501 | } 2502 | return XenoLib.buildSetting(data); 2503 | } 2504 | getSettingsPanel() { 2505 | return this.buildSettingsPanel() 2506 | .append(new XenoLib.Settings.PluginFooter(() => this.showChangelog())) 2507 | .getElement(); 2508 | } 2509 | saveSettings(category, setting, value) { 2510 | this.settings[category][setting] = value; 2511 | LibrarySettings[category][setting] = value; 2512 | PluginUtilities.saveSettings(this.name, LibrarySettings); 2513 | if (category === 'notifications') { 2514 | if (setting === 'position') { 2515 | const DOMElement = document.querySelector('.xenoLib-notifications'); 2516 | if (DOMElement) { 2517 | DOMElement.className = XenoLib.joinClassNames('xenoLib-notifications', `xenoLib-centering-${LibrarySettings.notifications.position}`); 2518 | notificationEvents.dispatch({ type: 'XL_NOTIFS_ANIMATED' }); 2519 | } 2520 | } else if (setting === 'backdrop' || setting === 'backdropColor') { 2521 | notificationEvents.dispatch({ type: 'XL_NOTIFS_SETTINGS_UPDATE', key: UPDATEKEY }); 2522 | UPDATEKEY = {}; 2523 | } 2524 | 2525 | } else if (category === 'addons') if (setting === 'extra') { 2526 | if (value && !patchAddonCardAnyway.patched) patchAddonCardAnyway(true); 2527 | XenoLib.Notifications.warning('Reopen plugins section for immediate effect'); 2528 | } else if (category === 'userCounter') if (setting === 'enabled') { 2529 | if (value) { 2530 | LibrarySettings.userCounter.enableTime = Date.now(); 2531 | LibrarySettings.userCounter.lastSubmission = Date.now(); 2532 | } else { 2533 | LibrarySettings.userCounter.enableTime = 0; 2534 | LibrarySettings.userCounter.lastSubmission = 0; 2535 | } 2536 | PluginUtilities.saveSettings(this.name, LibrarySettings); 2537 | } 2538 | 2539 | 2540 | } 2541 | showChangelog(footer) { 2542 | return; 2543 | XenoLib.showChangelog(`${this.name} has been updated!`, this.version, this._config.changelog, void 0, true); 2544 | } 2545 | get name() { 2546 | return config.info.name; 2547 | } 2548 | get short() { 2549 | let string = ''; 2550 | 2551 | for (let i = 0, len = config.info.name.length; i < len; i++) { 2552 | const char = config.info.name[i]; 2553 | if (char === char.toUpperCase()) string += char; 2554 | } 2555 | 2556 | return string; 2557 | } 2558 | get author() { 2559 | return config.info.authors.map(author => author.name).join(', '); 2560 | } 2561 | get version() { 2562 | return config.info.version; 2563 | } 2564 | get description() { 2565 | return config.info.description; 2566 | } 2567 | }; 2568 | }; 2569 | 2570 | /* Finalize */ 2571 | 2572 | let ZeresPluginLibraryOutdated = false; 2573 | try { 2574 | 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]); 2575 | let b = BdApi.Plugins.get('ZeresPluginLibrary'); 2576 | ((b, c) => b && b.version && a(b.version, c))(b, '2.0.8') && (ZeresPluginLibraryOutdated = !0); 2577 | } catch (e) { 2578 | console.error('Error checking if ZeresPluginLibrary is out of date', e); 2579 | } 2580 | 2581 | return !global.ZeresPluginLibrary || ZeresPluginLibraryOutdated || window.__XL_requireRenamePls 2582 | ? class { 2583 | constructor() { 2584 | this._config = config; 2585 | } 2586 | getName() { 2587 | return this.name.replace(/\s+/g, ''); 2588 | } 2589 | getAuthor() { 2590 | return this.author; 2591 | } 2592 | getVersion() { 2593 | return this.version; 2594 | } 2595 | getDescription() { 2596 | return `${this.description} You are missing ZeresPluginLibrary for this plugin, please enable the plugin to download it.`; 2597 | } 2598 | start() { } 2599 | load() { 2600 | try { 2601 | // asking people to do simple tasks is stupid, relying on stupid modals that are *supposed* to help them is unreliable 2602 | // forcing the download on enable is good enough 2603 | const fs = require('fs'); 2604 | const path = require('path'); 2605 | const pluginsDir = (BdApi.Plugins && BdApi.Plugins.folder) || (window.ContentManager && window.ContentManager.pluginsFolder); 2606 | const zeresLibDir = path.join(pluginsDir, '0PluginLibrary.plugin.js'); 2607 | 2608 | if (window.__XL_requireRenamePls) { 2609 | try { 2610 | delete window.__XL_requireRenamePls; 2611 | const oldSelfPath = path.join(pluginsDir, path.basename(__filename)); 2612 | const selfContent = fs.readFileSync(oldSelfPath); 2613 | // avoid windows blocking the file 2614 | fs.unlinkSync(oldSelfPath); 2615 | // avoid BDs watcher being shit as per usual 2616 | setTimeout(() => { 2617 | try { 2618 | fs.writeFileSync(path.join(pluginsDir, '1XenoLib.plugin.js'), selfContent); 2619 | window.__XL_waitingForWatcherTimeout = setTimeout(() => { 2620 | // what the fuck? 2621 | BdApi.Plugins.reload(this.getName()); 2622 | }, 3000); 2623 | } catch (e) { } 2624 | }, 1000); 2625 | } catch (e) { } 2626 | return; 2627 | } 2628 | 2629 | if (window.__XL_assumingZLibLoaded) return; 2630 | 2631 | for (const file of fs.readdirSync(pluginsDir)) { 2632 | if (file.indexOf('.js') !== file.length - 3) continue; 2633 | try { 2634 | switch (_extractMeta(fs.readFileSync(path.join(pluginsDir, file), 'utf8')).name) { 2635 | case 'XenoLib': { 2636 | if (file !== '1XenoLib.plugin.js') if (file === path.basename(__filename)) window.__XL_requireRenamePls = true; 2637 | else fs.unlinkSync(path.join(pluginsDir, file)); 2638 | 2639 | continue; 2640 | } 2641 | case 'ZeresPluginLibrary': { 2642 | fs.unlinkSync(path.join(pluginsDir, file)); 2643 | continue; 2644 | } 2645 | default: continue; 2646 | } 2647 | } catch (e) { } 2648 | } 2649 | 2650 | const https = require('https'); 2651 | 2652 | const onFail = () => BdApi.showConfirmationModal('Well shit', 'Failed to download Zeres Plugin Library, join this server for further assistance:https://discord.gg/NYvWdN5'); 2653 | 2654 | const req = https.get('https://raw.githubusercontent.com/rauenzi/BDPluginLibrary/master/release/0PluginLibrary.plugin.js', { headers: { origin: 'discord.com' } }, res => { 2655 | let body = ''; 2656 | // eslint-disable-next-line no-void 2657 | res.on('data', chunk => ((body += new TextDecoder("utf-8").decode(chunk)), void 0)); 2658 | res.on('end', (rez) => { 2659 | try { 2660 | if (rez.statusCode !== 200) return onFail(); 2661 | fs.writeFileSync(zeresLibDir, body); 2662 | // eslint-disable-next-line no-undef 2663 | window.__XL_waitingForWatcherTimeout = setTimeout(() => { 2664 | try { 2665 | if (!window.pluginModule || !window.pluginModule.loadPlugin) { 2666 | window.__XL_assumingZLibLoaded = true; 2667 | const didRename = window.__XL_requireRenamePls; 2668 | window.__XL_waitingForWatcherTimeout = setTimeout(() => { 2669 | try { 2670 | window.__XL_waitingForWatcherTimeout = setTimeout(() => { 2671 | try { 2672 | location.reload(); 2673 | } catch (e) { } 2674 | }, 3000); 2675 | BdApi.Plugins.reload(this.getName()); 2676 | } catch (e) { } 2677 | }, window.__XL_requireRenamePls ? 3000 : 0); 2678 | if (window.__XL_requireRenamePls) { 2679 | delete window.__XL_requireRenamePls; 2680 | const oldSelfPath = path.join(pluginsDir, path.basename(__filename)); 2681 | const selfContent = fs.readFileSync(oldSelfPath); 2682 | // avoid windows blocking the file 2683 | fs.unlinkSync(oldSelfPath); 2684 | fs.writeFileSync(path.join(pluginsDir, '1XenoLib.plugin.js'), selfContent); 2685 | } 2686 | return; 2687 | } 2688 | window.__XL_assumingZLibLoaded = true; 2689 | window.pluginModule.loadPlugin('0PluginLibrary'); 2690 | window.__XL_waitingForWatcherTimeout = setTimeout(() => { 2691 | try { 2692 | const didRename = window.__XL_requireRenamePls; 2693 | window.__XL_waitingForWatcherTimeout = setTimeout(() => { 2694 | try { 2695 | window.__XL_waitingForWatcherTimeout = setTimeout(onFail, 3000); 2696 | BdApi.Plugins.reload(this.getName()); 2697 | if (!BdApi.Plugins.get('XenoLib')) window.pluginModule.loadPlugin('1XenoLib'); 2698 | } catch (e) { } 2699 | }, window.__XL_requireRenamePls ? 3000 : 0); 2700 | if (window.__XL_requireRenamePls) { 2701 | delete window.__XL_requireRenamePls; 2702 | const oldSelfPath = path.join(pluginsDir, path.basename(__filename)); 2703 | const selfContent = fs.readFileSync(oldSelfPath); 2704 | // avoid windows blocking the file 2705 | fs.unlinkSync(oldSelfPath); 2706 | fs.writeFileSync(path.join(pluginsDir, '1XenoLib.plugin.js'), selfContent); 2707 | } 2708 | } catch (e) { } 2709 | }, 3000); 2710 | } catch (e) { } 2711 | }, 3000); 2712 | } catch (e) { } 2713 | }); 2714 | }); 2715 | req.on('error', _ => { 2716 | onFail(); 2717 | }); 2718 | } catch (e) { } 2719 | } 2720 | stop() { } 2721 | get name() { 2722 | return config.info.name; 2723 | } 2724 | get short() { 2725 | let string = ''; 2726 | for (let i = 0, len = config.info.name.length; i < len; i++) { 2727 | const char = config.info.name[i]; 2728 | if (char === char.toUpperCase()) string += char; 2729 | } 2730 | return string; 2731 | } 2732 | get author() { 2733 | return config.info.authors.map(author => author.name).join(', '); 2734 | } 2735 | get version() { 2736 | return config.info.version; 2737 | } 2738 | get description() { 2739 | return config.info.description; 2740 | } 2741 | } 2742 | : buildPlugin(global.ZeresPluginLibrary.buildPlugin(config)); 2743 | })(); 2744 | 2745 | /*@end@*/ 2746 | -------------------------------------------------------------------------------- /Plugins/MessageLoggerV2/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [MessageLoggerV2](https://1lighty.github.io/BetterDiscordStuff/?plugin=MessageLoggerV2 "MessageLoggerV2") Changelog 2 | ### 1.7.68 3 | - Fixed on canary. 4 | 5 | ### 1.7.67 6 | - Implemented fixes that allow patches to work properly on canary using Powercord. AKA plugin works now. 7 | 8 | ### 1.7.65 && 1.7.66 9 | - Implemented fixes that allow patches to work properly on canary using Powercord. 10 | 11 | ### 1.7.64 12 | - Fixed settings not working 13 | - Fixed some periodic lag spikes 14 | 15 | ### 1.7.63 16 | - Fixed forgetting to remove a console.log 17 | - XSS fix by [clv](https://github.com/clv-2) on GitHub 18 | 19 | ### 1.7.62 20 | - Fixed not logging replies. 21 | 22 | ### 1.7.61 23 | - Fixed plugin not working from the great canary update plugin massacre. 24 | 25 | ### 1.7.60 26 | - Fixed some issues related to extremely poor decision making in the powercord "injector" aka patcher, `Failed to patch message components, edit history and deleted tint will not show!` *should* no longer show up and instead work as intended. 27 | 28 | ### 1.7.59 29 | - Fixed deleted messages sometimes zapping your pfp (and sometimes others) 30 | - Fixed logger sometimes locking up Discord when opening a channel 31 | - Fixed some deleted messages not always showing in chat (they do now, no matter what, even ones that didn't before) 32 | 33 | ### 1.7.58 34 | - Fixed not working on canary 35 | 36 | ### 1.7.57 37 | - Fixed not working 38 | 39 | ### 1.7.56 40 | - Fixed not working on canary 41 | 42 | ### 1.7.55 43 | - Bruh. Gitlab sux, moved plugin to github. 44 | 45 | ### ??? - ??? 46 | - Unknown changes 47 | 48 | ### 1.5.2 49 | - Added option to disable the Open logs button 50 | - Fixed channels not loading if bots aren't ignored and a bot deletes its own message that contains an embed. 51 | 52 | ### 1.5.1 53 | - Added Open logs button in toolbar next to Member List. Clicking it opens the menu, rightclicking it opens the filtered menu 54 | - Added global NSFW channel ignore toggle, whitelist and log selected channel override this. 55 | - Added Changelog, Stats to show some simple stuff (will be expanded later on) and Donate buttons to settings 56 | - Fixed uncaught error on dead image links 57 | - Fixed image caching failing on dead network or some other unknown issue 58 | - Fixed issue in menu where you could only click on a persons profile on one message only 59 | - Fixed hiding/unhiding a delete message not immediately working in chat 60 | - Clicking a toast also dismisses it immediately, middle clicking a toast only dismisses it without doing anything else 61 | - Sorted settings page into something usable 62 | - Expand Stats modal to show per server and per channel stats, also show disk space usage. 63 | - Help button in settings 64 | - Extensive help for all functions of the logger, both in the menu and settings. Some things are rather hidden. 65 | 66 | ### 1.5.0 67 | - Added image caching. If a link of an image expires, you'll still have a local copy of the image. In its place will be a small thumbnail of the image and clicking on it will open it in your default image viewer. The reason it's like this is because it's a limitation of Discord and the only simple workaround will slow down a client to a halt. 68 | - Added context menu options for entire message groups, accessible by rightclicking the timestamp 69 | - Fixed image related context menu options not working too well when multiple images are sent in one message 70 | - Delete + click now works on images 71 | - Copy to clipboard/Save to folder should work more reliably due to image caching 72 | - Images that failed to load but still exist no longer get flagged as bad/expired images internally. They might still show as bad but reopening the menu or changing tabs should make the image load. 73 | 74 | ### 1.4.?? 75 | - Fixed bad images not being saved properly 76 | - Fixed menu loading error only showing for 1 second 77 | 78 | ### 1.4.23 79 | - Fixed conditional bug related to a chat being opened but logger not detecting it and failing 80 | - Fixed keybinds not working after unsuspend/wake up from sleep/probably hibernate too 81 | 82 | ### 1.4.22 83 | - Fixed download missing library dialog not working. 84 | 85 | ### 1.4.21 86 | - Added self install script for clueless users 87 | - Fixed keybinds not working after unsuspend/after a while 88 | - Fixed improper sorting in menu for edited messages 89 | - Fixed minor caching issue regarding edited and deleted messages 90 | - Fixed opening an image that does not exist resulting in infinite loading. 91 | - Dropped Metalloriffs method of saving in favor of something less extreme. The chance of a corrupted data file, or general lag should decrease. 92 | 93 | ### 1.4.20 94 | - Added hold Delete and click on message in menu to delete it 95 | - Added option to delete only one of the edits 96 | - Added check for outdated Zeres Plugin Library to avoid any issues in the future 97 | - Added option to show ghost pings either because the message was deleted, or because the user edited the message no longer tagging you 98 | - Fixed hiding edited messages bugginess 99 | - Fixed bugged messages breaking everything 100 | - Fixed improper sorting in menu for edited messages 101 | - Fixed improper times shown in menu for edited messages 102 | - Fixed messages hiding if you delete one that was shown after pressing the LOAD MORE button 103 | - Fixed images not showing in order relative to other messages 104 | - Fixed deleted messages context menu having redundant buttons 105 | - Fixed jumping to a message erasing its edits and deleted color 106 | - Fixed Clyde not being ignored. Thought he could have friends? Lol. 107 | - Clicking a message in the menu sent tab will jump to the message 108 | - Rightclicking a toast jumps to the message 109 | 110 | 111 | ### 1.4.19 112 | - Fixed keybind resetting if filtered log keybind isn't set 113 | - Fixed messages disappearing if show deleted message count is disabled 114 | 115 | ### 1.4.18 116 | - Fixed some major bugs 117 | 118 | ### 1.4.17 119 | - Fixed images not showing at all in menu 120 | 121 | ### 1.4.16 122 | - Added option to not restore messages after reload 123 | - Fixed not working with normalized classes disabled 124 | 125 | ### 1.4.15 126 | - Bots now have a tag in the menu 127 | - Fixed blacklist/whitelist being a big fat potato 128 | - Some stuff has been optimized 129 | - Edited messages and deleted messages now show instantly when switching channels 130 | - Added option to ignore blocked users 131 | - Added seperate options for displaying deleted, edited and purged messages in chat 132 | - Seperated guild and DM toasts for sent, edited and deleted 133 | - Fixed "Show date in timestamps" affecting menu 134 | - Edited messages and deleted messages now show instantly when switching channels 135 | 136 | ### 1.4.14 137 | - Fixed clear button not working 138 | 139 | ### 1.4.12 && 1.4.13 140 | - Added user context menu options 141 | - Added individual blacklist and whitelist 142 | - Added "Only log whitelist" option 143 | - Fixed settings page showing wrong menu sort direction 144 | - Fixed being rate limited when opening the menu 145 | - Fixed hidden messages showing again after reload 146 | - Fixed hiding a deleted message making others not show as deleted anymore 147 | - Fixed deleting a bot message with embed breaking everything 148 | - Fixed single key keybind being borked after reload 149 | - Data file should be cleaned more thoroughly resulting in a way smaller data file size and faster general operation 150 | - Added forced save to autobackup so it's not infinitely stalled by edits or deletes, so chances of a 100% restore are higher 151 | - ACTUALLY fix deleted bot messages with embeds breaking everything 152 | 153 | ### 1.4.11 154 | - Fixed accidentally logging deletion of system messages, aka user join messages etc 155 | - Fixed and improved some specific filter searches in the menu 156 | - Fixed spamming discord servers with user profile requests causing issues 157 | - Fix loading error in ghost pings tab 158 | 159 | ### 1.4.10 160 | - Fixed accidentally logging deletion of system messages, aka user join messages etc 161 | - Fixed and improved some specific filter searches in the menu 162 | - Fixed spamming discord servers with user profile requests causing issues 163 | - Improved menu performance when loading 164 | 165 | ### 1.4.8 && 1.4.9 166 | - Fixed random crash/freeze related to bad data causing an infinite loop 167 | - Fixed changelog omegalul 168 | 169 | ### 1.4.7 170 | - Added context menu options for images in the menu 171 | - Fixed filter input randomly losing focus 172 | - Fixed some profiles not opening in the menu if no channel is selected 173 | - Fixed editing bug 174 | - Fixed breaking profile pictures in discord in general 175 | - Fixed lag while typing in the filter input 176 | - Added detection of a failed menu load 177 | - Toasts last longer so you have time to read and click them 178 | - Clicking a toast while the menu is open, opens the relevant tab 179 | - Toasts and menu should more accurately show where the message is from 180 | - Pressing menu tabs should generally feel more responsive 181 | 182 | ### 1.4.6 183 | - Fix missing entry bug in menu 184 | 185 | ### 1.4.5 186 | - Toasts are now clickable 187 | - Added menu render limit to avoid lag loading thousands of messages 188 | - Traded massive client crash bug for a slight editing bug until a fix is made 189 | - Fixed menu profile pictures not showing sometimes 190 | - Fixed deleted images causing jitteriness 191 | - Fixed menu sent tab not displaying anything 👀 192 | - Fixed incorrect sort direction in sent tab 193 | - Fixed possible crash in menu 194 | - Fixed unneeded things like joins, leaves, group name changes etc being logged as messages 195 | 196 | ### 1.4.4 197 | - Context menus now exist, you can add/remove guilds/channels from whitelist/blacklist, open menu, hide and unhide deleted messages or edit history of a message. 198 | - Adding a channel or guild on whitelist while having log type set to all will make it ignore guild/channel ignores 199 | - Menu opens and now closes with bound keys. 200 | - Fixed menu opening in settings 201 | 202 | ### 1.4.2 && 1.4.3 203 | - Patch posibility of an empty entry in data causing menu to not show anything 204 | 205 | ### 1.4.1 206 | - Fixed whitelist/blacklist not working, again 207 | 208 | ### 1.4.0 209 | - Modal based menu was added. Accessible via default keys ctrl m or ctrl alt m 210 | - Plugin now manages data properly instead of letting it go loose and possibly make a massive data file 211 | - More corruption protection 212 | - Edit timestamps should be precise instead of only having hour and minute, an option to toggle between having dates and not having dates in edited tooltip will be added soon 213 | - More options in the menu 214 | - More customization 215 | - More options around the place like blacklisting/whitelisting servers/channels or users 216 | 217 | ### 1.3.4 218 | - Fixed other plugins loading ZLib with no EmulatedTooltip breaking stuff 219 | - Fixed editing a message back to original not doing working 220 | 221 | ### 1.3.3 222 | - Disabled relative IDs warning until a fix is implemented 223 | 224 | ### 1.3.2 225 | - Fixed toast toggles not saving 226 | 227 | ### 1.3.1 228 | - Badly written BDFDB library should no longer crash discord 229 | 230 | ### 1.3.0 231 | - Data storage and caching has been reworked, discord should no longer frezee when there is a large quantity of saved messages 232 | - Error messages in console are now detailed 233 | 234 | ### 1.2.1 235 | - Fixed ZLibrary download requirement being extremely user unfriendly 236 | 237 | ### 1.2.0 238 | - Changed library from NeatoBurritoLibrary to Zeres Plugin Library 239 | - Updater now works 240 | - Changelog now looks fancy af 241 | 242 | ### 1.1.0 243 | - Add auto data backup option, logged messages are kept even if data file is corrupted. 244 | - Add option to not save data at all, all logged messages will be erased after reload/restart. 245 | - Fix plugin breaking if you load it while not being in a channel. 246 | - Make aggressive message caching work better. You should see 'Failed to ger relative IDs' less often. 247 | - Auto self fix for corrupted data or settings file, plugin should work even if both files become empty. 248 | 249 | 250 | ### 1.0.0 251 | - Release 252 | - Deleted messages load back in after reload. 253 | - Built-in protection against abusive themes. 254 | - Aggresive message caching, on load fetches messages from a bunch of channels that have deleted or edited messages in them, same applies if a message is edited or deleted in an uncached channel. 255 | - Toggle dates in timestamps live. 256 | - Toggle deleted messages or edit history in chat live. 257 | - Toggle showing purged messages live. 258 | - Fix cancelling editing a message not showing edit history. 259 | -------------------------------------------------------------------------------- /Plugins/MessageLoggerV2/README.md: -------------------------------------------------------------------------------- 1 | # MessageLoggerV2 2 | Saves all deleted and purged messages, as well as all edit history and ghost pings. With highly configurable ignore options, and even restoring deleted messages after restarting Discord. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MOVED TO https://gitea.slowb.ro/Davilarek/MessageLoggerV2-fixed 2 | # DEVELOPMENT WILL BE CONTINUED THERE 3 | 4 | ## Original desc: 5 | 6 |
7 |
8 |
9 |
10 |
11 |
12 | 13 | # MessageLogger V2 14 | #### Info 15 | The MLV2 was created by Lighty. I just fixed it. 16 | 17 | Make sure to backup your data file before using my version. 18 | 19 | If you require support for any of his plugins, join his Discord server (can be found in original readme). 20 | 21 | All of his plugins are available for easy downloading on his website. 22 | 23 | ## What's fixed: 24 | - Edits 25 | - "Open logs" 26 | - Message removed/edited notification??? 27 | - Saving images to image cache directory 28 | - Context menus 29 | - Color of "Remove From Log" 30 | - Image cache server (alternative implementation) 31 | 32 | ## What's to be fixed: 33 | - Memory leaks 34 | - [#5](https://github.com/Davilarek/MessageLoggerV2-fixed/issues/5) 35 | - [#16](https://github.com/Davilarek/MessageLoggerV2-fixed/issues/16) 36 | 37 | ## What's custom: 38 | - Support for embed images (thanks for idea @Ansemik_CZE) 39 | - Saving images to directory structure main_folder/server_name/channel_name/file_name ("Use new cached images system" in settings) (thanks for idea @Ansemik_CZE) 40 | - Some other stuff I don't remember 41 | - Notification whitelist (thanks for idea @Voleno1, https://github.com/Davilarek/MessageLoggerV2-fixed/issues/3) 42 | -------------------------------------------------------------------------------- /download/README.md: -------------------------------------------------------------------------------- 1 | ### What? 2 | This directory allows easy download of latest MLV2. 3 | 4 | To use, just enter https://davilarek.github.io/MessageLoggerV2-fixed/download/ 5 | 6 | ### How? 7 | It makes browser simulate click on download link. Simple. 8 | 9 | ### Why? 10 | Some people may find it difficult to navigate Github. 11 | -------------------------------------------------------------------------------- /download/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | --------------------------------------------------------------------------------