Odysee Volume Control with Mouse Scroll Wheel (Scroll + Overlay + Memory + Mute)

Scroll to adjust volume, middle-click to mute, with overlay + per-site memory on Odysee

// ==UserScript==
// @name         Odysee Volume Control with Mouse Scroll Wheel (Scroll + Overlay + Memory + Mute)
// @namespace    violentmonkey-userscripts
// @version      1.3
// @description  Scroll to adjust volume, middle-click to mute, with overlay + per-site memory on Odysee
// @match        https://odysee.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    const CONFIG = {
        VOLUME_STEP: 0.05,
        REVERSE_SCROLL: true,    // true for macOS natural or inverted scrolling, set it to false for Windows behavior
        DEFAULT_VOLUME: 0.5,
        OVERLAY_TIMEOUT: 1500,
        OVERLAY_POSITIONS: {
            'odysee.com': { top: '110px', left: '110px' },
            // 'odysee.com': { bottom: '10px', right: '10px' },
        }
    };

    const hostname = location.hostname;
    const volumeKey = `volumeMemory_${hostname}`;
    let overlay = null;
    let overlayTimeoutId = null;

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

    function createOverlay() {
        if (overlay) return;

        overlay = document.createElement('div');
        Object.assign(overlay.style, {
            position: 'fixed',
            padding: '5px 10px',
            background: 'rgba(0,0,0,0.7)',
            color: '#fff',
            fontSize: '14px',
            borderRadius: '4px',
            zIndex: '9999',
            transition: 'opacity 0.4s ease',
            opacity: '0',
            pointerEvents: 'none',
        });

        // Apply per-site overlay position or default to top-left
        const pos = CONFIG.OVERLAY_POSITIONS[hostname] || { top: '10px', left: '10px' };
        Object.assign(overlay.style, pos);

        document.body.appendChild(overlay);
    }

    function showOverlay(text) {
        createOverlay();
        overlay.textContent = text;
        overlay.style.opacity = '1';
        if (overlayTimeoutId) clearTimeout(overlayTimeoutId);
        overlayTimeoutId = setTimeout(() => overlay.style.opacity = '0', CONFIG.OVERLAY_TIMEOUT);
    }

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

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

    function handleWheel(e) {
        const video = e.currentTarget;
        const delta = e.deltaY * (CONFIG.REVERSE_SCROLL ? -1 : 1);

        e.preventDefault();

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

        newVolume = clamp(newVolume, 0, 1);

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

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

    function handleMiddleClick(e) {
        if (e.button !== 1) return;

        const video = e.currentTarget;
        e.preventDefault();

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

    function initVideo(video) {
        if (!video || video.dataset.volumeEnhancerAttached) return;

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

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

        video.dataset.volumeEnhancerAttached = 'true';
    }

    function tryAttachToExistingVideos() {
        const videos = document.querySelectorAll('video');
        videos.forEach(initVideo);
    }

    function observeForNewVideos() {
        const observer = new MutationObserver(() => {
            tryAttachToExistingVideos();
        });

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

    function waitForVideos() {
        // For sites that dynamically load video
        const interval = setInterval(() => {
            const video = document.querySelector('video');
            if (video) {
                clearInterval(interval);
                tryAttachToExistingVideos();
                observeForNewVideos();
            }
        }, 300);
    }

    // Start
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', waitForVideos);
    } else {
        waitForVideos();
    }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址