// ==UserScript== // @name B站直播间音量快捷控制 // @namespace http://tampermonkey.net/ // @version 1.3.0 // @description 快捷调整B站直播间音量,支持循环切换和弹出菜单两种模式,支持深色模式,设置即时生效 // @author Super-Dian // @match https://live.bilibili.com/* // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @run-at document-idle // @license MIT // ==/UserScript== (function() { 'use strict'; // ==================== 深色模式检测 ==================== function isDarkMode() { // 检查B站的深色模式设置 const html = document.documentElement; // B站深色模式通常通过 class="dark" 或 data-theme="dark" 标记 if (html.classList.contains('dark') || html.getAttribute('data-theme') === 'dark' || html.getAttribute('data-darkmode') === 'true') { return true; } // 检查系统深色模式偏好 if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { return true; } return false; } // 获取当前主题颜色 function getThemeColors() { const dark = isDarkMode(); return { isDark: dark, bgPrimary: dark ? '#1f1f1f' : '#ffffff', bgSecondary: dark ? '#2a2a2a' : '#f5f5f5', bgTertiary: dark ? '#353535' : '#e8e8e8', textPrimary: dark ? '#e0e0e0' : '#333333', textSecondary: dark ? '#a0a0a0' : '#666666', border: dark ? '#404040' : '#dddddd', accent: '#fb7299', // B站粉色保持不变 accentHover: dark ? '#fc9db8' : '#fb7299', overlay: dark ? 'rgba(0, 0, 0, 0.85)' : 'rgba(0, 0, 0, 0.9)' }; } // ==================== 配置 ==================== const DEFAULT_CONFIG = { // 预设音量值(UI显示的0-100) presetVolumes: [0, 10, 30, 50, 70, 100], // 切换模式: 'cycle' 循环切换, 'menu' 弹出菜单 switchMode: 'cycle', // 按钮位置 buttonPosition: 'left-bottom', // 按钮透明度 buttonOpacity: 0.7, // 当前音量索引(用于循环模式) currentIndex: 2 }; let config = loadConfig(); // ==================== 配置管理 ==================== function loadConfig() { const saved = GM_getValue('volume_config', null); if (saved) { return { ...DEFAULT_CONFIG, ...JSON.parse(saved) }; } return { ...DEFAULT_CONFIG }; } function saveConfig(newConfig) { config = newConfig; GM_setValue('volume_config', JSON.stringify(config)); applySettings(); } // 应用设置到UI function applySettings() { // 更新主按钮透明度 if (mainButton) { mainButton.style.opacity = config.buttonOpacity.toString(); } // 重建弹出菜单(如果已创建) if (popupMenu) { const wasVisible = !popupMenu.classList.contains('hide'); popupMenu.remove(); popupMenu = null; if (wasVisible || config.switchMode === 'menu') { createPopupMenu(); } } // 更新样式 injectStyles(); console.log('[音量控制] 设置已应用'); } // ==================== 音量控制 ==================== function getVideoElement() { return document.querySelector('video'); } function getCurrentVolume() { const video = getVideoElement(); if (!video) return null; return Math.round(video.volume * 100); // 转换为0-100 } function setVolume(percent) { const video = getVideoElement(); if (!video) { console.warn('[音量控制] 未找到视频元素'); return false; } // 限制范围 0-100 percent = Math.max(0, Math.min(100, percent)); video.volume = percent / 100; // 转换为0-1 video.muted = percent === 0; // 记忆音量,覆盖B站原有的音量恢复 GM_setValue('saved_volume', percent); console.log(`[音量控制] 音量已设置为: ${percent}%`); updateButtonDisplay(); return true; } function getMuted() { const video = getVideoElement(); return video ? video.muted : false; } function setMuted(muted) { const video = getVideoElement(); if (video) { video.muted = muted; } } // ==================== 循环切换 ==================== function cycleVolume() { const { presetVolumes } = config; if (presetVolumes.length === 0) return; // 获取当前音量在预设列表中的位置 const currentVol = getCurrentVolume(); let currentIndex = presetVolumes.indexOf(currentVol); // 如果当前音量不在预设列表中,从头开始 if (currentIndex === -1) { currentIndex = -1; } // 切换到下一个 const nextIndex = (currentIndex + 1) % presetVolumes.length; config.currentIndex = nextIndex; setVolume(presetVolumes[nextIndex]); } // ==================== UI 创建 ==================== let mainButton = null; let popupMenu = null; let settingsPanel = null; let settingsBackdrop = null; function createMainButton() { if (mainButton) return mainButton; mainButton = document.createElement('div'); mainButton.id = 'volume-quick-btn'; mainButton.innerHTML = ` ${getCurrentVolume() || 0} `; mainButton.title = '左键:切换音量\n右键:打开设置'; mainButton.style.opacity = config.buttonOpacity.toString(); // 左键点击 - 根据模式切换 mainButton.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); if (config.switchMode === 'cycle') { cycleVolume(); } else { togglePopupMenu(); } }); // 右键点击 - 打开设置 mainButton.addEventListener('contextmenu', (e) => { e.preventDefault(); e.stopPropagation(); toggleSettingsPanel(); }); // 点击页面空白处关闭弹出菜单和设置面板 document.addEventListener('click', (e) => { if (popupMenu && !popupMenu.classList.contains('hide') && !popupMenu.contains(e.target) && !mainButton.contains(e.target)) { hidePopupMenu(); } }); document.addEventListener('contextmenu', (e) => { if (settingsPanel && !settingsPanel.classList.contains('hide') && !settingsPanel.contains(e.target) && !mainButton.contains(e.target)) { hideSettingsPanel(); } }); document.body.appendChild(mainButton); return mainButton; } function updateButtonDisplay() { const display = document.getElementById('volume-display'); if (display) { display.textContent = getCurrentVolume() || 0; } } // ==================== 弹出菜单 ==================== function createPopupMenu() { if (popupMenu) return popupMenu; popupMenu = document.createElement('div'); popupMenu.id = 'volume-popup-menu'; popupMenu.className = 'hide'; const menuContent = document.createElement('div'); menuContent.className = 'volume-menu-content'; // 添加音量按钮 config.presetVolumes.forEach((vol, index) => { const btn = document.createElement('button'); btn.textContent = `${vol}%`; btn.className = 'volume-menu-item'; btn.style.animationDelay = `${index * 0.04}s`; btn.addEventListener('click', (e) => { e.stopPropagation(); setVolume(vol); hidePopupMenu(); }); menuContent.appendChild(btn); }); popupMenu.appendChild(menuContent); document.body.appendChild(popupMenu); return popupMenu; } function togglePopupMenu() { if (!popupMenu) createPopupMenu(); if (popupMenu.classList.contains('hide')) { // 隐藏设置面板 hideSettingsPanel(); popupMenu.classList.remove('hide'); popupMenu.classList.add('visible'); } else { hidePopupMenu(); } } function hidePopupMenu() { if (popupMenu && !popupMenu.classList.contains('hide')) { popupMenu.classList.remove('visible'); setTimeout(() => { if (!popupMenu.classList.contains('visible')) { popupMenu.classList.add('hide'); } }, 200); } } // ==================== 样式管理 ==================== let dynamicStyle = null; function injectStyles() { if (dynamicStyle) dynamicStyle.remove(); const colors = getThemeColors(); dynamicStyle = document.createElement('style'); dynamicStyle.id = 'volume-control-styles'; dynamicStyle.textContent = ` /* 设置面板遮罩层 */ #volume-settings-backdrop { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.45); z-index: 999998; opacity: 0; pointer-events: none; visibility: hidden; transition: opacity 0.25s ease, visibility 0s 0.25s; } #volume-settings-backdrop.visible { opacity: 1; pointer-events: auto; visibility: visible; transition: opacity 0.25s ease, visibility 0s 0s; } #volume-settings-backdrop.hide { display: none; } /* 设置面板 */ #volume-settings-panel { position: fixed; width: 350px; max-height: 80vh; background-color: ${colors.bgPrimary}; border-radius: 12px; box-shadow: 0 10px 40px rgba(0,0,0,0.3); z-index: 999999; overflow: hidden; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; border: 1px solid ${colors.border}; opacity: 0; pointer-events: none; visibility: hidden; transition: opacity 0.25s ease, visibility 0s 0.25s; } #volume-settings-panel.visible { opacity: 1; pointer-events: auto; visibility: visible; transition: opacity 0.25s ease, visibility 0s 0s; animation: settings-panel-open 0.3s ease; } #volume-settings-panel.hide { display: none; } @keyframes settings-panel-open { from { transform: scale(0.95) translateY(8px); } to { transform: scale(1) translateY(0); } } #volume-settings-panel .settings-header { display: flex; justify-content: space-between; align-items: center; padding: 15px 20px; background: linear-gradient(135deg, #fb7299, #fc9db8); color: #fff; cursor: move; user-select: none; } #volume-settings-panel .settings-header h3 { margin: 0; font-size: 16px; } #volume-settings-panel .settings-close { background: none; border: none; color: #fff; font-size: 24px; cursor: pointer; padding: 0; line-height: 1; } #volume-settings-panel .settings-close:hover { opacity: 0.8; } #volume-settings-panel .settings-body { padding: 20px; max-height: 60vh; overflow-y: auto; } #volume-settings-panel .settings-group { margin-bottom: 20px; } #volume-settings-panel .settings-group > label { display: block; font-weight: 600; margin-bottom: 10px; color: ${colors.textPrimary}; } #volume-settings-panel .radio-group { display: flex; gap: 8px; } #volume-settings-panel .radio-group label { flex: 1; cursor: pointer; color: ${colors.textPrimary}; } #volume-settings-panel .radio-group label input[type="radio"] { display: none; } #volume-settings-panel .radio-group label .mode-tag { display: flex; align-items: center; justify-content: center; gap: 6px; padding: 10px 12px; border-radius: 8px; border: 1px solid ${colors.border}; background: ${colors.bgSecondary}; color: ${colors.textSecondary}; font-size: 13px; font-weight: 500; transition: all 0.2s ease; user-select: none; } #volume-settings-panel .radio-group label .mode-tag:hover { border-color: ${colors.accent}; color: ${colors.textPrimary}; } #volume-settings-panel .radio-group label input:checked + .mode-tag { background: ${colors.accent}; color: #fff; border-color: ${colors.accent}; box-shadow: 0 2px 8px rgba(251,114,153,0.3); } #volume-settings-panel .preset-item { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; padding: 6px 8px; background: ${colors.bgSecondary}; border-radius: 6px; border: 1px solid transparent; transition: transform 0.22s ease, opacity 0.18s ease, margin 0.22s ease, padding 0.22s ease, border-color 0.15s; } #volume-settings-panel .preset-item:hover { border-color: ${colors.border}; } #volume-settings-panel .preset-item.drag-item-hidden { opacity: 0; margin-bottom: 0; padding-top: 0; padding-bottom: 0; border-color: transparent; pointer-events: none; max-height: 0; overflow: hidden; } #volume-settings-panel .preset-item.drag-gap { margin-top: 28px; } #volume-settings-panel .drag-spacer { height: 0; transition: margin 0.22s ease; } #volume-settings-panel .drag-spacer.drag-gap { margin-top: 28px; } #volume-settings-panel .preset-item .drag-handle { cursor: grab; color: ${colors.textSecondary}; flex-shrink: 0; display: flex; align-items: center; padding: 2px; border-radius: 3px; transition: color 0.15s; } #volume-settings-panel .preset-item .drag-handle:hover { color: ${colors.textPrimary}; } #volume-settings-panel .preset-item .drag-handle:active { cursor: grabbing; } #volume-settings-panel .preset-item input { flex: 1; min-width: 0; padding: 5px 8px; border: 1px solid ${colors.border}; border-radius: 4px; font-size: 13px; background: ${colors.bgPrimary}; color: ${colors.textPrimary}; } #volume-settings-panel .preset-item input:focus { outline: none; border-color: ${colors.accent}; } #volume-settings-panel .preset-item .remove-btn { flex-shrink: 0; width: 22px; height: 22px; background: none; border: 1px solid ${colors.border}; border-radius: 4px; cursor: pointer; color: ${colors.textSecondary}; font-size: 15px; line-height: 1; display: flex; align-items: center; justify-content: center; transition: all 0.15s; } #volume-settings-panel .preset-item .remove-btn:hover { color: #ff4757; border-color: #ff4757; background: rgba(255, 71, 87, 0.1); } /* 拖拽浮动克隆 */ .preset-drag-clone { position: fixed; pointer-events: none; z-index: 1000000; display: flex; align-items: center; gap: 8px; padding: 6px 8px; background: ${colors.bgSecondary}; border: 2px solid ${colors.accent}; border-radius: 6px; box-shadow: 0 8px 24px rgba(0,0,0,0.25); opacity: 0.95; transform: scale(1.04); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; transition: transform 0.1s ease; } .preset-drag-clone .drag-handle { color: ${colors.textSecondary}; display: flex; align-items: center; } .preset-drag-clone input { width: 64px; padding: 5px 8px; border: 1px solid ${colors.border}; border-radius: 4px; font-size: 13px; background: ${colors.bgPrimary}; color: ${colors.textPrimary}; text-align: center; } .preset-drag-clone .remove-btn { display: none; } #volume-settings-panel .settings-btn { padding: 8px 16px; border: 1px solid ${colors.border}; border-radius: 6px; cursor: pointer; font-size: 14px; background: ${colors.bgPrimary}; color: ${colors.textPrimary}; transition: all 0.2s; } #volume-settings-panel .settings-btn:hover { background: ${colors.bgSecondary}; } #volume-settings-panel .settings-btn.primary { background: ${colors.accent}; color: #fff; border: none; } #volume-settings-panel .settings-btn.primary:hover { background: ${colors.accentHover}; } #volume-settings-panel .settings-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px; padding-top: 15px; border-top: 1px solid ${colors.border}; } #volume-settings-panel input[type="range"] { width: 100%; margin-top: 5px; accent-color: ${colors.accent}; } #volume-settings-panel #opacity-value { font-size: 14px; color: ${colors.textSecondary}; } /* 弹出菜单 */ #volume-popup-menu { position: fixed; left: 20px; bottom: 80px; background: ${colors.overlay}; border-radius: 8px; padding: 8px; z-index: 99998; box-shadow: 0 4px 15px rgba(0,0,0,0.4); border: 1px solid ${colors.border}; opacity: 0; transform: scale(0.9) translateY(8px); pointer-events: none; visibility: hidden; transition: opacity 0.2s ease, transform 0.2s ease, visibility 0s 0.2s; } #volume-popup-menu.visible { opacity: 1; transform: scale(1) translateY(0); pointer-events: auto; visibility: visible; transition: opacity 0.2s ease, transform 0.2s ease, visibility 0s 0s; } #volume-popup-menu.hide { display: none; } #volume-popup-menu .volume-menu-content { display: flex; flex-direction: column; gap: 5px; } #volume-popup-menu .volume-menu-item { padding: 10px 20px; background: transparent; border: none; color: ${colors.textPrimary}; cursor: pointer; border-radius: 6px; font-size: 14px; text-align: center; transition: background 0.2s; } #volume-popup-menu .volume-menu-item:hover { background: ${colors.accent}; color: #fff; } #volume-popup-menu.visible .volume-menu-item { animation: volume-menu-item-in 0.18s ease backwards; } @keyframes volume-menu-item-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } } /* 主按钮 */ #volume-quick-btn { position: fixed; left: 20px; bottom: 20px; width: 50px; height: 50px; background-color: ${colors.isDark ? 'rgba(50, 50, 50, 0.9)' : 'rgba(0, 0, 0, 0.7)'}; border-radius: 50%; display: flex; flex-direction: column; align-items: center; justify-content: center; cursor: pointer; z-index: 99999; color: #fff; box-shadow: 0 2px 10px rgba(0,0,0,0.3); transition: all 0.2s ease; user-select: none; border: 2px solid ${colors.isDark ? 'rgba(255,255,255,0.1)' : 'rgba(255,255,255,0.2)'}; } #volume-quick-btn:hover { transform: scale(1.1); opacity: 1 !important; box-shadow: 0 4px 15px rgba(251, 114, 153, 0.5); } #volume-quick-btn #volume-display { font-size: 10px; font-weight: bold; margin-top: 2px; } `; document.head.appendChild(dynamicStyle); } // 监听主题变化 function watchThemeChanges() { // 监听 html class 变化 const observer = new MutationObserver(() => { injectStyles(); }); observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'data-theme', 'data-darkmode'] }); // 监听系统深色模式变化 if (window.matchMedia) { window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { injectStyles(); }); } } // ==================== 设置面板 ==================== function createSettingsPanel() { if (settingsPanel) return settingsPanel; // 遮罩层 settingsBackdrop = document.createElement('div'); settingsBackdrop.id = 'volume-settings-backdrop'; settingsBackdrop.className = 'hide'; settingsBackdrop.addEventListener('click', () => { hideSettingsPanel(); }); document.body.appendChild(settingsBackdrop); settingsPanel = document.createElement('div'); settingsPanel.id = 'volume-settings-panel'; settingsPanel.className = 'hide'; settingsPanel.innerHTML = `