您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
在喇叭/音量按钮上使用滚轮调节音量 - 修复Steam失效问题 - 简化优化版
当前为
// ==UserScript== // @name DX_喇叭按钮滚轮调音_MAX // @namespace http://tampermonkey.net/ // @version 2.4.5 // @description 在喇叭/音量按钮上使用滚轮调节音量 - 修复Steam失效问题 - 简化优化版 // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @license MIT // @noframes // ==/UserScript== (function() { 'use strict'; if (window.top !== window.self || window.__SPEAKER_WHEEL_INITIALIZED__) return; window.__SPEAKER_WHEEL_INITIALIZED__ = true; // 简化的缓存管理 const CacheManager = { videoCache: null, getVideoCache() { return this.videoCache && document.contains(this.videoCache) ? this.videoCache : null; }, setVideoCache(video) { this.videoCache = video; }, clearVideoCache() { this.videoCache = null; } }; // 模式定义 const MODES = { 1: { id: 1, name: "保守模式", allVideos: false, universal: false, desc: "🎬关 + 🌐关 - 只处理已知平台的已知按钮" }, 2: { id: 2, name: "主流平台增强", allVideos: false, universal: true, desc: "🎬关 + 🌐开 - 只处理主流平台,但按钮识别更强" }, 3: { id: 3, name: "精准视频模式", allVideos: true, universal: false, desc: "🎬开 + 🌐关 - 只处理视频,但需要已知的音量按钮" }, 4: { id: 4, name: "全能模式", allVideos: true, universal: true, desc: "🎬开 + 🌐开 - 最大兼容性,任何网站都能用" } }; // 配置管理函数 const ConfigManager = { getSiteConfig() { const siteSettings = GM_getValue('SpeakerWheelSiteSettings', {}); const hostname = location.hostname; const siteConfig = siteSettings[hostname] || {}; const currentMode = siteConfig.mode || 1; const modeConfig = MODES[currentMode]; return { step: GM_getValue('SpeakerWheelStepVolume', 5), mode: currentMode, allVideos: modeConfig.allVideos, universal: modeConfig.universal, showDisplay: siteConfig.showVolumeDisplay ?? true, enabled: siteConfig.enabled ?? true }; }, saveSiteConfig(config) { const siteSettings = GM_getValue('SpeakerWheelSiteSettings', {}); const hostname = location.hostname; siteSettings[hostname] = { mode: config.mode, showVolumeDisplay: config.showDisplay, enabled: config.enabled }; GM_setValue('SpeakerWheelSiteSettings', siteSettings); } }; // 获取当前配置 const CONFIG = ConfigManager.getSiteConfig(); // 切换模式函数 const switchMode = () => { const nextMode = CONFIG.mode % 4 + 1; const modeConfig = MODES[nextMode]; CONFIG.mode = nextMode; CONFIG.allVideos = modeConfig.allVideos; CONFIG.universal = modeConfig.universal; ConfigManager.saveSiteConfig(CONFIG); alert(`🎬 视频检测加强 / 🌐 音频检测追加\n\n已切换到: ${modeConfig.name}\n${modeConfig.desc}\n\n页面将刷新以应用更改`); location.reload(); }; // 平台检测 const PLATFORM = (() => { const host = location.hostname; if (/youtube\.com|youtu\.be/.test(host)) return "YOUTUBE"; if (/bilibili\.com/.test(host)) return "BILIBILI"; if (/twitch\.tv/.test(host)) return "TWITCH"; if (/steam(community|powered)\.com/.test(host)) return "STEAM"; if (/netflix\.com/.test(host)) return "NETFLIX"; return "GENERIC"; })(); const IS_STEAM = PLATFORM === 'STEAM'; const IS_YOUTUBE = PLATFORM === 'YOUTUBE'; // 统一选择器配置 const PLATFORM_SELECTORS = { STEAM: ['svg._1CpOAgPPD7f_fGI4HaYX6C'], YOUTUBE: ['.ytp-mute-button', '.ytp-volume-panel', '.ytp-volume-slider', '.ytp-volume-slider-handle'], BILIBILI: ['.bpx-player-ctrl-volume', '.bpx-player-volume', '.bpx-player-ctrl-mute', '.bui-volume', '.bpx-player-vol'], TWITCH: ['[data-a-target="player-volume-button"]', '.player-controls__volume-control', '.volume-slider__slider'], NETFLIX: ['[data-uia="volume-slider"]', '[data-uia="audio-toggle"]'], GENERIC: [ '[class*="volume"]', '[class*="sound"]', '[class*="mute"]', '.volume-icon', '.mute-btn', '.volume-btn', '.mute-icon', '.s-volume', '.volume__button', '.mute__button', 'button[aria-label*="volume"]', 'button[title*="volume"]', 'button[aria-label*="mute"]', 'button[title*="mute"]', '.mute-button', '.sound-button' ] }; // 状态管理 const state = { display: null, target: null, volume: 50, overSpeaker: false, overDisplay: false, dragging: false, position: null, timeout: null, locked: false }; // 工具函数 - 简化版本 const clampVolume = vol => Math.round(Math.max(0, Math.min(100, vol))); const clampPos = (val, size) => { const margin = 20 + size / 2; return Math.max(margin, Math.min(window.innerWidth - margin, val)); }; let steamInitialized = false; // 视频元素检测函数 const getVideoElement = () => { // 先尝试从缓存获取 const cachedVideo = CacheManager.getVideoCache(); if (cachedVideo) { return cachedVideo; } let video = null; if (IS_STEAM) { video = findActiveSteamVideo(); } else { video = findActiveVideo(); } // 更新缓存 if (video) { CacheManager.setVideoCache(video); } return video; }; // 查找激活的视频 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. 如果启用全视频模式,返回第一个有效视频 if (CONFIG.allVideos) { return allVideos.find(v => v.offsetParent !== null) || allVideos[0]; } // 4. 检查是否是视频平台 const videoPlatforms = [ 'bilibili', 'youtube', 'twitch', 'netflix', 'nicovideo', 'vimeo', 'youku', 'iqiyi', 'qq.com', 'acfun' ]; const isVideoSite = videoPlatforms.some(platform => location.hostname.includes(platform)); return isVideoSite ? allVideos.find(v => v.offsetParent !== null) || allVideos[0] : null; }; // Steam平台专用视频检测 const findActiveSteamVideo = () => { const allVideos = Array.from(document.querySelectorAll('video')); if (allVideos.length === 0) return null; const playingVideo = allVideos.find(v => v.offsetParent !== null && !v.paused); if (playingVideo) return playingVideo; const visibleVideo = allVideos.find(v => v.offsetParent !== null && v.offsetWidth > 300 && v.offsetHeight > 150); return visibleVideo || allVideos.find(v => v.offsetParent !== null) || allVideos[0]; }; // 音量获取和设置函数 const getVolume = (video) => { if (!video) return state.volume; // YouTube特殊处理 if (IS_YOUTUBE) { const player = video.closest('#movie_player') || document.querySelector('.html5-video-player'); const isMuted = player?.isMuted?.(); if (isMuted) return 0; const platformVolume = player?.getVolume?.(); if (platformVolume !== undefined) return platformVolume; } // Steam特殊处理 if (IS_STEAM) { if (video.muted) return 0; return Math.round(video.volume * 100); } // 通用静音检测 if (video.muted) return 0; return Math.round(video.volume * 100); }; const setVolume = (video, volume) => { const clampedVolume = clampVolume(volume); state.volume = clampedVolume; if (video) { // Steam特殊处理 if (IS_STEAM) { video.volume = clampedVolume / 100; if (clampedVolume === 0) { video.muted = true; } else { video.muted = false; } return clampedVolume; } // 其他平台逻辑 if (clampedVolume === 0) { video.muted = true; if (IS_YOUTUBE) { const player = video.closest('#movie_player') || document.querySelector('.html5-video-player'); player?.mute?.(); } } else { video.muted = false; video.volume = clampedVolume / 100; if (IS_YOUTUBE) { const player = video.closest('#movie_player') || document.querySelector('.html5-video-player'); player?.unMute?.(); player?.setVolume?.(clampedVolume); } } video.dispatchEvent(new Event('volumechange', { bubbles: true })); } return clampedVolume; }; // 位置计算函数 const getDisplayPosition = (target) => { try { const rect = target?.getBoundingClientRect(); if (!rect?.width || rect.top < -50) return getDefaultPosition(); const x = clampPos(rect.left + rect.width / 2, 60); const y = clampPos(rect.top - 100, 180); return isNaN(x) || isNaN(y) ? getDefaultPosition() : { x, y }; } catch { return getDefaultPosition(); } }; const getDefaultPosition = () => { return { x: clampPos(window.innerWidth / 2, 60), y: clampPos(window.innerHeight / 3, 180) }; }; const applyPosition = (display, pos) => { display.style.left = `${pos.x}px`; display.style.top = `${pos.y}px`; display.style.transform = 'translate(-50%, -50%)'; }; // 显示/隐藏控制 const clearHide = () => { clearTimeout(state.timeout); state.timeout = null; }; const scheduleHide = () => { clearHide(); if (!state.overSpeaker && !state.overDisplay && !state.dragging) { state.timeout = setTimeout(() => { hideDisplay(); }, 1500); } }; const resetUIState = () => { state.target = null; state.overSpeaker = false; state.overDisplay = false; state.locked = false; state.dragging = false; state.position = null; clearHide(); }; const hideDisplay = () => { if (state.display) { state.display.style.opacity = '0'; setTimeout(() => { if (state.display?.style.opacity === '0') { state.display.style.display = 'none'; resetUIState(); } }, 300); } }; const handleScroll = () => { if (state.display && state.display.style.display !== 'none') { hideDisplay(); } }; // UI 更新函数 const updateUI = (volume, bar, text, handle) => { const percent = Math.round(volume); if (text) { text.textContent = `${percent}%`; text.style.color = '#fff'; } if (bar) { bar.style.height = `${percent}%`; bar.style.background = '#fff'; } if (handle) { handle.style.top = `${100 - percent}%`; handle.style.background = '#ff4444'; } }; // 创建显示元素 const createDisplay = () => { if (state.display) return state.display; const display = document.createElement('div'); display.id = 'speaker-wheel-volume-display'; const text = document.createElement('div'); const bar = document.createElement('div'); const slider = document.createElement('div'); const handle = document.createElement('div'); const container = document.createElement('div'); // 统一样式设置 Object.assign(display.style, { position: 'fixed', zIndex: '2147483647', padding: '15px 5px', background: 'rgba(40, 40, 40, 0.95)', borderRadius: '5px', opacity: '0', transition: 'opacity 0.3s ease', pointerEvents: 'auto', boxShadow: '0 4px 20px rgba(0,0,0,0.5)', border: '2px solid rgba(255,255,255,0.2)', backdropFilter: 'blur(10px)', display: 'none', flexDirection: 'column', alignItems: 'center', gap: '12px', userSelect: 'none' }); Object.assign(text.style, { color: '#fff', fontSize: '12px', fontFamily: 'Arial, sans-serif', fontWeight: 'bold', textAlign: 'center', minWidth: '40px', pointerEvents: 'none' }); Object.assign(container.style, { width: '8px', height: '120px', background: 'rgba(80, 80, 80, 0.8)', borderRadius: '5px', border: '1px solid rgba(255,255,255,0.1)', position: 'relative', overflow: 'hidden', pointerEvents: 'none' }); Object.assign(bar.style, { position: 'absolute', bottom: '0', left: '0', width: '100%', height: '0%', background: '#fff', borderRadius: '2px', transition: 'height 0.1s ease', pointerEvents: 'none', minHeight: '6px' }); Object.assign(slider.style, { position: 'absolute', left: '0', width: '100%', height: '100%', cursor: 'pointer', zIndex: '3', pointerEvents: 'auto' }); Object.assign(handle.style, { position: 'absolute', left: '0', width: '100%', height: '4px', background: '#ff4444', borderRadius: '2px', transition: 'top 0.05s ease', pointerEvents: 'none', minHeight: '4px' }); // 组装 slider.appendChild(handle); container.append(bar, slider); display.append(text, container); document.body.appendChild(display); // 事件处理 display.addEventListener('mouseenter', () => { state.overDisplay = state.locked = true; clearHide(); }); display.addEventListener('mouseleave', () => { state.overDisplay = false; scheduleHide(); }); // 滑块拖动处理 let dragging = false; const updateFromMouse = (y) => { const rect = slider.getBoundingClientRect(); const percent = Math.max(0, Math.min(1, (rect.bottom - y) / rect.height)); const volume = Math.round(percent * 100); const video = getVideoElement(); setVolume(video, volume); updateUI(volume, bar, text, handle); }; slider.addEventListener('mousedown', (e) => { if (e.button !== 0) return; dragging = state.dragging = state.locked = true; updateFromMouse(e.clientY); const move = (e) => dragging && updateFromMouse(e.clientY); const up = () => { dragging = state.dragging = false; document.removeEventListener('mousemove', move); document.removeEventListener('mouseup', up); scheduleHide(); }; document.addEventListener('mousemove', move); document.addEventListener('mouseup', up); }); // 滚轮事件 display.addEventListener('wheel', wheelHandler, { capture: true, passive: false }); state.display = display; display.elements = { text, bar, handle }; return state.display; }; // 显示音量 const showVolume = (vol, target, isWheel = false) => { if (!CONFIG.showDisplay) return; if (!state.overSpeaker && !isWheel && !state.dragging) { return; } const display = createDisplay(); const { bar, text, handle } = display.elements; updateUI(vol, bar, text, handle); display.style.display = 'flex'; if (isWheel && state.position) { applyPosition(display, state.position); } else if (target && !state.locked) { state.target = target; state.position = getDisplayPosition(target); state.locked = true; applyPosition(display, state.position); } else if (state.position) { applyPosition(display, state.position); } else { state.position = getDefaultPosition(); state.locked = true; applyPosition(display, state.position); } display.style.opacity = '1'; clearHide(); }; // 音量调整功能 const adjustVolume = (delta, target = null) => { const video = getVideoElement(); if (!video) return; let currentVol = getVolume(video); // Steam平台特殊处理 if (IS_STEAM && video.muted && delta > 0) { video.muted = false; const newVolume = setVolume(video, CONFIG.step); showVolume(newVolume, target || state.target, true); return; } // 如果视频是静音状态但音量不为0,取消静音 if (video.muted && delta > 0) { video.muted = false; } const newVolume = setVolume(video, currentVol + delta); showVolume(newVolume, target || state.target, true); }; // 滚轮事件处理 const wheelHandler = (e) => { const onVolume = state.overSpeaker || state.overDisplay; const video = getVideoElement(); if (onVolume && video) { e.preventDefault(); e.stopPropagation(); adjustVolume(-Math.sign(e.deltaY) * CONFIG.step, e.currentTarget); } }; // 选择器配置 - 使用统一的选择器配置 const getSelectors = () => { const platformSelectors = [ ...PLATFORM_SELECTORS.BILIBILI, ...PLATFORM_SELECTORS.YOUTUBE, ...PLATFORM_SELECTORS.TWITCH, ...PLATFORM_SELECTORS.NETFLIX, ...PLATFORM_SELECTORS.STEAM ]; if (!CONFIG.universal) { return [...new Set(platformSelectors)]; } else { return [...new Set([...platformSelectors, ...PLATFORM_SELECTORS.GENERIC])]; } }; // 元素事件绑定 const bindElementEvents = (el) => { if (el.dataset.bound) return; el.dataset.bound = 'true'; el.addEventListener('mouseenter', (e) => { state.overSpeaker = true; clearHide(); if (CONFIG.showDisplay && getVideoElement()) { showVolume(getVolume(getVideoElement()), e.currentTarget, false); } }); el.addEventListener('mouseleave', () => { state.overSpeaker = false; scheduleHide(); }); el.addEventListener('wheel', wheelHandler, { capture: true, passive: false }); if (!el.style.cursor) { el.style.cursor = 'ns-resize'; el.title = '使用鼠标滚轮调节音量'; } }; // Steam平台处理 const initSteamVolume = () => { if (steamInitialized) return; const setupGlobalClickHide = () => { document.addEventListener('mousedown', (e) => { if (!e.target.closest('#speaker-wheel-volume-display') && !state.overSpeaker) { hideDisplay(); } }); }; const bindSteamElements = () => { // 使用统一的选择器配置 PLATFORM_SELECTORS.STEAM.forEach(selector => { try { const elements = document.querySelectorAll(selector); elements.forEach(el => { if (!el.dataset.bound) { el.dataset.bound = 'true'; el.addEventListener('mouseenter', (e) => { state.overSpeaker = true; clearHide(); if (CONFIG.showDisplay && getVideoElement()) { const video = getVideoElement(); const volume = video.muted ? 0 : getVolume(video); showVolume(volume, e.currentTarget, false); } }); el.addEventListener('mouseleave', () => { state.overSpeaker = false; scheduleHide(); }); el.addEventListener('wheel', wheelHandler, { capture: true, passive: false }); el.addEventListener('click', () => { setTimeout(() => { const video = getVideoElement(); if (video && video.muted && state.display && state.display.style.display !== 'none') { showVolume(0, state.target, false); } }, 50); }); if (!el.style.cursor) { el.style.cursor = 'ns-resize'; el.title = '使用鼠标滚轮调节音量'; } } }); } catch (e) { console.log('Steam元素绑定失败:', e); } }); }; // 初始绑定 setupGlobalClickHide(); window.addEventListener('scroll', handleScroll, { passive: true }); document.addEventListener('scroll', handleScroll, { passive: true }); bindSteamElements(); // 监听DOM变化 const observer = new MutationObserver(() => { bindSteamElements(); CacheManager.clearVideoCache(); }); observer.observe(document.body, { childList: true, subtree: true }); steamInitialized = true; }; // 标准平台处理 const setupEvents = () => { // 全局事件 document.addEventListener('mousedown', (e) => { if (!e.target.closest('#speaker-wheel-volume-display') && !state.overSpeaker) { hideDisplay(); } }); window.addEventListener('scroll', handleScroll, { passive: true }); document.addEventListener('scroll', handleScroll, { passive: true }); // 绑定选择器 const uniqueSelectors = getSelectors(); uniqueSelectors.forEach(selector => { try { const elements = document.querySelectorAll(selector); elements.forEach(bindElementEvents); } catch {} }); // 视频监听 const video = getVideoElement(); if (video && !video.dataset.listener) { video.addEventListener('volumechange', () => { if (!state.dragging) { const volume = getVolume(video); showVolume(volume, state.target, false); } }); video.dataset.listener = 'true'; } }; // 菜单命令注册 const registerMenuCommands = () => { GM_registerMenuCommand( `🚀 本站脚本开关${CONFIG.enabled ? ' ✅' : ' ❌'}`, () => { CONFIG.enabled = !CONFIG.enabled; ConfigManager.saveSiteConfig(CONFIG); alert(`脚本已${CONFIG.enabled ? '启用' : '禁用'}\n页面将刷新`); location.reload(); } ); if (!CONFIG.enabled) return; const currentMode = MODES[CONFIG.mode]; const nextModeId = CONFIG.mode % 4 + 1; const nextMode = MODES[nextModeId]; GM_registerMenuCommand( `🔄 ${currentMode.name} → ${nextMode.name}`, switchMode ); GM_registerMenuCommand('🔊 设置音量步进', () => { const newVal = prompt('设置音量调整步进 (%)', CONFIG.step); if (newVal && !isNaN(newVal)) { const step = parseFloat(newVal); GM_setValue('SpeakerWheelStepVolume', step); alert('设置已保存'); location.reload(); } }); GM_registerMenuCommand( `👁️ 音量滑块(第三方)${CONFIG.showDisplay ? ' ✅' : ' ❌'}`, () => { CONFIG.showDisplay = !CONFIG.showDisplay; ConfigManager.saveSiteConfig(CONFIG); location.reload(); } ); }; // 初始化函数 const initSteamPlatform = () => { const tryInit = () => { const video = getVideoElement(); if (video) { initSteamVolume(); } else { setTimeout(tryInit, 500); } }; tryInit(); }; const initStandardPlatform = () => { setupEvents(); const observer = new MutationObserver(() => { setupEvents(); CacheManager.clearVideoCache(); }); observer.observe(document.body, { childList: true, subtree: true }); }; const init = () => { registerMenuCommands(); if (!CONFIG.enabled) return; if (IS_STEAM) { initSteamPlatform(); } else { setTimeout(() => { initStandardPlatform(); }, 300); } }; // 页面卸载时清理缓存 window.addEventListener('beforeunload', () => { CacheManager.clearVideoCache(); }); // 启动初始化 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { setTimeout(init, 1000); } })();