// ==UserScript== // @name 音量控制器 // @namespace http://tampermonkey.net/ // @version 5.0 // @description 悬浮按钮可拖动,点击显示/隐藏音量面板;支持0-600%非线性调节 // @author ukatoilol@gmail.com // @match *://*/* // @run-at document-start // @grant GM_setValue // @grant GM_getValue // @license MIT // @downloadURL none // ==/UserScript== (function() { 'use strict'; // ===== 用户配置 ===== const MAX_GAIN = 6.0; // 最大增益倍数(600%) const DEFAULT_VOL = 1.0; // 默认音量(100%) const CHECK_INTERVAL = 3000; // 定期重试连接失败的媒体(毫秒) const BUTTON_ID = 'volume-controller-btn'; const PANEL_ID = 'volume-controller-panel'; // =================== let audioContext = null; const mediaMap = new WeakMap(); let btn = null; // 悬浮按钮元素 let panel = null; // 音量面板元素 let isPanelVisible = false; // 当前面板显示状态(仅记录,实际由按钮切换) // ---------- 转换函数:滑块位置 (0~1) <-> 增益值 (0~MAX_GAIN) ---------- function sliderToGain(x) { if (x <= 0.5) return x * 2; else return 1 + (x - 0.5) * (MAX_GAIN - 1) * 2; } function gainToSlider(gain) { if (gain <= 1) return gain / 2; else return 0.5 + (gain - 1) / ((MAX_GAIN - 1) * 2); } // ---------- 核心:连接单个媒体元素 ---------- function connectMedia(media) { if (mediaMap.has(media)) return; if (media.readyState < 1) { media.addEventListener('loadedmetadata', () => connectMedia(media), { once: true }); return; } try { if (!audioContext) { audioContext = new (window.AudioContext || window.webkitAudioContext)(); document.addEventListener('visibilitychange', () => { if (document.hidden && audioContext.state === 'running') audioContext.suspend(); else if (!document.hidden && audioContext.state === 'suspended') audioContext.resume().catch(() => {}); }); } const track = audioContext.createMediaElementSource(media); const gainNode = audioContext.createGain(); gainNode.gain.value = DEFAULT_VOL; track.connect(gainNode); gainNode.connect(audioContext.destination); mediaMap.set(media, { status: 'connected', gainNode }); console.log('✅ 音量放大器: 成功连接', media); if (audioContext.state === 'suspended') audioContext.resume().catch(() => {}); } catch (err) { console.warn('⚠️ 音量放大器: 连接失败,降级为volume控制', err); mediaMap.set(media, { status: 'failed' }); } } // ---------- 设置所有媒体的音量(接收增益值 gain)---------- function setVolume(gain) { const volForVolumeProp = Math.min(1, Math.max(0, gain)); document.querySelectorAll('audio, video').forEach(media => { const info = mediaMap.get(media); if (info && info.status === 'connected' && info.gainNode) { try { info.gainNode.gain.value = gain; } catch (e) { console.warn('增益节点失效,尝试重新连接', media); mediaMap.delete(media); connectMedia(media); } } media.volume = volForVolumeProp; if (!info) connectMedia(media); }); } // ---------- 创建悬浮按钮 ---------- function createButton() { if (btn) return; btn = document.createElement('div'); btn.id = BUTTON_ID; btn.innerHTML = '🔊'; // 音量图标 btn.style.cssText = ` position: fixed; width: 20px; height: 20px; background: #ffffff; color: blue; border-radius: 50%; box-shadow: 0 2px 10px rgba(0,0,0,0.3); display: flex; align-items: center; justify-content: center; font-size: 16px; cursor: pointer; z-index: 100000; user-select: none; transition: background 0.2s, transform 0.1s; `; btn.addEventListener('mouseenter', () => btn.style.background = '#ffffff'); btn.addEventListener('mouseleave', () => btn.style.background = '#ffffff'); // 加载保存的位置 const savedX = GM_getValue('btnX', 10); const savedY = GM_getValue('btnY', 10); btn.style.left = savedX + 'px'; btn.style.top = savedY + 'px'; document.body.appendChild(btn); makeDraggable(btn); } // ---------- 将面板定位在按钮附近 ---------- function positionPanelNearButton() { if (!btn || !panel) return; const btnRect = btn.getBoundingClientRect(); const panelWidth = panel.offsetWidth; const panelHeight = panel.offsetHeight; const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; // 默认位置:按钮正下方,左边缘与按钮对齐 let top = btnRect.bottom + 5; let left = btnRect.left; // 如果下方空间不足,放在按钮上方 if (top + panelHeight > viewportHeight) { top = btnRect.top - panelHeight - 5; } // 如果上方也不足,则强制放在底部(但这种情况极少) if (top < 0) { top = Math.max(5, viewportHeight - panelHeight - 5); } // 水平边界检查:防止右侧溢出 if (left + panelWidth > viewportWidth) { left = viewportWidth - panelWidth - 5; } // 防止左侧溢出 if (left < 0) { left = 5; } panel.style.top = top + 'px'; panel.style.left = left + 'px'; } // ---------- 使元素可拖动 ---------- function makeDraggable(el) { let offsetX, offsetY, isDragging = false, startX, startY; const dragThreshold = 5; // 拖动阈值(像素) el.addEventListener('mousedown', (e) => { e.preventDefault(); startX = e.clientX; startY = e.clientY; isDragging = false; // 尚未开始拖动 const rect = el.getBoundingClientRect(); offsetX = e.clientX - rect.left; offsetY = e.clientY - rect.top; const onMouseMove = (e) => { e.preventDefault(); const dx = e.clientX - startX; const dy = e.clientY - startY; if (!isDragging && (Math.abs(dx) > dragThreshold || Math.abs(dy) > dragThreshold)) { isDragging = true; } if (isDragging) { let newLeft = e.clientX - offsetX; let newTop = e.clientY - offsetY; newLeft = Math.max(0, Math.min(window.innerWidth - el.offsetWidth, newLeft)); newTop = Math.max(0, Math.min(window.innerHeight - el.offsetHeight, newTop)); el.style.left = newLeft + 'px'; el.style.top = newTop + 'px'; // 如果面板当前可见,同步移动面板 if (panel && panel.style.display === 'block') { positionPanelNearButton(); } } }; const onMouseUp = (e) => { document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); document.body.style.userSelect = ''; // 恢复文本选择 if (!isDragging) { // 没有拖动,视为点击 togglePanel(); } else { // 拖动结束,保存位置 GM_setValue('btnX', parseInt(el.style.left)); GM_setValue('btnY', parseInt(el.style.top)); } }; document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); document.body.style.userSelect = 'none'; // 防止拖动时选中文本 }); } // ---------- 创建音量面板 ---------- function createPanel() { if (panel) return; panel = document.createElement('div'); panel.id = PANEL_ID; const initialSliderPos = gainToSlider(DEFAULT_VOL); panel.innerHTML = `