Greasy Fork is available in English.

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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