Rumble Volume Control with Mouse Scroll Wheel + Overlay

Change volume on Rumble.com by scrolling over the video and show volume overlay

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Rumble Volume Control with Mouse Scroll Wheel + Overlay
// @namespace    violentmonkey-userscripts
// @version      1.4
// @description  Change volume on Rumble.com by scrolling over the video and show volume overlay
// @match        https://rumble.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    const VOLUME_STEP = 0.05;                // 5% volume change per scroll
    const REVERSE_SCROLL_DIRECTION = true;   // true = normal PC scroll, false = natural/macOS
    const OVERLAY_TIMEOUT = 1500;            // ms before overlay fades out
    const VOLUME_KEY = 'rumble_volume_memory';

    let overlayElement = null;
    let overlayTimeoutId = null;

    function clamp(val, min, max) {
        return Math.min(Math.max(val, min), max);
    }

    function saveVolume(volume) {
        localStorage.setItem(VOLUME_KEY, volume.toString());
    }

    function loadVolume() {
        const v = parseFloat(localStorage.getItem(VOLUME_KEY));
        return isNaN(v) ? 0.5 : clamp(v, 0, 1);
    }

    function createOverlay() {
        if (overlayElement) return;

        overlayElement = document.createElement('div');
        overlayElement.style.position = 'fixed';
        overlayElement.style.top = '110px';
        overlayElement.style.left = '110px';
        overlayElement.style.padding = '5px 10px';
        overlayElement.style.background = 'rgba(0, 0, 0, 0.7)';
        overlayElement.style.color = '#fff';
        overlayElement.style.fontSize = '14px';
        overlayElement.style.borderRadius = '4px';
        overlayElement.style.zIndex = '9999';
        overlayElement.style.transition = 'opacity 0.4s ease';
        overlayElement.style.opacity = '0';
        overlayElement.style.pointerEvents = 'none';
        document.body.appendChild(overlayElement);
    }

    function showOverlay(text) {
        createOverlay();
        overlayElement.textContent = text;
        overlayElement.style.opacity = '1';

        if (overlayTimeoutId) clearTimeout(overlayTimeoutId);
        overlayTimeoutId = setTimeout(() => {
            overlayElement.style.opacity = '0';
        }, OVERLAY_TIMEOUT);
    }

    function onWheelVolumeAdjust(e) {
        if (!e.target || e.target.tagName !== 'VIDEO') return;
        e.preventDefault();

        const video = e.target;
        const delta = e.deltaY * (REVERSE_SCROLL_DIRECTION ? -1 : 1);

        let newVolume = video.volume;
        if (delta > 0) {
            newVolume -= VOLUME_STEP;
        } else if (delta < 0) {
            newVolume += VOLUME_STEP;
        }

        newVolume = clamp(newVolume, 0, 1);

        if (newVolume > 0 && video.muted) {
            video.muted = false;
        }

        video.volume = newVolume;
        video.defaultVolume = newVolume;
        video.dispatchEvent(new Event('volumechange', { bubbles: true }));

        saveVolume(newVolume);
        showOverlay(`Volume: ${Math.round(newVolume * 100)}%`);
    }

    function onMiddleClick(e) {
        if (!e.target || e.target.tagName !== 'VIDEO') return;
        if (e.button !== 1) return; // middle click only
        e.preventDefault();

        const video = e.target;
        video.muted = !video.muted;
        showOverlay(video.muted ? 'Muted' : `Unmuted (${Math.round(video.volume * 100)}%)`);
    }

    function addVolumeControl(video) {
        if (video.dataset.rumbleVolumeAttached) return;

        video.addEventListener('wheel', onWheelVolumeAdjust, { passive: false });
        video.addEventListener('mousedown', onMiddleClick, true);

        const savedVolume = loadVolume();
        video.volume = savedVolume;
        if (savedVolume === 0) {
            video.muted = true;
        }

        video.dataset.rumbleVolumeAttached = 'true';
    }

    function observeVideos() {
        document.querySelectorAll('video').forEach(addVolumeControl);

        const observer = new MutationObserver((mutations) => {
            for (const mutation of mutations) {
                for (const node of mutation.addedNodes) {
                    if (node.tagName === 'VIDEO') {
                        addVolumeControl(node);
                    } else if (node.querySelectorAll) {
                        node.querySelectorAll('video').forEach(addVolumeControl);
                    }
                }
            }
        });

        observer.observe(document.body, { childList: true, subtree: true });
    }

    if (document.readyState === 'complete' || document.readyState === 'interactive') {
        observeVideos();
    } else {
        window.addEventListener('DOMContentLoaded', observeVideos);
    }
})();