Greasy Fork is available in English.
悬浮按钮可拖动,点击显示/隐藏音量面板;支持0-600%非线性调节
当前为
// ==UserScript== // @name 音量控制器 // @namespace http://tampermonkey.net/ // @version 5.0 // @description 悬浮按钮可拖动,点击显示/隐藏音量面板;支持0-600%非线性调节 // @author [email protected] // @match *://*/* // @run-at document-start // @grant GM_setValue // @grant GM_getValue // @license MIT // ==/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 = ` <div style="margin-bottom:5px; font-weight:bold;">全局音量 (0-600%,左半0-100%)</div> <input type="range" id="global-gain-slider" min="0" max="1" step="0.001" value="${initialSliderPos}" style="cursor:grab;"> <span id="global-gain-display">${Math.round(DEFAULT_VOL * 100)}%</span> `; panel.style.cssText = ` cursor:default; position: fixed; top: 60px; /* 默认在按钮下方,可通过拖动改变位置?如果需要可添加独立拖动 */ left: 10px; background: white; padding: 10px; border: 1px solid #ccc; border-radius: 5px; box-shadow: 0 2px 10px rgba(0,0,0,0.2); z-index: 99999; font-family: sans-serif; display: none; /* 默认隐藏 */ `; document.body.appendChild(panel); // 在 panel 创建并插入到 document.body 之后,添加点击事件 panel.addEventListener('click', (e) => { // 如果点击的是滑块(<input type="range">),不处理,让滑块正常工作 if (e.target.tagName === 'INPUT' && e.target.type === 'range') { return; // 注意:这里不需要阻止冒泡,因为滑块本身不触发隐藏即可 } // 否则隐藏面板 panel.style.display = 'none'; isPanelVisible = false; // 更新状态变量 e.stopPropagation(); // 阻止事件冒泡到按钮,避免按钮的切换逻辑干扰 }); const slider = document.getElementById('global-gain-slider'); const display = document.getElementById('global-gain-display'); slider.addEventListener('input', (e) => { const x = parseFloat(e.target.value); const gain = sliderToGain(x); display.textContent = Math.round(gain * 100) + '%'; setVolume(gain); }); slider.addEventListener('mousedown', (e) => { if (audioContext && audioContext.state === 'suspended') { audioContext.resume().catch(() => {}); } }); } // ---------- 切换面板显示/隐藏 ---------- function togglePanel() { if (!panel) return; isPanelVisible = !isPanelVisible; panel.style.display = isPanelVisible ? 'block' : 'none'; if (isPanelVisible) { positionPanelNearButton(); // 显示时定位 } } // ---------- 更新UI的显示状态(根据有无媒体)---------- function updateUIVisibility() { const hasMedia = document.querySelectorAll('audio, video').length > 0; if (hasMedia) { if (!btn) createButton(); if (!panel) createPanel(); // 按钮始终显示 btn.style.display = 'flex'; // 面板的显示由按钮控制,但若媒体消失,我们强制隐藏面板 if (!hasMedia) { panel.style.display = 'none'; isPanelVisible = false; } } else { if (btn) btn.style.display = 'none'; if (panel) { panel.style.display = 'none'; isPanelVisible = false; } } } // ---------- 监听媒体元素的增减 ---------- const observer = new MutationObserver(() => { document.querySelectorAll('audio, video').forEach(media => { if (!mediaMap.has(media)) connectMedia(media); }); updateUIVisibility(); }); // 初始化 if (document.body) { observer.observe(document.body, { childList: true, subtree: true }); document.querySelectorAll('audio, video').forEach(media => connectMedia(media)); updateUIVisibility(); } else { window.addEventListener('DOMContentLoaded', () => { observer.observe(document.body, { childList: true, subtree: true }); document.querySelectorAll('audio, video').forEach(media => connectMedia(media)); updateUIVisibility(); }); } // 定期重试连接失败的媒体 setInterval(() => { document.querySelectorAll('audio, video').forEach(media => { const info = mediaMap.get(media); if (!info || info.status === 'failed') connectMedia(media); }); updateUIVisibility(); }, CHECK_INTERVAL); // window.addEventListener('resize', () => { if (panel && panel.style.display === 'block') { positionPanelNearButton(); } }); // 清理 window.addEventListener('beforeunload', () => { observer.disconnect(); if (audioContext) audioContext.close(); }); })();