您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
在喇叭/音量按钮上使用滚轮调节音量
当前为
// ==UserScript== // @name DX_喇叭按钮滚轮调音_MAX // @namespace http://tampermonkey.net/ // @version 2.3.1 // @description 在喇叭/音量按钮上使用滚轮调节音量 // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @license MIT // @noframes // ==/UserScript== (() => { 'use strict'; if (window.top !== window.self || window.__SPEAKER_WHEEL_INITIALIZED__) return; window.__SPEAKER_WHEEL_INITIALIZED__ = true; // 模式定义 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 initScript = () => { // 注册菜单命令 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); if (!CONFIG.showDisplay) hideDisplay(); location.reload(); } ); }; // 初始化菜单 registerMenuCommands(); // 如果脚本被禁用,不初始化任何功能 if (!CONFIG.enabled) return; // 以下是脚本启用时的功能初始化 // 平台标志 const IS_YOUTUBE = location.hostname.includes('youtube'); // 选择器配置(合并重复选择器) const getSelectors = () => { const platformSelectors = [ // B站 '.bpx-player-ctrl-volume', '.bpx-player-volume', '.bpx-player-ctrl-mute', '.bui-volume', '.bpx-player-vol', // YouTube '.ytp-mute-button', '.ytp-volume-panel', '.ytp-volume-slider', '.ytp-volume-slider-handle', // Twitch '[data-a-target="player-volume-button"]', '.player-controls__volume-control', '.volume-slider__slider', // Netflix '[data-uia="volume-slider"]', '[data-uia="audio-toggle"]', // STEAM '.video_volume_control', '.volume_control' ]; if (!CONFIG.universal) { return [...new Set(platformSelectors)]; // 去重 } // 通用模式下的宽松选择器 const universalSelectors = [ // 类名匹配 '[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' ]; return [...new Set([...platformSelectors, ...universalSelectors])]; // 合并并去重 }; // 状态管理 const state = { display: null, target: null, volume: 50, overSpeaker: false, overDisplay: false, dragging: false, position: null, timeout: null, locked: false }; // 工具函数 const clamp = vol => Math.max(0, Math.min(100, Math.round(vol))); const clampPos = (val, size) => { const margin = 20 + size / 2; return Math.max(margin, Math.min(window.innerWidth - margin, val)); }; // 平台检测和视频处理(合并相关逻辑) const PlatformUtils = { isVideoSite(host) { const videoPlatforms = [ 'bilibili', 'youtube', 'twitch', 'netflix', 'nicovideo', 'vimeo', 'youku', 'iqiyi', 'qq.com', 'acfun' ]; return videoPlatforms.some(platform => host.includes(platform)); }, getVideo() { // 平台特定视频选择器配置 const platformVideoSelectors = { 'bilibili': '.bpx-player-video-wrap video, .bilibili-player video', 'youtube': '#movie_player video, .html5-video-player video', 'twitch': '.video-ref video, .twitch-video video', 'netflix': 'video' }; // 优先使用平台特定选择器 for (const [platform, selector] of Object.entries(platformVideoSelectors)) { if (location.hostname.includes(platform)) { const video = document.querySelector(selector); if (video && video.readyState > 0) { return video; } } } // 回退到通用检测 const videos = [...document.querySelectorAll('video')].filter(v => v.offsetParent !== null && v.readyState > 0 ); return (CONFIG.allVideos || this.isVideoSite(location.hostname)) ? videos.find(v => !v.paused) || videos[0] : null; }, getVolume(video) { if (!video) return state.volume; // YouTube特殊处理 if (IS_YOUTUBE) { const player = video.closest('#movie_player') || document.querySelector('.html5-video-player'); // 先检查YouTube是否静音 const isMuted = player?.isMuted?.(); if (isMuted) { return 0; } const platformVolume = player?.getVolume?.(); if (platformVolume !== undefined) { return platformVolume; } } // 通用静音检测 if (video.muted) { return 0; } return Math.round(video.volume * 100); }, setVolume(video, volume) { state.volume = clamp(volume); if (video) { // 记录变化前的状态 const changed = video.volume !== state.volume / 100 || video.muted !== (state.volume === 0); video.volume = state.volume / 100; // 统一静音处理 if (state.volume === 0) { video.muted = true; // YouTube特殊静音处理 if (IS_YOUTUBE) { const player = video.closest('#movie_player') || document.querySelector('.html5-video-player'); player?.mute?.(); } } else { video.muted = false; // YouTube取消静音 if (IS_YOUTUBE) { const player = video.closest('#movie_player') || document.querySelector('.html5-video-player'); player?.unMute?.(); } } // YouTube:使用原生API if (IS_YOUTUBE) { const player = video.closest('#movie_player') || document.querySelector('.html5-video-player'); if (state.volume > 0) { player?.setVolume?.(state.volume); } } // 其他平台:只有实际发生变化时才触发 else if (changed) { video.dispatchEvent(new Event('volumechange', { bubbles: true })); } } return state.volume; } }; // 位置计算函数(合并相关逻辑) const PositionUtils = { getPosition(target) { try { const rect = target?.getBoundingClientRect(); if (!rect?.width || rect.top < -50) return this.getDefaultPos(); const x = clampPos(rect.left + rect.width / 2, 60); const y = clampPos(rect.top - 100, 180); return isNaN(x) || isNaN(y) ? this.getDefaultPos() : { x, y }; } catch { return this.getDefaultPos(); } }, getDefaultPos() { return { x: clampPos(window.innerWidth / 2, 60), y: clampPos(window.innerHeight / 3, 180) }; }, applyPosition(display, pos) { display.style.left = `${pos.x}px`; display.style.top = `${pos.y}px`; display.style.transform = 'translate(-50%, -50%)'; } }; // 音量控制函数 const adjustVolume = (delta, target = null) => { const video = PlatformUtils.getVideo(); if (!video) return; // 如果视频是静音状态但音量不为0,取消静音 if (video.muted && delta > 0) { video.muted = false; } let currentVol = PlatformUtils.getVolume(video); const newVolume = clamp(currentVol + delta); PlatformUtils.setVolume(video, newVolume); showVolume(newVolume, target || state.target, true); }; // 显示/隐藏控制(合并相关逻辑) const DisplayManager = { clearHide() { clearTimeout(state.timeout); state.timeout = null; }, scheduleHide() { this.clearHide(); if (!state.overSpeaker && !state.overDisplay && !state.dragging) { state.timeout = setTimeout(this.hideDisplay, 1500); } }, resetUIState() { state.target = null; state.overSpeaker = false; state.overDisplay = false; state.locked = false; state.dragging = false; state.position = null; this.clearHide(); }, hideDisplay() { if (state.display) { state.display.style.opacity = '0'; setTimeout(() => { if (state.display?.style.opacity === '0') { state.display.style.display = 'none'; this.resetUIState(); } }, 300); } } }; // 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'); // 统一样式设置 const styles = { display: { 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' }, text: { color: '#fff', fontSize: '12px', fontFamily: 'Arial, sans-serif', fontWeight: 'bold', textAlign: 'center', minWidth: '40px', pointerEvents: 'none' }, container: { 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' }, bar: { position: 'absolute', bottom: '0', left: '0', width: '100%', height: '0%', background: '#fff', borderRadius: '2px', transition: 'height 0.1s ease', pointerEvents: 'none', minHeight: '6px' }, slider: { position: 'absolute', left: '0', width: '100%', height: '100%', cursor: 'pointer', zIndex: '3', pointerEvents: 'auto' }, handle: { position: 'absolute', left: '0', width: '100%', height: '4px', background: '#ff4444', borderRadius: '2px', transition: 'top 0.05s ease', pointerEvents: 'none', minHeight: '4px' } }; Object.assign(display.style, styles.display); Object.assign(text.style, styles.text); Object.assign(container.style, styles.container); Object.assign(bar.style, styles.bar); Object.assign(slider.style, styles.slider); Object.assign(handle.style, styles.handle); // 组装 slider.appendChild(handle); container.append(bar, slider); display.append(text, container); document.body.appendChild(display); // 事件处理(合并相关事件) const setupDisplayEvents = () => { display.addEventListener('mouseenter', () => { state.overDisplay = state.locked = true; DisplayManager.clearHide(); }); display.addEventListener('mouseleave', () => { state.overDisplay = false; DisplayManager.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); PlatformUtils.setVolume(PlatformUtils.getVideo(), 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); DisplayManager.scheduleHide(); }; document.addEventListener('mousemove', move); document.addEventListener('mouseup', up); }); // 滚轮事件 display.addEventListener('wheel', wheelHandler, { capture: true, passive: false }); }; setupDisplayEvents(); 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) { PositionUtils.applyPosition(display, state.position); } else if (target && !state.locked) { state.target = target; state.position = PositionUtils.getPosition(target); state.locked = true; PositionUtils.applyPosition(display, state.position); } else if (state.position) { PositionUtils.applyPosition(display, state.position); } else { state.position = PositionUtils.getDefaultPos(); state.locked = true; PositionUtils.applyPosition(display, state.position); } display.style.opacity = '1'; DisplayManager.clearHide(); }; // 事件处理函数 const wheelHandler = (e) => { const onVolume = state.overSpeaker || state.overDisplay; const video = PlatformUtils.getVideo(); if (onVolume && video) { e.preventDefault(); e.stopPropagation(); adjustVolume(-Math.sign(e.deltaY) * CONFIG.step, e.currentTarget); } }; // 元素事件绑定(优化去重逻辑) const bindElementEvents = (el) => { if (el.dataset.bound) return; el.dataset.bound = 'true'; el.addEventListener('mouseenter', (e) => { state.overSpeaker = true; DisplayManager.clearHide(); if (CONFIG.showDisplay && PlatformUtils.getVideo()) { showVolume(PlatformUtils.getVolume(PlatformUtils.getVideo()), e.currentTarget, false); } }); el.addEventListener('mouseleave', () => { state.overSpeaker = false; DisplayManager.scheduleHide(); }); el.addEventListener('wheel', wheelHandler, { capture: true, passive: false }); if (!el.style.cursor) { el.style.cursor = 'ns-resize'; el.title = '使用鼠标滚轮调节音量'; } }; // 事件设置(合并初始化逻辑) const setupEvents = () => { // 全局事件 document.addEventListener('mousedown', (e) => { if (!e.target.closest('#speaker-wheel-volume-display') && !state.overSpeaker) { DisplayManager.hideDisplay(); } }); // 绑定选择器(使用优化后的去重选择器) const uniqueSelectors = getSelectors(); uniqueSelectors.forEach(selector => { try { const elements = document.querySelectorAll(selector); elements.forEach(bindElementEvents); } catch {} }); // 视频监听 const video = PlatformUtils.getVideo(); if (video && !video.dataset.listener) { const handleVolumeChange = () => { if (!state.dragging) { const volume = PlatformUtils.getVolume(video); showVolume(volume, state.target, false); } }; video.addEventListener('volumechange', handleVolumeChange); video.dataset.listener = 'true'; } }; // 延迟执行初始设置 setTimeout(() => { setupEvents(); }, 300); // DOM变化监听(优化防抖逻辑) let observerTimeout; const observer = new MutationObserver(() => { clearTimeout(observerTimeout); observerTimeout = setTimeout(() => { setupEvents(); }, 1); }); observer.observe(document.body, { childList: true, subtree: true }); }; // 执行初始化 initScript(); })();