Greasy Fork

Greasy Fork is available in English.

滚动音量Dx版

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

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

您需要先安装一款用户脚本管理器扩展,例如 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      9.0
// @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";
    })();

    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) {
        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: commonAdjustVolume,
            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);
        }
    }

    const state = {
        volumeAccumulator: 0,
        lastCustomRate: 1.0,
        isDefaultRate: true
    };

    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 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) {
        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 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 handleWheel(e) {
        if (!isMouseOverVideo(e)) return;
        e.preventDefault();
        const video = getVideoElement();
        if (!video) return;
        PLATFORM_HANDLERS[PLATFORM].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 init() {
        document.addEventListener('wheel', handleWheel, { passive: false });
        document.addEventListener('keydown', handleKeyEvent, true);
        if (Date.now() - lastVideoCheck > 5000) getVideoElement();
    }

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