您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
新增自定义修饰键微调音量功能。滚轮、013速度、28音量、46+-5sec、5(空白键)播放暂停、enter全萤幕切换、小键盘+-增减10%进度。完整支援:YouTube、B站、Steam。B站直播(局部)
// ==UserScript== // @name 滾動音量Dx版 Scroll Volume Dx Edition // @name:zh-CN 滚动音量Dx版 // @name:en Scroll Volume Dx Edition // @namespace http://tampermonkey.net/ // @version 9.6.1 // @description 新增自訂修飾鍵微調音量功能。滾輪、013速度、28音量、46+-5sec、5(空白鍵)播放暫停、enter全螢幕切換、小鍵盤+-增減10%進度。完整支援:YouTube、B站、Steam。B站直播(局部) // @description:zh-CN 新增自定义修饰键微调音量功能。滚轮、013速度、28音量、46+-5sec、5(空白键)播放暂停、enter全萤幕切换、小键盘+-增减10%进度。完整支援:YouTube、B站、Steam。B站直播(局部) // @description:en Added custom modifier key for fine volume adjustment. wheel scroll for volume. NumpadKey:013 for speed, 28 for volume, 46 for 5sec、5(space) for play/pause, enter for fullscreen, numpad+- for 5sec. Fully supports: YouTube, Bilibili, Steam. Bilibili live (partial) // @match *://www.youtube.com/* // @match *://www.bilibili.com/* // @match *://live.bilibili.com/* // @match *://www.twitch.tv/* // @match *://store.steampowered.com/* // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @license MIT // ==/UserScript== (function() { 'use strict'; const LANG = /^zh-(cn|tw|hk|mo|sg)/i.test(navigator.language) ? 'zh' : 'en'; const i18n = { zh: { menuStep: '⚙️ 設定步進', menuLongStep: '⏱️ 設定長步進', menuVolumeStep: '🔊 設定音量步進', menuModifier: '🎚️ 設定修飾鍵微調', menuKeyFunc: '🎛️ 設定按鍵7/9功能', promptStep: '設定快進/快退 (秒)', promptLongStep: '設定長跳轉 (秒)', promptVolume: '設定音量幅度 (%)', modifierOptions: { 1: '1. Alt 鍵', 2: '2. Ctrl 鍵', 3: '3. Shift 鍵', 4: '4. Meta 鍵 (⌘)', 5: '5. 關閉此功能' }, keyFuncOptions: { 1: '1. 長步進', 2: '2. 上一頁/下一頁', 3: '3. 上/下一個影片', 4: '4. 平台原生功能' }, saveAlert: '設定已保存,需重新整理頁面後生效' }, en: { menuStep: '⚙️ Set Step', menuLongStep: '⏱️ Set Long Step', menuVolumeStep: '🔊 Set Volume Step', menuModifier: '🎚️ Set Modifier Key', menuKeyFunc: '🎛️ Set Key 7/9 Function', promptStep: 'Set step time (seconds)', promptLongStep: 'Set long jump time (seconds)', promptVolume: 'Set volume step (%)', modifierOptions: { 1: '1. Alt key', 2: '2. Ctrl key', 3: '3. Shift key', 4: '4. Meta key (⌘)', 5: '5. Disable feature' }, keyFuncOptions: { 1: '1. Long step', 2: '2. Browser navigation', 3: '3. Previous/Next video', 4: '4. Platform native' }, saveAlert: 'Settings saved. Refresh page to apply' } }; // 配置菜单本地化 const registerMenuCommands = () => { const t = i18n[LANG]; GM_registerMenuCommand(t.menuStep, () => handleConfigPrompt(t.promptStep, 'stepTime')); GM_registerMenuCommand(t.menuLongStep, () => handleConfigPrompt(t.promptLongStep, 'stepTimeLong')); GM_registerMenuCommand(t.menuVolumeStep, () => handleConfigPrompt(t.promptVolume, 'stepVolume')); GM_registerMenuCommand(t.menuModifier, handleModifierSetting); GM_registerMenuCommand(t.menuKeyFunc, handleKeyFunctionSetting); }; const handleConfigPrompt = (promptText, configKey) => { const newVal = prompt(promptText, CONFIG[configKey]); if (newVal && !isNaN(newVal)) { CONFIG[configKey] = parseFloat(newVal); saveConfig(CONFIG); } }; const handleModifierSetting = () => { const t = i18n[LANG]; const options = t.modifierOptions; const choice = prompt( `${LANG === 'zh' ? '選擇音量微調修飾鍵:' : 'Select modifier key:'}\n${Object.values(options).join('\n')}`, CONFIG.modifierKey ); if (choice && options[choice]) { CONFIG.modifierKey = parseInt(choice); saveConfig(CONFIG); alert(t.saveAlert); } }; const handleKeyFunctionSetting = () => { const t = i18n[LANG]; const baseOptions = {...t.keyFuncOptions}; if (!['YOUTUBE', 'BILIBILI'].includes(PLATFORM)) delete baseOptions[4]; const getChoice = (msgKey, currentVal) => { const message = `${LANG === 'zh' ? '選擇按鍵功能:' : 'Select key function:'}\n${Object.values(baseOptions).join('\n')}`; return prompt(message, currentVal); }; const choice7 = getChoice('key7', CONFIG.key7Function); if (choice7 && baseOptions[choice7]) CONFIG.key7Function = parseInt(choice7); const choice9 = getChoice('key9', CONFIG.key9Function); if (choice9 && baseOptions[choice9]) CONFIG.key9Function = parseInt(choice9); saveConfig(CONFIG); }; // 获取标准化的域名标识 (简化为二级域名) const getDomainId = () => { const hostParts = location.hostname.split('.'); return hostParts.length > 2 ? hostParts.slice(-2).join('_') : hostParts.join('_'); }; // 平台检测 const PLATFORM = (() => { const host = location.hostname; if (/youtube\.com|youtu\.be/.test(host)) return "YOUTUBE"; if (/www.bilibili\.com/.test(host)) return "BILIBILI"; if (/twitch\.tv/.test(host)) return "TWITCH"; if (/steam(community|powered)\.com/.test(host)) return "STEAM"; return "GENERIC"; })(); // 存储配置结构优化 const CONFIG_STORAGE_KEY = 'ScrollVolumeDxConfig'; const DEFAULT_CONFIG = { stepTime: 5, stepTimeLong: 30, stepVolume: 10, key7Function: ['YOUTUBE', 'BILIBILI'].includes(PLATFORM) ? 4 : 1, key9Function: ['YOUTUBE', 'BILIBILI'].includes(PLATFORM) ? 4 : 1, modifierKey: 5, // 新增:1=Alt 2=Ctrl 3=Shift 4=Meta 5=None fineVolumeStep: 1 // 微调音量步进值 }; // 获取配置(修复自定义参数保存问题) const getConfig = () => { const savedConfig = GM_getValue(CONFIG_STORAGE_KEY, {}); const domainId = getDomainId(); // 返回深拷贝的配置对象 return { ...DEFAULT_CONFIG, ...(savedConfig[domainId] || {}) }; }; // 保存配置(修复自定义参数保存问题) const saveConfig = (config) => { const savedConfig = GM_getValue(CONFIG_STORAGE_KEY, {}); const domainId = getDomainId(); // 创建当前域名的配置副本 const currentConfig = { ...config }; // 检查是否所有值都是默认值 const isDefault = Object.keys(DEFAULT_CONFIG).every(key => currentConfig[key] === DEFAULT_CONFIG[key] ); if (isDefault) { // 删除默认配置 if (savedConfig[domainId]) { delete savedConfig[domainId]; GM_setValue(CONFIG_STORAGE_KEY, savedConfig); } return; } // 只存储与默认值不同的配置项 const diffConfig = {}; Object.keys(currentConfig).forEach(key => { if (currentConfig[key] !== DEFAULT_CONFIG[key]) { diffConfig[key] = currentConfig[key]; } }); savedConfig[domainId] = diffConfig; GM_setValue(CONFIG_STORAGE_KEY, savedConfig); }; // 初始化配置 const CONFIG = (() => { const config = getConfig(); saveConfig(config); return config; })(); registerMenuCommands(); let cachedVideo = null; let lastVideoCheck = 0; let videoElements = []; let currentVideoIndex = 0; let activeVideoId = null; const videoStateMap = new WeakMap(); function getVideoState(video) { if (!videoStateMap.has(video)) { videoStateMap.set(video, { lastCustomRate: 1.0, isDefaultRate: true }); } return videoStateMap.get(video); } // 生成视频唯一ID const generateVideoId = (video) => `${video.src}_${video.clientWidth}x${video.clientHeight}`; function getVideoElement() { // 优先使用当前激活的视频 if (activeVideoId) { const activeVideo = videoElements.find(v => generateVideoId(v) === activeVideoId); if (activeVideo && document.contains(activeVideo)) { cachedVideo = activeVideo; return cachedVideo; } } // 常规检测逻辑 if (cachedVideo && document.contains(cachedVideo) && (Date.now() - lastVideoCheck < 300)) { return cachedVideo; } const handler = PLATFORM_HANDLERS[PLATFORM] || PLATFORM_HANDLERS.GENERIC; cachedVideo = handler.getVideo(); lastVideoCheck = Date.now(); // 更新视频元素列表和当前索引 updateVideoElements(); if (cachedVideo && videoElements.length > 0) { currentVideoIndex = videoElements.indexOf(cachedVideo); if (currentVideoIndex === -1) currentVideoIndex = 0; activeVideoId = generateVideoId(cachedVideo); } return cachedVideo; } function updateVideoElements() { videoElements = Array.from(document.querySelectorAll('video')) .filter(v => v.offsetParent !== null && v.readyState > 0); } function switchToNextVideo() { if (videoElements.length < 2) return null; currentVideoIndex = (currentVideoIndex + 1) % videoElements.length; cachedVideo = videoElements[currentVideoIndex]; activeVideoId = generateVideoId(cachedVideo); lastVideoCheck = Date.now(); return cachedVideo; } function switchToPrevVideo() { if (videoElements.length < 2) return null; currentVideoIndex = (currentVideoIndex - 1 + videoElements.length) % videoElements.length; cachedVideo = videoElements[currentVideoIndex]; activeVideoId = generateVideoId(cachedVideo); lastVideoCheck = Date.now(); return cachedVideo; } // 修正通用音量調整函數 function commonAdjustVolume(video, delta) { const isFineAdjust = Math.abs(delta) === CONFIG.fineVolumeStep; const actualDelta = isFineAdjust ? delta : (delta > 0 ? CONFIG.stepVolume : -CONFIG.stepVolume); // 修正符號 const newVolume = clampVolume((video.volume * 100) + actualDelta); video.volume = newVolume / 100; showVolume(newVolume); return newVolume; } function clampVolume(vol) { return Math.round(Math.max(0, Math.min(100, vol)) * 100) / 100; } const PLATFORM_HANDLERS = { YOUTUBE: { getVideo: () => document.querySelector('video, ytd-player video') || findVideoInIframes(), adjustVolume: (video, delta) => { const ytPlayer = document.querySelector('#movie_player'); if (ytPlayer?.getVolume) { const currentVol = ytPlayer.getVolume(); const newVol = clampVolume(currentVol + delta); // 直接應用delta ytPlayer.setVolume(newVol); video.volume = newVol / 100; showVolume(newVol); } else { commonAdjustVolume(video, delta); // 後備通用邏輯 } }, toggleFullscreen: () => document.querySelector('.ytp-fullscreen-button')?.click(), specialKeys: { 'Space': () => {}, 'Numpad7': () => document.querySelector('.ytp-prev-button')?.click(), 'Numpad9': () => document.querySelector('.ytp-next-button')?.click() } }, BILIBILI: { getVideo: () => document.querySelector('.bpx-player-video-wrap video') || findVideoInIframes(), adjustVolume: commonAdjustVolume, toggleFullscreen: () => document.querySelector('.bpx-player-ctrl-full')?.click(), specialKeys: { 'Space': () => {}, 'Numpad2': () => {}, 'Numpad8': () => {}, 'Numpad4': () => {}, 'Numpad6': () => {}, 'Numpad7': () => document.querySelector('.bpx-player-ctrl-prev')?.click(), 'Numpad9': () => document.querySelector('.bpx-player-ctrl-next')?.click() } }, TWITCH: { getVideo: () => document.querySelector('.video-ref video') || findVideoInIframes(), adjustVolume: commonAdjustVolume, toggleFullscreen: () => document.querySelector('[data-a-target="player-fullscreen-button"]')?.click(), specialKeys: { 'Numpad7': () => simulateKeyPress('ArrowLeft'), 'Numpad9': () => simulateKeyPress('ArrowRight') } }, STEAM: { getVideo: () => { const videos = Array.from(document.querySelectorAll('video')); const playingVideo = videos.find(v => v.offsetParent !== null && !v.paused); if (playingVideo) return playingVideo; const visibleVideo = videos.find(v => v.offsetParent !== null); if (visibleVideo) return visibleVideo; return findVideoInIframes(); }, adjustVolume: commonAdjustVolume, toggleFullscreen: (video) => { if (!video) return; const container = video.closest('.game_hover_activated') || video.parentElement; if (container && !document.fullscreenElement) { container.requestFullscreen?.().catch(() => video.requestFullscreen?.()); } else { document.exitFullscreen?.(); } }, handleWheel: function(e) { if (isInputElement(e.target)) return; const video = this.getVideo(); if (!video) return; const rect = video.getBoundingClientRect(); const inVideoArea = e.clientX >= rect.left - 50 && e.clientX <= rect.right + 50 && e.clientY >= rect.top - 30 && e.clientY <= rect.bottom + 30; if (inVideoArea) { e.preventDefault(); e.stopPropagation(); const delta = -Math.sign(e.deltaY); this.adjustVolume(video, delta * CONFIG.stepVolume); showVolume(video.volume * 100); } } }, GENERIC: { getVideo: () => { return document.querySelector('video.player') || findVideoInIframes() || document.querySelector('video, .video-player video, .video-js video, .player-container video'); }, adjustVolume: commonAdjustVolume, toggleFullscreen: (video) => toggleNativeFullscreen(video), } }; function findVideoInIframes() { const iframes = document.querySelectorAll('iframe'); for (const iframe of iframes) { try { const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document; return iframeDoc?.querySelector('video'); } catch {} } return null; } function toggleNativeFullscreen(video) { if (!video) return; try { if (document.fullscreenElement) { document.exitFullscreen(); } else { let elementToFullscreen = video; for (let i = 0; i < 2; i++) { elementToFullscreen = elementToFullscreen.parentElement || elementToFullscreen; } elementToFullscreen.requestFullscreen?.() || elementToFullscreen.webkitRequestFullscreen?.() || elementToFullscreen.msRequestFullscreen?.() || video.requestFullscreen?.() || video.webkitRequestFullscreen?.() || video.msRequestFullscreen?.(); } } catch (e) { console.error('Fullscreen error:', e); } } function simulateKeyPress(key) { document.dispatchEvent(new KeyboardEvent('keydown', {key, bubbles: true})); } function isInputElement(target) { return /^(INPUT|TEXTAREA|SELECT)$/.test(target.tagName) || target.isContentEditable; } function adjustRate(video, changeValue) { const state = getVideoState(video); const newRate = Math.max(0.1, Math.min(16, video.playbackRate + changeValue)); video.playbackRate = parseFloat(newRate.toFixed(1)); state.lastCustomRate = video.playbackRate; state.isDefaultRate = (video.playbackRate === 1.0); showVolume(video.playbackRate * 100); } function togglePlaybackRate(video) { const state = getVideoState(video); if (state.isDefaultRate) { video.playbackRate = state.lastCustomRate; state.isDefaultRate = false; } else { state.lastCustomRate = video.playbackRate; video.playbackRate = 1.0; state.isDefaultRate = true; } showVolume(video.playbackRate * 100); } function showVolume(vol) { const display = document.getElementById('dynamic-volume-display') || createVolumeDisplay(); display.textContent = `${Math.round(vol)}%`; display.style.opacity = '1'; setTimeout(() => display.style.opacity = '0', 1000); } function createVolumeDisplay() { const display = document.createElement('div'); display.id = 'dynamic-volume-display'; Object.assign(display.style, { position: 'fixed', zIndex: 2147483647, top: '50%', left: '50%', transform: 'translate(-50%, -50%)', padding: '10px 20px', borderRadius: '8px', backgroundColor: 'rgba(0, 0, 0, 0.7)', color: '#fff', fontSize: '24px', fontFamily: 'Arial, sans-serif', opacity: '0', transition: 'opacity 1s', pointerEvents: 'none' }); document.body.appendChild(display); return display; } function handleVideoWheel(e) { e.preventDefault(); e.stopPropagation(); const video = e.target; const normalizedDelta = -Math.sign(e.deltaY); // 滾輪向上=+1,向下=-1 PLATFORM_HANDLERS[PLATFORM].adjustVolume(video, normalizedDelta * CONFIG.stepVolume); } function handleTwitchWheel(e) { if (isInputElement(e.target)) return; const video = getVideoElement(); if (!video) return; const rect = video.getBoundingClientRect(); const inVideoArea = e.clientX >= rect.left - 50 && e.clientX <= rect.right + 50 && e.clientY >= rect.top - 30 && e.clientY <= rect.bottom + 30; if (inVideoArea) { e.preventDefault(); e.stopPropagation(); const delta = -Math.sign(e.deltaY); const volumeChange = delta * CONFIG.stepVolume; PLATFORM_HANDLERS.TWITCH.adjustVolume(video, volumeChange); showVolume(video.volume * 100); } } function handleKeyEvent(e) { if (isInputElement(e.target)) return; const video = getVideoElement(); const handler = PLATFORM_HANDLERS[PLATFORM]; // 強化修飾鍵檢測邏輯 const isCustomModifier = (() => { if (CONFIG.modifierKey === 5) return false; const requiredModifier = { 1: 'altKey', 2: 'ctrlKey', 3: 'shiftKey', 4: 'metaKey' }[CONFIG.modifierKey]; // 嚴格檢測:僅允許單一修飾鍵且無其他按鍵組合 const otherModifiers = ['altKey','ctrlKey','shiftKey','metaKey'] .filter(k => k !== requiredModifier) .some(k => e[k]); return e[requiredModifier] && !otherModifiers; })(); // ==== 修正點2:非自定義修飾鍵穿透處理 ==== const hasOtherModifiers = e.altKey || e.ctrlKey || e.shiftKey || e.metaKey; if (!isCustomModifier && hasOtherModifiers) { return; // 允許瀏覽器處理其他修飾鍵組合 } // ==== 修正點3:微調音量步進值應用 ==== if (isCustomModifier) { const volumeActions = { 'Numpad8': () => handler.adjustVolume(video, CONFIG.fineVolumeStep), 'Numpad2': () => handler.adjustVolume(video, -CONFIG.fineVolumeStep) }; if (volumeActions[e.code]) { volumeActions[e.code](); e.preventDefault(); return; } return; } // 处理按键7 if (e.code === 'Numpad7') { switch (CONFIG.key7Function) { case 1: // 长步进 video && (video.currentTime -= CONFIG.stepTimeLong); break; case 2: // 浏览器返回 history.back(); break; case 3: // 上一个影片 switchToPrevVideo()?.play().catch(() => {}); break; case 4: // 平台原生功能 if (handler.specialKeys?.Numpad7) { handler.specialKeys.Numpad7(); } break; } e.preventDefault(); return; } // 处理按键9 if (e.code === 'Numpad9') { switch (CONFIG.key9Function) { case 1: // 长步进 video && (video.currentTime += CONFIG.stepTimeLong); break; case 2: // 浏览器前进 history.forward(); break; case 3: // 下一个影片 switchToNextVideo()?.play().catch(() => {}); break; case 4: // 平台原生功能 if (handler.specialKeys?.Numpad9) { handler.specialKeys.Numpad9(); } break; } e.preventDefault(); return; } // 处理平台特殊按键 if (handler.specialKeys?.[e.code]) { handler.specialKeys[e.code](); e.preventDefault(); return; } // 处理其他通用按键 const actions = { 'Space': () => video && video[video.paused ? 'play' : 'pause'](), 'Numpad5': () => video && video[video.paused ? 'play' : 'pause'](), 'NumpadEnter': () => handler.toggleFullscreen(video), 'NumpadAdd': () => video && (video.currentTime += video.duration * 0.1), 'NumpadSubtract': () => video && (video.currentTime -= video.duration * 0.1), 'Numpad0': () => video && togglePlaybackRate(video), 'Numpad1': () => video && adjustRate(video, -0.1), 'Numpad3': () => video && adjustRate(video, 0.1), 'Numpad8': () => video && handler.adjustVolume(video, CONFIG.stepVolume), 'Numpad2': () => video && handler.adjustVolume(video, -CONFIG.stepVolume), 'Numpad4': () => video && (video.currentTime -= CONFIG.stepTime), 'Numpad6': () => video && (video.currentTime += CONFIG.stepTime) }; if (actions[e.code]) { actions[e.code](); e.preventDefault(); } } function bindVideoEvents() { if (PLATFORM === 'TWITCH' || PLATFORM === 'STEAM') return; document.querySelectorAll('video').forEach(video => { if (!video.dataset.volumeBound) { video.addEventListener('wheel', handleVideoWheel, { passive: false }); video.dataset.volumeBound = 'true'; } }); } function init() { bindVideoEvents(); document.addEventListener('keydown', handleKeyEvent, true); if (PLATFORM === 'STEAM') { document.addEventListener('wheel', PLATFORM_HANDLERS.STEAM.handleWheel.bind(PLATFORM_HANDLERS.STEAM), { capture: true, passive: false } ); } if (PLATFORM === 'TWITCH') { document.addEventListener('wheel', handleTwitchWheel, { capture: true, passive: false }); } // 初始化视频元素列表 updateVideoElements(); // 监听DOM变化更新视频列表 new MutationObserver(() => { bindVideoEvents(); updateVideoElements(); if (activeVideoId && !videoElements.some(v => generateVideoId(v) === activeVideoId)) { activeVideoId = null; } }).observe(document.body, { childList: true, subtree: true }); } if (document.readyState !== 'loading') init(); else document.addEventListener('DOMContentLoaded', init); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址