Rumble Volume Control with Mouse Scroll Wheel + Overlay

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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);
    }
})();