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

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

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

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

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

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

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