🔒 Robux Privacy

Adds a sleek "Hide/Unhide Robux" toggle button into the Roblox navigation header menu, allowing you to instantly mask your currency balance for privacy or content creation. Saves preferences across your session.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         🔒 Robux Privacy
// @namespace    https://github.com/yourusername/roblox-robux-privacy
// @version      0.2
// @description  Adds a sleek "Hide/Unhide Robux" toggle button into the Roblox navigation header menu, allowing you to instantly mask your currency balance for privacy or content creation. Saves preferences across your session.
// @author       not-oops
// @match        https://*.roblox.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=roblox.com
// @grant        none
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function() {
'use strict';
  /**
   * Shared application state managing persistence.
   */
  class PrivacyState {
      static STORAGE_KEY = 'robux_privacy';

      /**
       * Retrieves the current privacy state.
       * @returns {boolean} True if Robux should be hidden.
       */
      static isHidden() {
          const savedState = sessionStorage.getItem(this.STORAGE_KEY);
          return savedState !== null ? savedState === 'true' : true;
      }

      /**
       * Toggles and saves the privacy state.
       * @returns {boolean} The new privacy state.
       */
      static toggle() {
          const newState = !this.isHidden();
          sessionStorage.setItem(this.STORAGE_KEY, String(newState));
          return newState;
      }
  }

  /**
   * Component responsible for creating standardized elements.
   */
  class ElementFactory {
      /**
       * Creates a uniform span element styled to show "hidden".
       * @returns {HTMLSpanElement} The styled hidden label element.
       */
      static createHiddenSpan() {
          const hiddenSpan = document.createElement('span');
          hiddenSpan.textContent = 'hidden';
          hiddenSpan.style.opacity = '0.5';
          hiddenSpan.style.fontSize = 'small';
          hiddenSpan.setAttribute('data-hidden-marker', 'true');
          return hiddenSpan;
      }

      /**
       * Creates the toggle privacy button for the dropdown list.
       * @param {Function} onClickCallback - Action to trigger on click.
       * @returns {HTMLLIElement} The structured menu item container.
       */
      static createToggleButton(onClickCallback) {
          const listItem = document.createElement('li');
          listItem.className = 'rbx-menu-item-container';
          listItem.setAttribute('data-privacy-toggle', 'true');

          const anchor = document.createElement('a');
          anchor.className = 'rbx-menu-item';
          anchor.href = '#';
          anchor.textContent = PrivacyState.isHidden() ? 'Unhide Robux' : 'Hide Robux';
          anchor.style.cursor = 'pointer';

          anchor.addEventListener('click', (event) => {
              event.preventDefault();
              onClickCallback(anchor);
          });

          listItem.appendChild(anchor);
          return listItem;
      }
  }

  /**
   * Base interface/class for elements that need Robux data masked or restored.
   */
  class RobuxElementMasker {
      /**
       * Process target elements based on visibility state.
       */
      mask() {
          throw new Error('Method "mask()" must be implemented.');
      }

      /**
       * Replaces an element's target content with the standardized hidden element.
       * @param {HTMLElement} element - The DOM element to transform.
       */
      applyHiddenLabel(element) {
          if (!element.querySelector('[data-hidden-marker]')) {
              element.setAttribute('data-original-amount', element.textContent.trim());
              element.textContent = '';
              element.appendChild(ElementFactory.createHiddenSpan());
          }
      }

      /**
       * Restores the original currency text value back to the element.
       * @param {HTMLElement} element - The DOM element to restore.
       */
      restoreOriginalText(element) {
          const original = element.getAttribute('data-original-amount');
          if (original !== null) {
              element.textContent = original;
              element.removeAttribute('data-original-amount');
          }
          const marker = element.querySelector('[data-hidden-marker]');
          if (marker) {
              marker.remove();
          }
      }
  }

  /**
   * Handles the navigation bar Robux counter element.
   */
  class NavRobuxMasker extends RobuxElementMasker {
      mask() {
          const navRobuxAmount = document.getElementById('nav-robux-amount');
          if (!navRobuxAmount) return;

          if (PrivacyState.isHidden()) {
              this.applyHiddenLabel(navRobuxAmount);
          } else {
              this.restoreOriginalText(navRobuxAmount);
          }
      }
  }

  /**
   * Handles the text-based balance details container elements.
   */
  class BalanceContainerMasker extends RobuxElementMasker {
      mask() {
          const balanceContainers = document.querySelectorAll('.balance-label.icon-robux-container');
          balanceContainers.forEach(container => {
              const innerSpan = container.querySelector('span');
              if (!innerSpan) return;

              if (PrivacyState.isHidden()) {
                  if (!innerSpan.querySelector('[data-hidden-marker]')) {
                      this.saveAndClearTextNodes(innerSpan);
                      innerSpan.appendChild(ElementFactory.createHiddenSpan());
                  }
              } else {
                  this.restoreTextNodes(innerSpan);
              }
          });
      }

      saveAndClearTextNodes(innerSpan) {
          const textNodes = Array.from(innerSpan.childNodes);
          textNodes.forEach(node => {
              if (node.nodeType === Node.TEXT_NODE && node.textContent.trim() !== 'My Balance:' && node.textContent.trim() !== '') {
                  innerSpan.setAttribute('data-original-balance', node.textContent.trim());
                  node.textContent = '';
              }
          });
      }

      restoreTextNodes(innerSpan) {
          const original = innerSpan.getAttribute('data-original-balance');
          const marker = innerSpan.querySelector('[data-hidden-marker]');
          if (marker) marker.remove();

          if (original !== null) {
              const textNodes = Array.from(innerSpan.childNodes);
              let filled = false;
              textNodes.forEach(node => {
                  if (node.nodeType === Node.TEXT_NODE && node.textContent.trim() === '') {
                      node.textContent = ' ' + original;
                      filled = true;
                  }
              });
              if (!filled) {
                  innerSpan.appendChild(document.createTextNode(' ' + original));
              }
              innerSpan.removeAttribute('data-original-balance');
          }
      }
  }

  /**
   * Handles large extended font display elements containing Robux text.
   */
  class LargeTitleMasker extends RobuxElementMasker {
      mask() {
          const largeTitleAmounts = document.querySelectorAll('.font-builder-extended.content-action-standard.text-title-large');
          largeTitleAmounts.forEach(element => {
              if (PrivacyState.isHidden()) {
                  if (element.textContent.trim() !== '') {
                      this.applyHiddenLabel(element);
                  }
              } else {
                  this.restoreOriginalText(element);
              }
          });
      }
  }

  /**
   * Orchestrates mutation observation, manages masking components, and injects toggle UI controls.
   */
  class RobuxPrivacyManager {
      constructor() {
          this.maskers = [
              new NavRobuxMasker(),
              new BalanceContainerMasker(),
              new LargeTitleMasker()
          ];
          this.observer = null;
      }

      /**
       * Runs all registered element maskers safely.
       */
      runMaskers() {
          this.maskers.forEach(masker => masker.mask());
      }

      /**
       * Handles injection of the Privacy Toggle button inside the opened dropdown menu interface.
       */
      injectToggleButton() {
          const menu = document.getElementById('buy-robux-popover-menu');
          if (menu && !menu.querySelector('[data-privacy-toggle]')) {
              const buyButtonContainer = menu.querySelector('.rbx-menu-item-container');

              const toggleButton = ElementFactory.createToggleButton((buttonAnchor) => {
                  const isHidden = PrivacyState.toggle();
                  buttonAnchor.textContent = isHidden ? 'Unhide Robux' : 'Hide Robux';
                  this.runMaskers();
              });

              if (buyButtonContainer && buyButtonContainer.nextSibling) {
                  menu.insertBefore(toggleButton, buyButtonContainer.nextSibling);
              } else {
                  menu.appendChild(toggleButton);
              }
          }
      }

      /**
       * Starts DOM observation loop without creating recursive processing lag.
       */
      init() {
          this.observer = new MutationObserver(() => {
              this.observer.disconnect();
              this.injectToggleButton();
              this.runMaskers();
              this.observer.observe(document.body, { childList: true, subtree: true });
          });

          this.observer.observe(document.body, { childList: true, subtree: true });
          this.injectToggleButton();
          this.runMaskers();
      }
  }

  const privacyManager = new RobuxPrivacyManager();
  privacyManager.init();
})();