滚动音量Dx版

滚轮、013速度28音量5(空白键)播放暂停、enter全萤幕切换、小键盘+-增减10%进度。完整支援:YouTube、B站、Steam。B站直播(局部)

安装此脚本?
作者推荐脚本

你可能也喜欢 YouTube聊天室增強

安装此脚本
// ==UserScript==
// @name         滾動音量Dx版 Scroll Volume Dx Edition
// @name:zh-CN   滚动音量Dx版
// @name:en      Scroll Volume Dx Edition
// @namespace    http://tampermonkey.net/
// @version      7.9
// @description  滾輪、013速度28音量5(空白鍵)播放暫停、enter全螢幕切換、小鍵盤+-增減10%進度。完整支援:YouTube、B站、Steam。B站直播(局部)
// @description:zh-CN 滚轮、013速度28音量5(空白键)播放暂停、enter全萤幕切换、小键盘+-增减10%进度。完整支援:YouTube、B站、Steam。B站直播(局部)
// @description:en  wheel scroll for volume. NumpadKey:013 for speed, 28 for volume, 5(space) for play/pause, enter for fullscreen. Fully supports: YouTube, Bilibili, Steam. Bilibili live (partial)
// @match        *://*/*
// @match        *://www.youtube.com/*
// @match        *://www.bilibili.com/*
// @match        *://live.bilibili.com/*
// @match        *://www.twitch.tv/*
// @match        *://store.steampowered.com/*
// @exclude      *://www.facebook.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const VOLUME_STORAGE_PREFIX = 'volume_';

    const PLATFORMS = {
        YOUTUBE: isYouTubeMain(),
        BILIBILI: isBilibili(),
        TWITCH: isTwitch(),
        STEAM: isSteam(),
        FULLSCREEN_API_SUPPORT: !!document.fullscreenEnabled
    };

    function isBilibili() {
        return /bilibili\.com/.test(location.hostname);
    }
    function isYouTubeMain() {
        return /youtube\.com|youtu\.be/.test(location.hostname);
    }
    function isTwitch() {
        return /twitch\.tv/.test(location.hostname);
    }
    function isSteam() {
        return /steam(community|powered)\.com/.test(location.hostname);
    }

    function getYTPlayer() {
        return document.querySelector('ytd-player')?.getPlayer?.();
    }

    function getVideoElement() {
        const handler = getCurrentPlatformHandler();
        return handler.getVideo();
    }

    function getDomainKey() {
        return VOLUME_STORAGE_PREFIX + window.location.hostname;
    }

    const adjustVolumeHandler = (video, delta) => {
        const volumeValue = PLATFORMS.YOUTUBE ? getYTPlayer()?.getVolume() : video?.volume * 100;
        if (volumeValue === undefined) return;

        const newVolume = Math.max(0, Math.min(100, volumeValue + (delta > 0 ? -10 : 10)));
        PLATFORMS.YOUTUBE ? getYTPlayer()?.setVolume(newVolume) : (video.volume = newVolume / 100);
        showVolume(newVolume);

        GM_setValue(getDomainKey(), newVolume);
        return newVolume;
    };

    function initVolumeMemory(video, retryCount = 0) {
        if (!video || retryCount > 3) return;

        const savedVolume = GM_getValue(getDomainKey());
        if (savedVolume === undefined) return;

        const applyVolume = () => {
            try {
                if (PLATFORMS.YOUTUBE) {
                    const player = getYTPlayer();
                    if (player?.setVolume) {
                        player.setVolume(savedVolume);
                        player.unMute();
                    } else {
                        video.volume = savedVolume / 100;
                    }
                } else {
                    video.volume = savedVolume / 100;
                    video.muted = false;
                }
            } catch (e) {
                console.warn('Volume restore failed:', e);
            }
        };

        if (video.readyState < 2) {
            setTimeout(() => initVolumeMemory(video, retryCount + 1), 500);
        } else {
            applyVolume();
        }
    }

    const PLATFORM_HANDLERS = {
        YOUTUBE: {
            getVideo: () => {
                const iframeVideo = findVideoInIframes();
                return iframeVideo || document.querySelector('window.ytplayer,html5-video-player.ytp-player,ytd-player video');
            },
            adjustVolume: adjustVolumeHandler,
            toggleFullscreen: () => {
                const video = getVideoElement();
                if (document.querySelector('.ytp-fullscreen-button')) {
                    simulateKeyPress('f', 70);
                } else {
                    toggleNativeFullscreen(video);
                }
            },
            specialKeys: {
                '7': () => {
                    const player = getYTPlayer();
                    if (player?.getPlayerState() === 1) {
                        document.querySelector('.ytp-prev-button')?.click();
                    }
                },
                '9': () => {
                    const player = getYTPlayer();
                    if (player?.getPlayerState() === 1) {
                        document.querySelector('.ytp-next-button')?.click();
                    }
                },
                'NumpadEnter': () => simulateKeyPress('f', 70)
            }
        },
        BILIBILI: {
            getVideo: () => {
                const iframeVideo = findVideoInIframes();
                return iframeVideo || document.querySelector('.bpx-player-video-wrap video');
            },
            adjustVolume: adjustVolumeHandler,
            toggleFullscreen: (video) => {
                const fullscreenBtn = document.querySelector('.bpx-player-ctrl-full');
                if (fullscreenBtn) {
                    fullscreenBtn.click();
                } else {
                    toggleNativeFullscreen(video);
                }
            },
            specialKeys: {
                '2': () => {},
                '8': () => {},
                '7': () => {
                    const prevBtn = document.querySelector('.bpx-player-ctrl-eplist-prev');
                    if (prevBtn) prevBtn.click();
                },
                '9': () => {
                    const nextBtn = document.querySelector('.bpx-player-ctrl-eplist-next');
                    if (nextBtn) nextBtn.click();
                }
            }
        },
        TWITCH: {
            getVideo: () => {
                const iframeVideo = findVideoInIframes();
                return iframeVideo || document.querySelector('.video-ref video');
            },
            adjustVolume: adjustVolumeHandler,
            toggleFullscreen: (video) => {
                const fullscreenBtn = document.querySelector('[data-a-target="player-fullscreen-button"]');
                if (fullscreenBtn) {
                    fullscreenBtn.click();
                } else {
                    toggleNativeFullscreen(video);
                }
            },
            specialKeys: {
                '7': () => simulateKeyPress('ArrowLeft', 37),
                '9': () => simulateKeyPress('ArrowRight', 39)
            }
        },
        STEAM: {
            getVideo: () => {
                const activeVideo = Array.from(document.querySelectorAll('video'))
                    .find(v => v.offsetParent !== null);
                return activeVideo || findVideoInIframes();
            },
            adjustVolume: adjustVolumeHandler,
            toggleFullscreen: (video) => {
                if (!video) return;
                const container = video.closest('.game_hover_activated') || video.parentElement;
                if (container && !document.fullscreenElement) {
                    container.requestFullscreen?.().catch(() => {
                        video.requestFullscreen?.();
                    });
                } else {
                    document.exitFullscreen?.();
                }
            },
            specialKeys: {
                '7': () => {
                    const video = getVideoElement();
                    if (video) video.currentTime -= 30;
                },
                '9': () => {
                    const video = getVideoElement();
                    if (video) video.currentTime += 30;
                }
            }
        },
        GENERIC: {
            getVideo: () => {
                const iframeVideo = findVideoInIframes();
                if (iframeVideo) return iframeVideo;

                const selectors = [
                    'window.ytplayer',
                    'html5-video-player.ytp-player',
                    'div.html5-video-player',
                    'video',
                    '.video-player video',
                    '.video-js video',
                    '.html5-video-container video',
                    '.player-container video',
                    '.media-container video',
                    'div.ytp-cued-thumbnail-overlay',
                    '.video-container video'
                ];
                for (const sel of selectors) {
                    const el = document.querySelector(sel);
                    if (el?.tagName === 'VIDEO') return el;
                }
                return null;
            },
            adjustVolume: adjustVolumeHandler,
            toggleFullscreen: (video) => toggleNativeFullscreen(video),
            specialKeys: {
                '7': () => {
                    const video = getVideoElement();
                    if (video) video.currentTime -= 10;
                },
                '9': () => {
                    const video = getVideoElement();
                    if (video) video.currentTime += 10;
                },
                'NumpadEnter': (video) => toggleNativeFullscreen(video)
            }
        }
    };

    function findVideoInIframes() {
        const iframes = document.querySelectorAll('iframe');
        for (const iframe of iframes) {
            try {
                const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
                const video = iframeDoc?.querySelector('video');
                if (video) return video;
            } catch (e) {
                console.log('Cannot access iframe content:', e);
            }
        }
        return null;
    }

    function setupMutationObserver() {
        const handleIframeLoad = (iframe) => {
            try {
                const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
                const iframeVideo = iframeDoc?.querySelector('video');
                if (iframeVideo) {
                    initVideoHandlers(iframeVideo);
                }
            } catch (e) {
                console.log('Cannot access iframe content:', e);
            }
        };

        const observer = new MutationObserver((mutations) => {
            const processedIframes = new Set();

            for (const mutation of mutations) {
                if (mutation.type !== 'childList') continue;

                for (const node of mutation.addedNodes) {
                    if (node.nodeName === 'VIDEO') {
                        initVideoHandlers(node);
                        continue;
                    }

                    if (node.querySelector) {
                        const video = node.querySelector('video');
                        if (video) {
                            initVideoHandlers(video);
                        }

                        const iframes = node.querySelectorAll('iframe:not([data-volume-observed])');
                        iframes.forEach(iframe => {
                            if (processedIframes.has(iframe)) return;

                            iframe.dataset.volumeObserved = 'true';
                            processedIframes.add(iframe);

                            if (iframe.contentDocument) {
                                handleIframeLoad(iframe);
                            }
                            iframe.addEventListener('load', () => handleIframeLoad(iframe));
                        });
                    }
                }
            }
        });

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

    function initVideoHandlers(video) {
        if (!video) return;
        initVolumeMemory(video);
        video.addEventListener('play', () => {
            state.hasPlayed = true;
        });
    }

    function toggleNativeFullscreen(video) {
        if (!video) return;
        try {
            if (document.fullscreenElement) {
                document.exitFullscreen();
            } else if (video.requestFullscreen) {
                video.requestFullscreen();
            } else if (video.webkitRequestFullscreen) {
                video.webkitRequestFullscreen();
            } else if (video.msRequestFullscreen) {
                video.msRequestFullscreen();
            }
        } catch (e) {
            console.error('Fullscreen error:', e);
        }
    }

    const state = {
        volumeAccumulator: 0,
        lastCustomRate: 1.0,
        hasPlayed: false,
        matchedContainer: null
    };

    function simulateKeyPress(key, keyCode) {
        document.dispatchEvent(new KeyboardEvent('keydown', {key, keyCode, bubbles: true}));
    }
    function isInputElement(target) {
        return /^(INPUT|TEXTAREA|SELECT)$/.test(target.tagName) ||
               target.isContentEditable;
    }

    function getCurrentPlatformHandler() {
        const platform = Object.keys(PLATFORMS).find(k => PLATFORMS[k]);
        return PLATFORM_HANDLERS[platform] || PLATFORM_HANDLERS.GENERIC;
    }

    function adjustRate(video, changeValue) {
        const newRate = Math.max(0.1, Math.min(16,
            video.playbackRate + changeValue
        ));
        video.playbackRate = parseFloat(newRate.toFixed(1));
        state.lastCustomRate = newRate;
        showVolume(newRate * 100);
    }

    function togglePlaybackRate(video) {
        const currentRate = video.playbackRate;
        const newRate = currentRate === 1 ? state.lastCustomRate : 1;

        video.playbackRate = newRate;
        if (newRate !== 1) {
            state.lastCustomRate = currentRate;
        }
        showVolume(newRate * 100);
    }

    function showVolume(vol) {
        const display = document.getElementById('dynamic-volume-display') ||
                       createVolumeDisplay();
        display.textContent = `${Math.round(vol)}%`;
        display.style.opacity = '1';
        setTimeout(() => {
            display.style.opacity = '0';
        }, 1000);
    }

    function createVolumeDisplay() {
        const display = document.createElement('div');
        display.id = 'dynamic-volume-display';
        Object.assign(display.style, {
            position: 'fixed',
            zIndex: 2147483647,
            top: '50%',
            left: '50%',
            transform: 'translate(-50%, -50%)',
            padding: '10px 20px',
            borderRadius: '8px',
            backgroundColor: 'rgba(0, 0, 0, 0.7)',
            color: '#fff',
            fontSize: '24px',
            fontFamily: 'Arial, sans-serif',
            opacity: '0',
            transition: 'opacity 1s',
            pointerEvents: 'none'
        });
        document.body.appendChild(display);
        return display;
    }

    function handleWheel(e) {
        const video = getVideoElement();
        if (!video || !isMouseOverVideo(e)) return;

        e.preventDefault();
        const handler = getCurrentPlatformHandler();
        handler.adjustVolume(video, e.deltaY);
    }

    function isMouseOverVideo(e) {
        const video = getVideoElement();
        if (!video) return false;

        const rect = video.getBoundingClientRect();
        return e.clientX >= rect.left &&
               e.clientX <= rect.right &&
               e.clientY >= rect.top &&
               e.clientY <= rect.bottom;
    }

    function handleKeyEvent(e) {
        const video = getVideoElement();
        if (!video || isInputElement(e.target)) return;

        const handler = getCurrentPlatformHandler();

        if (handler.specialKeys && handler.specialKeys[e.key]) {
            handler.specialKeys[e.key](video);
            e.preventDefault();
            return;
        }

        const actions = {
            'Space': () => video[video.paused ? 'play' : 'pause'](),
            'Numpad5': () => video[video.paused ? 'play' : 'pause'](),
            'NumpadEnter': () => {
                const handler = getCurrentPlatformHandler();
                handler.toggleFullscreen?.(video);
            },
            'NumpadAdd': () => { video.currentTime += video.duration * 0.1; },
            'NumpadSubtract': () => { video.currentTime -= video.duration * 0.1; },
            'Numpad0': () => togglePlaybackRate(video),
            'Numpad1': () => adjustRate(video, -0.1),
            'Numpad3': () => adjustRate(video, 0.1),
            'Numpad8': () => adjustVolumeHandler(video, -10),
            'Numpad2': () => adjustVolumeHandler(video, 10),
            'Numpad4': () => { video.currentTime -= 10; },
            'Numpad6': () => { video.currentTime += 10; }
        };

        const action = actions[e.code] || (['+','-'].includes(e.key) && actions[`Numpad${e.key}`]);
        if (action) {
            action();
            e.preventDefault();
        }
    }

    function init() {
        document.addEventListener('wheel', handleWheel, { passive: false });
        document.addEventListener('keydown', handleKeyEvent, true);

        const video = getVideoElement();
        if (video) {
            initVideoHandlers(video);
        }

        setupMutationObserver();

        document.querySelectorAll('iframe').forEach(iframe => {
            if (iframe.dataset.volumeObserved) return;

            iframe.dataset.volumeObserved = 'true';
            iframe.addEventListener('load', () => {
                try {
                    const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
                    const iframeVideo = iframeDoc.querySelector('video');
                    if (iframeVideo) {
                        initVideoHandlers(iframeVideo);
                    }
                } catch (e) {
                    console.log('Cannot access iframe content:', e);
                }
            });

            if (iframe.contentDocument) {
                const iframeVideo = iframe.contentDocument.querySelector('video');
                if (iframeVideo) {
                    initVideoHandlers(iframeVideo);
                }
            }
        });
    }

    if (document.readyState !== 'loading') {
        init();
    } else {
        document.addEventListener('DOMContentLoaded', init);
    }
})();