☰

πŸ”’ 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.

Π§Ρ‚ΠΎΠ±Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ этот скрипт, Π²Ρ‹ сначала Π΄ΠΎΠ»ΠΆΠ½Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅ Π±Ρ€Π°ΡƒΠ·Π΅Ρ€Π°, Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€ Tampermonkey, Greasemonkey ΠΈΠ»ΠΈ Violentmonkey.

Для установки этого скрипта Π²Π°ΠΌ Π½Π΅ΠΎΠ±Ρ…ΠΎΠ΄ΠΈΠΌΠΎ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅, Ρ‚Π°ΠΊΠΎΠ΅ ΠΊΠ°ΠΊ Tampermonkey.

Π§Ρ‚ΠΎΠ±Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ этот скрипт, Π²Ρ‹ сначала Π΄ΠΎΠ»ΠΆΠ½Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅ Π±Ρ€Π°ΡƒΠ·Π΅Ρ€Π°, Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€ Tampermonkey ΠΈΠ»ΠΈ Violentmonkey.

Π§Ρ‚ΠΎΠ±Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ этот скрипт, Π²Ρ‹ сначала Π΄ΠΎΠ»ΠΆΠ½Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅ Π±Ρ€Π°ΡƒΠ·Π΅Ρ€Π°, Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€ Tampermonkey ΠΈΠ»ΠΈ Userscripts.

Π§Ρ‚ΠΎΠ±Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ этот скрипт, сначала Π²Ρ‹ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ ΡƒΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅ Π±Ρ€Π°ΡƒΠ·Π΅Ρ€Π°, Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€ 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();
})();