Greasy Fork

Greasy Fork is available in English.

滚动音量Dx版

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

当前为 2025-05-28 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         滾動音量Dx版 Scroll Volume Dx Edition
// @name:zh-CN   滚动音量Dx版
// @name:en      Scroll Volume Dx Edition
// @namespace    http://tampermonkey.net/
// @version      7.9.3
// @description  滾輪、013速度、28音量、46+-5sec、5(空白鍵)播放暫停、enter全螢幕切換、小鍵盤+-增減10%進度。完整支援:YouTube、B站、Steam。B站直播(局部)
// @description:zh-CN 滚轮、013速度、28音量、46+-5sec、5(空白键)播放暂停、enter全萤幕切换、小键盘+-增减10%进度。完整支援:YouTube、B站、Steam。B站直播(局部)
// @description:en  wheel scroll for volume. NumpadKey:013 for speed, 28 for volume, 46 for 5sec、5(space) for play/pause, enter for fullscreen, numpad+- for 5sec. Fully supports: YouTube, Bilibili, Steam. Bilibili live (partial)
// @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 /www.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: {
                'Numpad7': () => {
                    const player = getYTPlayer();
                    if (player?.getPlayerState() === 1) {
                        document.querySelector('.ytp-prev-button')?.click();
                    }
                },
                'Numpad9': () => {
                    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();
                }
            },
            specialKeys: {
                '2': () => {},
                '8': () => {},
                '4': () => {},
                '6': () => {},
                'Numpad7': () => {
                    const prevBtn = document.querySelector('.bpx-player-ctrl-eplist-prev');
                    if (prevBtn) prevBtn.click();
                },
                'Numpad9': () => {
                    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: {
                'Numpad7': () => simulateKeyPress('ArrowLeft', 37),
                'Numpad9': () => 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?.();
                }
            }
        },
        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: {
                '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 -= 5; },
            'Numpad6': () => { video.currentTime += 5; },
            'Numpad7': () => { video.currentTime -= 30; },
            'Numpad9': () => { video.currentTime += 30;
                },
        };

        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);
    }
})();