登录

【油猴脚本】烧饼娱乐 - 游戏音量调节

568
  • 楼主博主

    默认 60% 音量

    image.webp

    代码
    // ==UserScript==
    // @name         烧饼娱乐游戏音量调节
    // @namespace    https://sb.sb/
    // @version      1.0.0
    // @description  点击音量按钮弹出音量调节面板
    // @author       Antigravity
    // @match        *://sb.sb/games/*
    // @match        *://sb.sb/games*
    // @include      *://sb.sb/games/*
    // @include      *://sb.sb/games*
    // @run-at       document-start
    // @grant        none
    // ==/UserScript==
    
    (function () {
      'use strict';
    
      // --- 1. 配置与状态初始化 ---
      const STORAGE_KEY_VOLUME = 'bbs-game-master-volume';
      const STORAGE_KEY_MUTE = 'bbs-wheel-mute';
      const MUTE_BTN_SELECTOR = 'button[data-ddz-mute], button[data-wheel-mute], button[data-slot-mute], button.wheel-mute, button.slot-mute, [data-mute]';
      const CONTAINER_SELECTOR = '.wheel-panel, [data-wheel-panel], .form-panel, [data-ddz], .ddz-panel';
    
      // 读取本地存储音量,缺省为 60%
      let currentVolume = parseInt(localStorage.getItem(STORAGE_KEY_VOLUME) ?? '60', 10);
      if (isNaN(currentVolume) || currentVolume < 0 || currentVolume > 100) {
        currentVolume = 60;
      }
    
      // --- 2. 音频总线劫持与控制 ---
      const ctxMasterGains = new WeakMap();
      const allMasterGainNodes = new Set();
      const activeMediaElements = new Set();
      const origConnect = AudioNode.prototype.connect;
    
      // 为每个 AudioContext 获取或创建独立的主增益节点
      function getOrCreateMasterGain(ctx) {
        if (!ctx || !ctx.destination) return null;
        let master = ctxMasterGains.get(ctx);
        if (!master) {
          try {
            master = ctx.createGain();
            const isMuted = localStorage.getItem(STORAGE_KEY_MUTE) === '1';
            master.gain.value = isMuted ? 0 : (currentVolume / 100);
            origConnect.call(master, ctx.destination);
            ctxMasterGains.set(ctx, master);
            allMasterGainNodes.add(master);
          } catch (e) {
            return null;
          }
        }
        return master;
      }
    
      // 拦截 Web Audio 目标连接,统一经由 MasterGainNode 控制输出增益
      AudioNode.prototype.connect = function (destination, ...args) {
        try {
          if (this.context && destination === this.context.destination) {
            const master = getOrCreateMasterGain(this.context);
            if (master && this !== master) {
              return origConnect.call(this, master, ...args);
            }
          }
        } catch {}
        return origConnect.apply(this, [destination, ...args]);
      };
    
      // 拦截 HTMLMediaElement 播放,保持音量同步
      const origMediaPlay = HTMLMediaElement.prototype.play;
      HTMLMediaElement.prototype.play = function () {
        activeMediaElements.add(this);
        const isMuted = localStorage.getItem(STORAGE_KEY_MUTE) === '1';
        try {
          this.volume = isMuted ? 0 : (currentVolume / 100);
        } catch {}
        return origMediaPlay.apply(this, arguments);
      };
    
      // 跟踪动态创建的 Audio 实例
      if (typeof window.Audio === 'function') {
        const OrigAudio = window.Audio;
        window.Audio = function (...args) {
          const audio = new OrigAudio(...args);
          activeMediaElements.add(audio);
          const isMuted = localStorage.getItem(STORAGE_KEY_MUTE) === '1';
          try {
            audio.volume = isMuted ? 0 : (currentVolume / 100);
          } catch {}
          return audio;
        };
        window.Audio.prototype = OrigAudio.prototype;
      }
    
      // 全局下发音量与静音状态
      function setMasterGain(valPercent) {
        const isMuted = localStorage.getItem(STORAGE_KEY_MUTE) === '1';
        const targetGain = isMuted ? 0 : (valPercent / 100);
    
        // 1. 同步 Web Audio 增益
        allMasterGainNodes.forEach((gainNode) => {
          try {
            gainNode.gain.setValueAtTime(targetGain, gainNode.context.currentTime);
          } catch {
            try {
              gainNode.gain.value = targetGain;
            } catch {}
          }
        });
    
        // 2. 同步动态与内存中的媒体元素
        activeMediaElements.forEach((el) => {
          try {
            el.volume = targetGain;
          } catch {}
        });
    
        // 3. 同步页面现存音视频元素
        document.querySelectorAll('audio, video').forEach((el) => {
          try {
            el.volume = targetGain;
            activeMediaElements.add(el);
          } catch {}
        });
    
        // 4. 同步挂载在音乐按钮属性中的音频实例
        document.querySelectorAll('[data-slot-music]').forEach((btn) => {
          if (btn.audio) {
            try {
              btn.audio.volume = targetGain;
              activeMediaElements.add(btn.audio);
            } catch {}
          }
        });
      }
    
      // --- 3. UI 样式注入 ---
      const styleEl = document.createElement('style');
      styleEl.id = 'sb-game-vol-custom-style';
      styleEl.textContent = `
        .wheel-panel > .sb-vol-popup,
        .form-panel > .sb-vol-popup,
        .ddz-panel > .sb-vol-popup,
        .sb-vol-popup {
          position: absolute !important;
          z-index: 99999 !important;
          width: 250px;
          padding: 12px 14px;
          background: rgba(13, 29, 51, 0.96);
          border: 1px solid rgba(217, 164, 65, 0.7);
          border-radius: 12px;
          box-shadow: 0 8px 24px rgba(0, 0, 0, 0.75), 0 0 12px rgba(217, 164, 65, 0.25);
          backdrop-filter: blur(10px);
          display: none;
          flex-direction: column;
          gap: 10px;
          color: #ffe9a8;
          font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", sans-serif;
          box-sizing: border-box;
          user-select: none;
          animation: sbFadeIn 0.18s cubic-bezier(0.16, 1, 0.3, 1);
        }
        .wheel-panel > .sb-vol-popup.is-open,
        .form-panel > .sb-vol-popup.is-open,
        .ddz-panel > .sb-vol-popup.is-open,
        .sb-vol-popup.is-open {
          display: flex !important;
        }
        @keyframes sbFadeIn {
          from { opacity: 0; transform: translateY(-4px) scale(0.96); }
          to { opacity: 1; transform: translateY(0) scale(1); }
        }
        .sb-vol-header {
          display: flex;
          justify-content: space-between;
          align-items: center;
          font-size: 13px;
          font-weight: 600;
          letter-spacing: 0.5px;
        }
        .sb-vol-percent {
          color: #ffd02f;
          font-family: monospace;
          font-size: 14px;
        }
        .sb-vol-main-row {
          display: flex;
          align-items: center;
          gap: 10px;
        }
        .sb-vol-mute-btn {
          flex-shrink: 0;
          width: 32px;
          height: 32px;
          padding: 0;
          border: 1px solid rgba(217, 164, 65, 0.5);
          border-radius: 50%;
          background: rgba(212, 42, 32, 0.85);
          color: #ffe9a8;
          cursor: pointer;
          display: inline-flex;
          align-items: center;
          justify-content: center;
          transition: all 0.2s;
        }
        .sb-vol-mute-btn:hover {
          background: rgba(227, 55, 44, 1);
          transform: scale(1.05);
        }
        .sb-vol-mute-btn.is-muted {
          background: rgba(95, 95, 95, 0.85);
          border-color: rgba(189, 189, 189, 0.5);
          color: #ccc;
        }
        .sb-vol-mute-btn svg {
          width: 18px;
          height: 18px;
        }
        .sb-vol-range {
          flex: 1;
          height: 6px;
          -webkit-appearance: none;
          background: rgba(255, 255, 255, 0.15);
          border-radius: 3px;
          outline: none;
          cursor: pointer;
          transition: background 0.2s;
        }
        .sb-vol-range::-webkit-slider-thumb {
          -webkit-appearance: none;
          width: 16px;
          height: 16px;
          border-radius: 50%;
          background: #ffd02f;
          border: 2px solid #fff;
          box-shadow: 0 0 6px rgba(0,0,0,0.5);
          cursor: pointer;
          transition: transform 0.1s;
        }
        .sb-vol-range::-webkit-slider-thumb:hover {
          transform: scale(1.15);
        }
        .sb-vol-presets {
          display: flex;
          gap: 6px;
          justify-content: space-between;
        }
        .sb-vol-preset-btn {
          flex: 1;
          padding: 4px 0;
          font-size: 11px;
          border: 1px solid rgba(217, 164, 65, 0.35);
          border-radius: 4px;
          background: rgba(255, 255, 255, 0.06);
          color: #ffe9a8;
          cursor: pointer;
          transition: all 0.15s;
          text-align: center;
        }
        .sb-vol-preset-btn:hover {
          background: rgba(217, 164, 65, 0.2);
          border-color: #ffd02f;
        }
        .sb-vol-preset-btn.is-active {
          background: #ffd02f;
          color: #0d1d33;
          font-weight: 700;
          border-color: #ffd02f;
        }
      `;
    
      function injectStyle() {
        if (!document.getElementById('sb-game-vol-custom-style')) {
          (document.head || document.documentElement).appendChild(styleEl);
        }
      }
      injectStyle();
    
      // --- 4. 弹窗 UI 构建与交互控制 ---
      let popup = null;
      let slider = null;
      let percentTxt = null;
      let innerMuteBtn = null;
      let iconOn = null;
      let iconOff = null;
      let presetBtns = [];
      let activeMuteBtn = null;
      let suppressCapture = false;
    
      // 创建弹窗 DOM 结构
      function createPopup(container) {
        if (document.getElementById('sb-volume-popup')) {
          popup = document.getElementById('sb-volume-popup');
          return popup;
        }
    
        popup = document.createElement('div');
        popup.id = 'sb-volume-popup';
        popup.className = 'sb-vol-popup';
        popup.innerHTML = `
          <div class="sb-vol-header">
            <span>🎮 游戏音量</span>
            <span class="sb-vol-percent" id="sb-vol-percent-txt">${currentVolume}%</span>
          </div>
          <div class="sb-vol-main-row">
            <button type="button" class="sb-vol-mute-btn" id="sb-vol-inner-mute" title="静音切换">
              <svg class="sb-icon-on" viewBox="0 0 24 24" fill="currentColor">
                <path d="M4 9h3.5L12 5v14l-4.5-4H4z"></path>
                <path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" d="M15.5 9a4 4 0 010 6M18 6.5a7.5 7.5.0 010 11"></path>
              </svg>
              <svg class="sb-icon-off" viewBox="0 0 24 24" fill="currentColor" style="display:none;">
                <path d="M4 9h3.5L12 5v14l-4.5-4H4z"></path>
                <path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" d="m16 9.5 5 5m0-5-5 5"></path>
              </svg>
            </button>
            <input type="range" class="sb-vol-range" id="sb-vol-slider" min="0" max="100" value="${currentVolume}">
          </div>
          <div class="sb-vol-presets">
            <button type="button" class="sb-vol-preset-btn ${currentVolume === 30 ? 'is-active' : ''}" data-val="30">30%</button>
            <button type="button" class="sb-vol-preset-btn ${currentVolume === 60 ? 'is-active' : ''}" data-val="60">60%</button>
            <button type="button" class="sb-vol-preset-btn ${currentVolume === 80 ? 'is-active' : ''}" data-val="80">80%</button>
            <button type="button" class="sb-vol-preset-btn ${currentVolume === 100 ? 'is-active' : ''}" data-val="100">100%</button>
          </div>
        `;
    
        (container || document.body).appendChild(popup);
    
        slider = popup.querySelector('#sb-vol-slider');
        percentTxt = popup.querySelector('#sb-vol-percent-txt');
        innerMuteBtn = popup.querySelector('#sb-vol-inner-mute');
        iconOn = innerMuteBtn.querySelector('.sb-icon-on');
        iconOff = innerMuteBtn.querySelector('.sb-icon-off');
        presetBtns = popup.querySelectorAll('.sb-vol-preset-btn');
    
        // 内部静音按钮:触发原按钮静音切换
        innerMuteBtn.addEventListener('click', function (e) {
          e.stopPropagation();
          const btn = activeMuteBtn || document.querySelector(MUTE_BTN_SELECTOR);
          if (btn) {
            suppressCapture = true;
            btn.click();
          } else {
            const wasMuted = localStorage.getItem(STORAGE_KEY_MUTE) === '1';
            localStorage.setItem(STORAGE_KEY_MUTE, wasMuted ? '0' : '1');
          }
          setTimeout(syncMuteUI, 30);
        });
    
        // 滑块滑动调节
        slider.addEventListener('input', (e) => applyVolume(e.target.value));
    
        // 预设音量按钮点击
        presetBtns.forEach((btn) => {
          btn.addEventListener('click', (e) => {
            e.stopPropagation();
            applyVolume(btn.dataset.val);
          });
        });
    
        syncMuteUI();
        return popup;
      }
    
      // 同步静音图标及高亮状态
      function syncMuteUI() {
        if (!innerMuteBtn || !iconOn || !iconOff) return;
        const btn = activeMuteBtn || document.querySelector(MUTE_BTN_SELECTOR);
        const isMuted = btn
          ? (btn.classList.contains('is-muted') || btn.getAttribute('aria-pressed') === 'true' || localStorage.getItem(STORAGE_KEY_MUTE) === '1')
          : (localStorage.getItem(STORAGE_KEY_MUTE) === '1');
    
        innerMuteBtn.classList.toggle('is-muted', isMuted);
        iconOn.style.display = isMuted ? 'none' : 'block';
        iconOff.style.display = isMuted ? 'block' : 'none';
    
        setMasterGain(currentVolume);
      }
    
      // 应用音量数值
      function applyVolume(val) {
        currentVolume = parseInt(val, 10);
        if (slider) slider.value = currentVolume;
        if (percentTxt) percentTxt.textContent = `${currentVolume}%`;
        localStorage.setItem(STORAGE_KEY_VOLUME, currentVolume);
    
        const btn = activeMuteBtn || document.querySelector(MUTE_BTN_SELECTOR);
        const isMuted = btn
          ? (btn.classList.contains('is-muted') || btn.getAttribute('aria-pressed') === 'true' || localStorage.getItem(STORAGE_KEY_MUTE) === '1')
          : (localStorage.getItem(STORAGE_KEY_MUTE) === '1');
    
        // 静音时增加音量自动解除静音;降至 0 自动静音
        if (currentVolume > 0 && isMuted && btn) {
          suppressCapture = true;
          btn.click();
        } else if (currentVolume === 0 && !isMuted && btn) {
          suppressCapture = true;
          btn.click();
        }
    
        setMasterGain(currentVolume);
    
        presetBtns.forEach((pBtn) => {
          pBtn.classList.toggle('is-active', parseInt(pBtn.dataset.val, 10) === currentVolume);
        });
        syncMuteUI();
      }
    
      // 挂载弹窗至对应容器
      function ensurePopupAttached(targetBtn) {
        const container = (targetBtn && targetBtn.closest(CONTAINER_SELECTOR)) ||
                          document.querySelector(CONTAINER_SELECTOR) ||
                          (targetBtn && targetBtn.parentElement) ||
                          document.body;
    
        if (!popup) {
          createPopup(container);
        } else if (popup.parentElement !== container) {
          container.appendChild(popup);
        }
      }
    
      // 弹窗定位:吸附于音效按钮正下方
      function updatePosition(targetBtn) {
        const btn = targetBtn || activeMuteBtn || document.querySelector(MUTE_BTN_SELECTOR);
        if (!btn || !popup || !popup.parentElement) return;
    
        const cRect = popup.parentElement.getBoundingClientRect();
        const mRect = btn.getBoundingClientRect();
    
        const offsetTopInContainer = mRect.top - cRect.top;
        const offsetRightInContainer = cRect.right - mRect.right;
    
        popup.style.top = `${offsetTopInContainer + mRect.height + 8}px`;
        popup.style.right = `${Math.max(8, offsetRightInContainer)}px`;
        popup.style.left = 'auto';
      }
    
      function openPopup(targetBtn) {
        ensurePopupAttached(targetBtn);
        popup.classList.add('is-open');
        syncMuteUI();
        updatePosition(targetBtn);
      }
    
      function closePopup() {
        if (popup) popup.classList.remove('is-open');
      }
    
      function togglePopup(targetBtn) {
        if (popup && popup.classList.contains('is-open')) {
          closePopup();
        } else {
          openPopup(targetBtn);
        }
      }
    
      // --- 5. 事件监听与委托 ---
      // 全局捕获阶段拦截音效按钮点击
      document.addEventListener(
        'click',
        function (e) {
          const muteBtn = e.target.closest && e.target.closest(MUTE_BTN_SELECTOR);
          if (!muteBtn) return;
    
          // 内部触发时放行原生点击
          if (suppressCapture) {
            suppressCapture = false;
            return;
          }
    
          e.stopImmediatePropagation();
          e.preventDefault();
    
          activeMuteBtn = muteBtn;
          togglePopup(muteBtn);
        },
        true
      );
    
      // 点击外部区域自动关闭
      document.addEventListener('click', function (e) {
        if (!popup || !popup.classList.contains('is-open')) return;
        if (!popup.contains(e.target) && !(activeMuteBtn && activeMuteBtn.contains(e.target))) {
          closePopup();
        }
      });
    
      // ESC 键关闭
      document.addEventListener('keydown', function (e) {
        if (e.key === 'Escape' && popup && popup.classList.contains('is-open')) {
          closePopup();
        }
      });
    
      // 窗口大小变化重新定位
      window.addEventListener('resize', () => {
        if (popup && popup.classList.contains('is-open')) updatePosition();
      });
    
      // --- 6. DOM 初始化与观察 ---
      function safeInit() {
        injectStyle();
        const muteBtn = document.querySelector(MUTE_BTN_SELECTOR);
        if (muteBtn) {
          ensurePopupAttached(muteBtn);
        }
      }
    
      if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', safeInit);
      } else {
        safeInit();
      }
    
      // 监听 DOM 树加载与节点动态注入
      const observer = new MutationObserver(() => {
        injectStyle();
        if (!document.getElementById('sb-volume-popup') && document.querySelector(MUTE_BTN_SELECTOR)) {
          safeInit();
        }
      });
    
      const targetRoot = document.documentElement || document.body;
      if (targetRoot) {
        try {
          observer.observe(targetRoot, { childList: true, subtree: true });
        } catch (e) {
          document.addEventListener('DOMContentLoaded', () => {
            observer.observe(document.body, { childList: true, subtree: true });
          });
        }
      }
    })();
    
    
    收藏支持反对打赏
    个性签名

    我是个性签名

8 条回复

发表回复