🔒 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.

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==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();
})();