滾動音量Dx版 Scroll Volume Dx Edition

滾輪、013速度、28音量、46+-5sec、5(空白鍵)播放暫停、enter全螢幕切換、小鍵盤+-增減10%進度。完整支援:YouTube、B站、Steam。B站直播(局部)

Instalar este script¿?
Script recomendado por el autor

Puede que también te guste YouTube聊天室增強 YouTube Chat Enhancement.

Instalar este script
// ==UserScript==
// @name         滾動音量Dx版 Scroll Volume Dx Edition
// @name:zh-CN   滚动音量Dx版
// @name:en      Scroll Volume Dx Edition
// @namespace    http://tampermonkey.net/
// @version      9.4
// @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/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const initStorage = () => {
        if (GM_getValue('storageInitialized') !== true) {
            GM_setValue('stepTime', 5);
            GM_setValue('stepTimeLong', 30);
            GM_setValue('stepVolume', 10);
            GM_setValue('storageInitialized', true);
        }
    };
    initStorage();

    const CONFIG = {
        stepTime: GM_getValue('stepTime', 5),
        stepTimeLong: GM_getValue('stepTimeLong', 30),
        stepVolume: GM_getValue('stepVolume', 10)
    };

    GM_registerMenuCommand("⚙️ 設定步進", () => {
        const newVal = prompt("設定快進/快退", CONFIG.stepTime);
        if (newVal && !isNaN(newVal)) {
            GM_setValue('stepTime', parseFloat(newVal));
            CONFIG.stepTime = parseFloat(newVal);
        }
    });

    GM_registerMenuCommand("⏱️ 設定長步進", () => {
        const newVal = prompt("設定長跳轉", CONFIG.stepTimeLong);
        if (newVal && !isNaN(newVal)) {
            GM_setValue('stepTimeLong', parseFloat(newVal));
            CONFIG.stepTimeLong = parseFloat(newVal);
        }
    });

    GM_registerMenuCommand("🔊 設定音量步進", () => {
        const newVal = prompt("設定音量幅度 (%)", CONFIG.stepVolume);
        if (newVal && !isNaN(newVal)) {
            GM_setValue('stepVolume', parseFloat(newVal));
            CONFIG.stepVolume = parseFloat(newVal);
        }
    });

    let cachedVideo = null;
    let lastVideoCheck = 0;

    const PLATFORM = (() => {
        const host = location.hostname;
        if (/youtube\.com|youtu\.be/.test(host)) return "YOUTUBE";
        if (/www.bilibili\.com/.test(host)) return "BILIBILI"; //live.bilibili對應GENERIC
        if (/twitch\.tv/.test(host)) return "TWITCH";
        if (/steam(community|powered)\.com/.test(host)) return "STEAM";
        return "GENERIC";
    })();

    const videoStateMap = new WeakMap();

    function getVideoState(video) {
        if (!videoStateMap.has(video)) {
            videoStateMap.set(video, {
                lastCustomRate: 1.0,
                isDefaultRate: true
            });
        }
        return videoStateMap.get(video);
    }

    function getVideoElement() {
        if (cachedVideo && document.contains(cachedVideo)) {
            return cachedVideo;
        }

        const handler = PLATFORM_HANDLERS[PLATFORM] || PLATFORM_HANDLERS.GENERIC;
        cachedVideo = handler.getVideo();
        lastVideoCheck = Date.now();
        return cachedVideo;
    }

    function commonAdjustVolume(video, delta) {
        if (delta < 0 && video.muted) {
            video.muted = false;
        }
        const newVolume = Math.max(0, Math.min(100,
             (video.volume * 100) +
             (delta > 0 ? -CONFIG.stepVolume : CONFIG.stepVolume)
        ));
        video.volume = newVolume / 100;
        showVolume(newVolume);
        return newVolume;
    }

    const PLATFORM_HANDLERS = {
        YOUTUBE: {
            getVideo: () => document.querySelector('video, ytd-player video') || findVideoInIframes(),
            adjustVolume: (video, delta) => {
                if (delta < 0 && video.muted) {
                    video.muted = false;
                    const muteButton = document.querySelector('.ytp-mute-button');
                    if (muteButton && muteButton.getAttribute('aria-label')?.includes('取消靜音')) {
                        muteButton.click();
                    }
                }
                const newVolume = Math.max(0, Math.min(100,
                    (video.volume * 100) +
                    (delta > 0 ? -CONFIG.stepVolume : CONFIG.stepVolume)
                ));
                video.volume = newVolume / 100;
                const ytPlayer = document.querySelector('#movie_player');
                if (ytPlayer && ytPlayer.setVolume) {
                    ytPlayer.setVolume(newVolume);
                }
                showVolume(newVolume);
                return newVolume;
            },
            toggleFullscreen: () => document.querySelector('.ytp-fullscreen-button')?.click(),
            specialKeys: {
                'Space': () => {}, //代表使用YT默認
                'Numpad7': () => document.querySelector('.ytp-prev-button')?.click(),
                'Numpad9': () => document.querySelector('.ytp-next-button')?.click()
            }
        },
        BILIBILI: {
            getVideo: () => document.querySelector('.bpx-player-video-wrap video') || findVideoInIframes(),
            adjustVolume: commonAdjustVolume,
            toggleFullscreen: () => document.querySelector('.bpx-player-ctrl-full')?.click(),
            specialKeys: {
                'Space': () => {},
                'Numpad2': () => {},
                'Numpad8': () => {},
                'Numpad4': () => {},
                'Numpad6': () => {}, //空值代表使用bilibili默認
                'Numpad7': () => document.querySelector('.bpx-player-ctrl-prev')?.click(),
                'Numpad9': () => document.querySelector('.bpx-player-ctrl-next')?.click()
            }
        },
        TWITCH: {
            getVideo: () => document.querySelector('.video-ref video') || findVideoInIframes(),
            adjustVolume: commonAdjustVolume,
            toggleFullscreen: () => document.querySelector('[data-a-target="player-fullscreen-button"]')?.click(),
            specialKeys: {
                'Numpad7': () => simulateKeyPress('ArrowLeft'),
                'Numpad9': () => simulateKeyPress('ArrowRight')
            }
        },
        STEAM: {
            getVideo: () => Array.from(document.querySelectorAll('video')).find(v => v.offsetParent !== null) || findVideoInIframes(),
            adjustVolume: commonAdjustVolume,
            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;
                return document.querySelector('video, .video-player video, .video-js video, .player-container video');
            },
            adjustVolume: commonAdjustVolume,
            toggleFullscreen: (video) => toggleNativeFullscreen(video),
        }
    };

    function findVideoInIframes() {
        const iframes = document.querySelectorAll('iframe');
        for (const iframe of iframes) {
            try {
                const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
                return iframeDoc?.querySelector('video');
            } catch {}
        }
        return null;
    }

    function toggleNativeFullscreen(video) {
        if (!video) return;
        try {
            if (document.fullscreenElement) {
                document.exitFullscreen();
            } else {
                let elementToFullscreen = video;
                for (let i = 0; i < 2; i++) {
                    if (elementToFullscreen.parentElement) {
                        elementToFullscreen = elementToFullscreen.parentElement;
                    } else {
                        break;
                    }
                }
                if (elementToFullscreen.requestFullscreen) {
                    elementToFullscreen.requestFullscreen();
                } else if (elementToFullscreen.webkitRequestFullscreen) {
                    elementToFullscreen.webkitRequestFullscreen();
                } else if (elementToFullscreen.msRequestFullscreen) {
                    elementToFullscreen.msRequestFullscreen();
                } 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);
        }
    }

    function simulateKeyPress(key) {
        document.dispatchEvent(new KeyboardEvent('keydown', {key, bubbles: true}));
    }

    function isInputElement(target) {
        return /^(INPUT|TEXTAREA|SELECT)$/.test(target.tagName) || target.isContentEditable;
    }

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

    function togglePlaybackRate(video) {
        const state = getVideoState(video);
        if (state.isDefaultRate) {
            video.playbackRate = state.lastCustomRate;
            state.isDefaultRate = false;
        } else {
            state.lastCustomRate = video.playbackRate;
            video.playbackRate = 1.0;
            state.isDefaultRate = true;
        }
        showVolume(video.playbackRate * 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 handleVideoWheel(e) {
        e.preventDefault();
        e.stopPropagation();
        e.stopImmediatePropagation();
        const video = e.target;
        PLATFORM_HANDLERS[PLATFORM].adjustVolume(video, e.deltaY);
    }

    function handleTwitchWheel(e) {
        if (isInputElement(e.target)) return;
        const video = getVideoElement();
        if (!video) return;
        const rect = video.getBoundingClientRect();
        const inVideoArea = e.clientX >= rect.left && e.clientX <= rect.right &&
                            e.clientY >= rect.top && e.clientY <= rect.bottom;
        if (inVideoArea) {
            e.preventDefault();
            e.stopPropagation();
            PLATFORM_HANDLERS.TWITCH.adjustVolume(video, e.deltaY);
        }
    }

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

        const handler = PLATFORM_HANDLERS[PLATFORM];
        if (handler.specialKeys?.[e.code]) {
            handler.specialKeys[e.code]();
            e.preventDefault();
            return;
        }

        const actions = {
            'Space': () => video[video.paused ? 'play' : 'pause'](),
            'Numpad5': () => video[video.paused ? 'play' : 'pause'](),
            'NumpadEnter': () => 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': () => handler.adjustVolume(video, -CONFIG.stepVolume),
            'Numpad2': () => handler.adjustVolume(video, CONFIG.stepVolume),
            'Numpad4': () => video.currentTime -= CONFIG.stepTime,
            'Numpad6': () => video.currentTime += CONFIG.stepTime,
            'Numpad7': () => video.currentTime -= CONFIG.stepTimeLong,
            'Numpad9': () => video.currentTime += CONFIG.stepTimeLong
        };

        if (actions[e.code]) {
            actions[e.code]();
            e.preventDefault();
        }
    }

    function bindVideoEvents() {
        if (PLATFORM === 'TWITCH') return;

        const videos = document.querySelectorAll('video');
        videos.forEach(video => {
            if (!video.dataset.volumeBound) {
                video.addEventListener('wheel', handleVideoWheel, { passive: false });
                video.dataset.volumeBound = 'true';
            }
        });
    }

    function init() {
        bindVideoEvents();
        document.addEventListener('keydown', handleKeyEvent, true);

        if (PLATFORM === 'TWITCH') {
            document.addEventListener('wheel', handleTwitchWheel, { capture: true, passive: false });
        }

        const observer = new MutationObserver(bindVideoEvents);
        observer.observe(document.body, { childList: true, subtree: true });
    }

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