Steam++ DX_喇叭按钮滚轮调音 [仅Watt Toolkit(Steam++) 特用]

在Steam客户端中喇叭/音量按钮上使用滚轮调节音量。集成唯一ID管理,优化多视频检测,支持Steam多视频独立控制

// ==UserScript==
// @name         Steam++ DX_喇叭按钮滚轮调音 [仅Watt Toolkit(Steam++) 特用]
// @namespace    http://tampermonkey.net/
// @version      1.6.1
// @description  在Steam客户端中喇叭/音量按钮上使用滚轮调节音量。集成唯一ID管理,优化多视频检测,支持Steam多视频独立控制
// @match        *://store.steampowered.com/*
// @match        *://steamcommunity.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @license      MIT
// @noframes
// ==/UserScript==

(function() {
    'use strict';
    
    // 检查是否在Steam客户端中运行
    const isSteamClient = () => {
        return navigator.userAgent.includes('Valve Steam') || 
               navigator.userAgent.includes('Steam') ||
               typeof SteamClient !== 'undefined';
    };
    
    // 如果不是Steam客户端,直接退出
    if (!isSteamClient()) {
        console.log('DX_喇叭按钮滚轮调音: 仅在Steam客户端中运行');
        return;
    }
    
    // 常量定义
    const DISPLAY_TIMEOUT = 1000;
    const INIT_DELAY = 1000;
    const RETRY_DELAY = 500;
    
    const CONFIG = {
        stepVolume: GM_getValue('SpeakerWheelStepVolume', 5)
    };
    
    // Steam平台选择器配置
    const steamSelectors = [
        'svg._1CpOAgPPD7f_fGI4HaYX6C', 
        'svg.SVGIcon_Volume', 
        'svg.SVGIcon_Button.SVGIcon_Volume',
        '[class*="volume" i] svg',
        'button:has(svg.SVGIcon_Volume)',
        'button:has(svg[class*="Volume" i])'
    ];
    
    // 轻量级唯一ID管理器
    const SimpleIdManager = {
        buttonVideoMap: new WeakMap(),
        videoIdCounter: 0,
        
        // 绑定按钮到视频
        bindButtonToVideo(button, video) {
            this.buttonVideoMap.set(button, video);
            
            if (!video.dataset.speakerVideoId) {
                this.videoIdCounter++;
                video.dataset.speakerVideoId = `speaker_video_${this.videoIdCounter}`;
            }
            
            button.dataset.boundVideoId = video.dataset.speakerVideoId;
        },
        
        // 获取绑定的视频
        getVideoByButton(button) {
            // 优先从WeakMap获取
            const cachedVideo = this.buttonVideoMap.get(button);
            if (cachedVideo && document.contains(cachedVideo)) {
                return cachedVideo;
            }
            
            // 备用:通过ID查找
            const videoId = button.dataset.boundVideoId;
            if (videoId) {
                const foundVideo = document.querySelector(`video[data-speaker-video-id="${videoId}"]`);
                if (foundVideo) {
                    this.buttonVideoMap.set(button, foundVideo);
                    return foundVideo;
                }
            }
            
            return null;
        }
    };
    
    // 工具函数集合
    const utils = {
        clampVolume: vol => Math.round(Math.max(0, Math.min(100, vol)) * 100) / 100,
        
        // 根据坐标查找视频(备用方案)
        findVideoAtPosition: (x, y) => {
            const videos = Array.from(document.querySelectorAll('video')).filter(v => 
                v.offsetParent !== null && v.offsetWidth > 100 && v.offsetHeight > 50
            );
            
            if (videos.length === 0) return null;
            if (videos.length === 1) return videos[0];
            
            // 多视频时查找最近的
            let closestVideo = null;
            let minDistance = Infinity;
            
            for (const video of videos) {
                const rect = video.getBoundingClientRect();
                const centerX = rect.left + rect.width / 2;
                const centerY = rect.top + rect.height / 2;
                
                const distance = Math.sqrt(
                    Math.pow(centerX - x, 2) + Math.pow(centerY - y, 2)
                );
                
                if (distance < minDistance) {
                    minDistance = distance;
                    closestVideo = video;
                }
            }
            
            return closestVideo;
        },
        
        // 计算元素中心坐标
        getElementCenter: (element) => {
            const rect = element.getBoundingClientRect();
            return {
                x: rect.left + rect.width / 2,
                y: rect.top + rect.height / 2
            };
        }
    };
    
    let volumeDisplay = null;
    let steamInitialized = false;
    
    // Steam按钮专用视频查找
    const findVideoForSteamButton = (button) => {
        // 优先使用唯一ID绑定
        const boundVideo = SimpleIdManager.getVideoByButton(button);
        if (boundVideo) {
            return boundVideo;
        }
        
        // 备用:坐标匹配
        const center = utils.getElementCenter(button);
        const coordVideo = utils.findVideoAtPosition(center.x, center.y);
        
        if (coordVideo && button) {
            SimpleIdManager.bindButtonToVideo(button, coordVideo);
        }
        
        return coordVideo;
    };
    
    // 查找激活的视频
    const findActiveVideo = () => {
        const allVideos = Array.from(document.querySelectorAll('video'));
        if (allVideos.length === 0) return null;
        
        // 1. 优先查找正在播放的视频
        const playingVideo = allVideos.find(v => v.offsetParent !== null && !v.paused && v.readyState > 0);
        if (playingVideo) return playingVideo;
        
        // 2. 查找可见的视频
        const visibleVideo = allVideos.find(v => v.offsetParent !== null && v.offsetWidth > 100 && v.offsetHeight > 50);
        if (visibleVideo) return visibleVideo;
        
        // 3. 返回第一个有效视频
        return allVideos.find(v => v.offsetParent !== null) || allVideos[0];
    };
    
    // 音量显示功能
    const showVolume = (vol, targetVideo = null) => {
        if (!volumeDisplay) {
            volumeDisplay = document.createElement('div');
            volumeDisplay.id = 'speaker-wheel-volume-display';
            
            Object.assign(volumeDisplay.style, {
                position: 'fixed',
                zIndex: 2147483647,
                minWidth: '90px',
                height: '50px',
                lineHeight: '50px',
                textAlign: 'center',
                borderRadius: '4px',
                backgroundColor: 'rgba(0, 0, 0, 0.7)',
                color: '#fff',
                fontSize: '24px',
                fontFamily: 'Arial, sans-serif',
                opacity: '0',
                transition: 'opacity 0.3s',
                pointerEvents: 'none'
            });
            
            document.body.appendChild(volumeDisplay);
        }
        
        // 更新位置函数
        const updatePosition = () => {
            const video = targetVideo || findActiveVideo();
            if (video && volumeDisplay) {
                const rect = video.getBoundingClientRect();
                volumeDisplay.style.top = (rect.top + rect.height / 2) + 'px';
                volumeDisplay.style.left = (rect.left + rect.width / 2) + 'px';
                volumeDisplay.style.transform = 'translate(-50%, -50%)';
            }
        };
        
        updatePosition();
        
        // 只在弹窗显示时监听滚动
        if (!volumeDisplay._scrollHandler) {
            volumeDisplay._scrollHandler = () => {
                if (volumeDisplay.style.opacity === '1') {
                    updatePosition();
                }
            };
            window.addEventListener('scroll', volumeDisplay._scrollHandler, { passive: true });
        }
        
        volumeDisplay.textContent = `${Math.round(vol)}%`;
        volumeDisplay.style.opacity = '1';
        
        if (volumeDisplay._timeout) clearTimeout(volumeDisplay._timeout);
        volumeDisplay._timeout = setTimeout(() => {
            volumeDisplay.style.opacity = '0';
        }, DISPLAY_TIMEOUT);
    };
    
    // 音量调整功能
    const adjustVolume = (video, delta) => {
        if (!video) return;
        
        const newVolume = utils.clampVolume((video.volume * 100) + delta);
        video.volume = newVolume / 100;
        video.muted = false;
        showVolume(newVolume, video);
    };
    
    // Steam平台音量图标查找
    const findVolumeIcon = (element) => {
        let currentElement = element;
        
        while (currentElement && currentElement !== document.body) {
            if (currentElement.tagName === 'svg' || currentElement.tagName === 'BUTTON') {
                // 选择器匹配
                for (const selector of steamSelectors) {
                    if (currentElement.matches?.(selector)) {
                        return currentElement;
                    }
                }
                
                // 类名匹配(作为备选)
                const className = currentElement.className || '';
                if (typeof className === 'string' && (
                    className.includes('Volume') || 
                    className.includes('volume')
                )) {
                    return currentElement;
                }
            }
            currentElement = currentElement.parentElement;
        }
        return null;
    };
    
    // Steam平台处理
    const initSteamVolume = () => {
        if (steamInitialized) return;
        
        let currentTarget = null;
        
        const mouseEnterHandler = (e) => {
            const volumeIcon = findVolumeIcon(e.target);
            if (volumeIcon) {
                currentTarget = volumeIcon;
            }
        };
        
        const mouseLeaveHandler = (e) => {
            currentTarget = null;
        };
        
        const wheelHandler = (e) => {
            if (!currentTarget) return;
            
            const volumeIcon = findVolumeIcon(e.target);
            if (volumeIcon === currentTarget) {
                e.preventDefault();
                e.stopPropagation();
                
                const targetVideo = findVideoForSteamButton(volumeIcon);
                if (!targetVideo) return;
                
                const delta = -Math.sign(e.deltaY) * CONFIG.stepVolume;
                adjustVolume(targetVideo, delta);
            }
        };
        
        // 为所有音量相关元素添加鼠标事件
        document.addEventListener('mouseover', mouseEnterHandler, { capture: true });
        document.addEventListener('mouseout', mouseLeaveHandler, { capture: true });
        document.addEventListener('wheel', wheelHandler, { 
            capture: true, 
            passive: false 
        });
        
        steamInitialized = true;
    };
    
    // Steam初始化逻辑
    const initSteamPlatform = () => {
        if (document.querySelectorAll('video').length > 0) {
            initSteamVolume();
        } else {
            setTimeout(initSteamPlatform, RETRY_DELAY);
        }
    };
    
    // 初始化函数
    const registerMenuCommands = () => {
        GM_registerMenuCommand('🔊 设置音量步进', () => {
            const newVal = prompt('设置音量步进 (%)', CONFIG.stepVolume);
            if (newVal && !isNaN(newVal)) {
                CONFIG.stepVolume = parseFloat(newVal);
                GM_setValue('SpeakerWheelStepVolume', CONFIG.stepVolume);
                alert('设置已保存');
            }
        });
    };
    
    const init = () => {
        registerMenuCommands();
        initSteamPlatform();
    };
    
    // 清理函数
    const cleanup = () => {
        if (volumeDisplay) {
            if (volumeDisplay._timeout) {
                clearTimeout(volumeDisplay._timeout);
            }
            if (volumeDisplay._scrollHandler) {
                window.removeEventListener('scroll', volumeDisplay._scrollHandler);
            }
        }
    };
    
    // 启动初始化
    window.addEventListener('beforeunload', cleanup);
    
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        setTimeout(init, INIT_DELAY);
    }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址