Greasy Fork

Greasy Fork is available in English.

滚动音量Dx版

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Scroll Volume Dx Edition
// @name:zh-TW   滾動音量Dx版
// @name:zh-CN   滚动音量Dx版
// @namespace    http://tampermonkey.net/
// @version      7.6
// @description  Fixed numpad enter fullscreen issue. For recognized players, wheel scroll for volume, 013 for speed, 28 for volume, 5(space) for play/pause, enter for fullscreen. Fully supports: YouTube, Bilibili, Steam. Bilibili live (partial:012358/no wheel/enter)
// @description:zh-TW 修復小鍵盤enter全螢幕問題。滾輪、013速度28音量5(空白鍵)播放暫停、enter全螢幕切換、小鍵盤+-增減10%進度。完整支援:YouTube、B站、Steam。B站直播(局部:012358/無滾輪enter)
// @description:zh-CN 修复小键盘enter全萤幕问题。滚轮、013速度28音量5(空白键)播放暂停、enter全萤幕切换、小键盘+-增减10%进度。完整支援:YouTube、B站、Steam。B站直播(局部:012358/无滚轮enter)
// @match        *://*/*
// @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?.();
    }

    let cachedVideoElement = null;
    function getVideoElement() {
        if (!cachedVideoElement) {
            const handler = getCurrentPlatformHandler();
            cachedVideoElement = handler.getVideo();
        }
        return cachedVideoElement;
    }

    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();
                // 統一使用YouTube原生控制
                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) // 將YT的numpadenter功能移到specialKeys
            }
        },
        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: {
                '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 iframeVideo = findVideoInIframes();
                return iframeVideo || document.querySelector('video');
            },
            adjustVolume: adjustVolumeHandler,
            toggleFullscreen: (video) => {
                document.fullscreenElement
                    ? document.exitFullscreen()
                    : video.requestFullscreen?.();
            },
            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) // 通用numpadenter功能
            }
        }
    };

    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) {
                    cachedVideoElement = 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') {
                        cachedVideoElement = node;
                        initVideoHandlers(node);
                        continue;
                    }

                    if (node.querySelector) {
                        const video = node.querySelector('video');
                        if (video) {
                            cachedVideoElement = 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); // 使用平台專用的adjustVolume處理
    }

    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': () => { // 保留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),
            '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) {
                        cachedVideoElement = iframeVideo;
                        initVideoHandlers(iframeVideo);
                    }
                } catch (e) {
                    console.log('Cannot access iframe content:', e);
                }
            });

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

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