diff --git a/src/copy_code/index.jsx b/src/copy_code/index.jsx new file mode 100644 index 00000000..837a6349 --- /dev/null +++ b/src/copy_code/index.jsx @@ -0,0 +1,195 @@ +// noinspection JSUnresolvedVariable +const { createElement } = FrankerFaceZ.utilities.dom; + +class CopyCode extends Addon { + constructor(...args) { + super(...args); + this.inject('i18n'); + + /** @type {RegExp} */ + this.codeRegex = /Click here to redeem: (.*?)\./; + /** @type {HTMLDivElement | undefined} */ + this.inboxPopup = undefined; + /** @type {string} */ + /** @type {FineWrapper} */ + // this.RootFine = this.fine.define( + // 'notification', () => true); + /** @type {Record} */ + this.cache = {}; + /** @type {HTMLDivElement[]} */ + this.buttons = []; + } + + /** + * Triggered on enable of the add-on + */ + onEnable() { + this.notificationObserver = new MutationObserver(this.notificationObserverCallback.bind(this)); + this.notificationObserver.observe(document.body, { childList: true, subtree: true, attributes: true }); + } + + elementIs(element, ...queries) { + for (const query of queries) { + const queried = document.querySelector(query); + if (queried == element) { + return true; + } + } + return false; + } + + /** + * Updates notifications menus. + * @param {MutationRecord[]} mutations The list of mutations + * @param {MutationObserver} _observer The mutation observer + */ + /* eslint-disable-next-line no-unused-vars */ + notificationObserverCallback(mutations, _observer) { + for (const mutation of mutations) { + if ((mutation.type === 'childList' && mutation.addedNodes.length > 0 && this.elementIs(mutation.target, 'div[data-test-selector="center-window__content"]')) || this.elementIs(mutation.target, '.ReactModal__Body--open')) { + for (const element of mutation.addedNodes) { + for (const innerElement of element.querySelectorAll('div.simplebar-scroll-content div[data-test-selector="center-window__content"] > div > div:not(:first-child) > div')) { + this.updateButton(innerElement); + } + } + } + } + } + + /** + * Triggered on disabling of the add-on + */ + onDisable() { + this.notificationObserver.disconnect(); + this.destroyButtons(); + } + + /** + * Destroys all button instances. + */ + destroyButtons() { + // statically store the count of how many buttons. + const count = this.buttons.length; + // Loop from end to the beginning of the array as pop() removes the last one. + for (let i = count - 1; i >= 0; i--) { + this.buttons[i]?.remove(); + this.buttons.pop(); + } + } + + /** + * Creates all the button and/or updates them. + * @param {HTMLDivElement} element The element that was mutated or mounted. + * @returns {void} Nothing... + */ + updateButton(element) { + if (!element) return; + + /** @type {HTMLParagraphElement | undefined} */ + const notificationBody = element.querySelector('div[data-test-selector="persistent-notification__body"] p'); + + if (!notificationBody) return; + + /** @type {HTMLDivElement | undefined} */ + const button = element.querySelector('.ffz--copy-code-button'); + + if (button) return; + + if (!this.codeRegex.test(notificationBody.innerText)) return; + + if (this.cache[notificationBody.innerText] === undefined) { + /** @type {string[] | null} */ + const match = notificationBody.innerText.match(this.codeRegex); + + if (match === null || match.length != 2) return; + + /** @type {string} */ + this.cache[notificationBody.innerText] = match[1]; + } + + /** @type {string} */ + const code = this.cache[notificationBody.innerText]; + + /** @type {string} */ + const label = this.i18n.t('addon.copy_code.button.label', 'Copy redeem code'); + + /** @type {HTMLDivElement} */ + const cont = (
+ + +
); + + this.buttons.push(cont); + + /** @type {HTMLDivElement | undefined} */ + const thing = element.querySelector('a > div > div'); + if (thing) { + thing.appendChild(cont); + } + } + + /** + * Finds the parent element based on the selector. + * @param {HTMLElement} element the element to find the parent of. + * @param {string} selector the selector to check for if it is the element. + * @returns {HTMLElement | undefined} the element if found; otherwise undefined. + */ + findParent(element, selector) { + /** @type {HTMLElement | undefined} */ + let found; + /** @type {HTMLElement | undefined} */ + let lastParent = element.parentElement; + + while (!found && lastParent) { + if (lastParent.nodeName === 'HTML' || lastParent.nodeName === 'BODY') { + lastParent = undefined; + } + if (lastParent.nodeName.toLowerCase() === selector.toLowerCase()) { + found = lastParent; + } else if (document.querySelector(selector) === lastParent) { + found = lastParent; + } else { + lastParent = lastParent.parentElement; + } + } + return found; + } + + /** + * Handles the copy button click. + * @param {Event} event The click event that fires this method. + */ + onButtonClick(event) { + // Prevent bubbling. + event.preventDefault(); + try { + /** @type {string | undefined} */ + const code = this.findParent(event.target, 'button').getAttribute('data-code'); + if (!code) + throw new Error('could not get attribute, "data-code", on event target'); + try { + navigator.clipboard.writeText(code); + } catch (err) { + this.log.error('Clipboard is not accessible', event, err); + } + } catch (err) { + this.log.error('Failed to copy redeem code.', event, err); + } + } +} + +CopyCode.register(); \ No newline at end of file diff --git a/src/copy_code/manifest.json b/src/copy_code/manifest.json new file mode 100644 index 00000000..ee52a0f3 --- /dev/null +++ b/src/copy_code/manifest.json @@ -0,0 +1,12 @@ +{ + "enabled": true, + "requires": [], + "version": "1.0.0", + "short_name": "CopyCode", + "name": "Copy Code", + "author": "Neko Boi Nick", + "website": "https://github.com/thakyZ/FrankerFaceZ_Add-Ons", + "description": "Adds a copy code button to notifications with a redeem code.", + "created": "2024-12-04T22:24:51.713Z", + "updated": "2024-12-09T19:58:58.065Z" +} \ No newline at end of file