Greasy Fork

来自缓存

Greasy Fork is available in English.

滚动音量Dx版

改善Web Fullscreen容器定位。新增YouTube全萤幕模式选项与Web全萤幕点击播放控制。新增自定义修饰键微调音量功能。滚轮、013速度、28音量、46+-5sec、5(空白键)播放暂停、enter全萤幕切换、小键盘+-增减10%进度。完整支援:YouTube、B站、Steam。B站直播(局部)

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴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.13
// @description  改善Web Fullscreen容器定位。新增YouTube全螢幕模式選項與Web全螢幕點擊播放控制。新增自訂修飾鍵微調音量功能。滾輪、013速度、28音量、46+-5sec、5(空白鍵)播放暫停、enter全螢幕切換、小鍵盤+-增減10%進度。完整支援:YouTube、B站、Steam。B站直播(局部)
// @description:zh-CN 改善Web Fullscreen容器定位。新增YouTube全萤幕模式选项与Web全萤幕点击播放控制。新增自定义修饰键微调音量功能。滚轮、013速度、28音量、46+-5sec、5(空白键)播放暂停、enter全萤幕切换、小键盘+-增减10%进度。完整支援:YouTube、B站、Steam。B站直播(局部)
// @description:en  Improved Web Fullscreen container positioning. Added YouTube fullscreen mode option and Web fullscreen click-to-play control. Added custom modifier key for fine volume adjustment. 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        *://*/*
// @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 LANG = /^zh-(cn|tw|hk|mo|sg)/i.test(navigator.language) ? 'zh' : 'en'; // 語言檢測 / Language detection
    const i18n = { // 國際化文本 / Internationalization text
        zh: {
            menuStep: '⚙️ 設定步進',
            menuLongStep: '⏱️ 設定長步進',
            menuVolumeStep: '🔊 設定音量步進',
            menuModifier: '🎚️ 設定修飾鍵微調',
            menuKeyFunc: '🎛️ 設定按鍵7/9功能',
            menuFullscreenMode: '📺 設定YouTube全螢幕模式',
            promptStep: '設定快進/快退 (秒)',
            promptLongStep: '設定長跳轉 (秒)',
            promptVolume: '設定音量幅度 (%)',
            modifierOptions: {
                1: '1. Alt 鍵',
                2: '2. Ctrl 鍵',
                3: '3. Shift 鍵',
                4: '4. Meta 鍵 (⌘)',
                5: '5. 關閉此功能'
            },
            keyFuncOptions: {
                1: '1. 長步進',
                2: '2. 上一頁/下一頁',
                3: '3. 上/下一個影片',
                4: '4. 平台原生功能'
            },
            fullscreenModeOptions: {
                1: '1. 原生最大化 (點擊 .ytp-fullscreen-button)',
                2: '2. 原生API最大化 (toggleNativeFullscreen)',
                3: '3. 網頁全螢幕 (Web Fullscreen)'
            },
            saveAlert: '設定已保存,需重新整理頁面後生效',
            promptModifier: '選擇音量微調修飾鍵:',
            promptKey7: '選擇按鍵7功能:',
            promptKey9: '選擇按鍵9功能:',
            promptFullscreen: '選擇YouTube全螢幕模式:'
        },
        en: {
            menuStep: '⚙️ Set Step',
            menuLongStep: '⏱️ Set Long Step',
            menuVolumeStep: '🔊 Set Volume Step',
            menuModifier: '🎚️ Set Modifier Key',
            menuKeyFunc: '🎛️ Set Key 7/9 Function',
            menuFullscreenMode: '📺 Set YouTube Fullscreen Mode',
            promptStep: 'Set step time (seconds)',
            promptLongStep: 'Set long jump time (seconds)',
            promptVolume: 'Set volume step (%)',
            modifierOptions: {
                1: '1. Alt key',
                2: '2. Ctrl key',
                3: '3. Shift key',
                4: '4. Meta key (⌘)',
                5: '5. Disable feature'
            },
            keyFuncOptions: {
                1: '1. Long step',
                2: '2. Browser navigation',
                3: '3. Previous/Next video',
                4: '4. Platform native'
            },
            fullscreenModeOptions: {
                1: '1. Native maximization (click .ytp-fullscreen-button)',
                2: '2. Native API maximization (toggleNativeFullscreen)',
                3: '3. Web Fullscreen'
            },
            saveAlert: 'Settings saved. Refresh page to apply',
            promptModifier: 'Select modifier key:',
            promptKey7: 'Select key 7 function:',
            promptKey9: 'Select key 9 function:',
            promptFullscreen: 'Select YouTube fullscreen mode:'
        }
    };
    const registerMenuCommands = () => { // 註冊設定選單 / Register settings menu
        const t = i18n[LANG];
        GM_registerMenuCommand(t.menuStep, () => handleConfigPrompt(t.promptStep, 'stepTime'));
        GM_registerMenuCommand(t.menuLongStep, () => handleConfigPrompt(t.promptLongStep, 'stepTimeLong'));
        GM_registerMenuCommand(t.menuVolumeStep, () => handleConfigPrompt(t.promptVolume, 'stepVolume'));
        GM_registerMenuCommand(t.menuModifier, handleModifierSetting);
        GM_registerMenuCommand(t.menuKeyFunc, handleKeyFunctionSetting);
        if (PLATFORM === 'YOUTUBE') GM_registerMenuCommand(t.menuFullscreenMode, handleFullscreenModeSetting);
    };
    const handleConfigPrompt = (promptText, configKey) => { // 處理數值設定提示 / Handle numeric setting prompt
        const newVal = prompt(promptText, CONFIG[configKey]);
        if (newVal && !isNaN(newVal)) {
            CONFIG[configKey] = parseFloat(newVal);
            saveConfig(CONFIG);
        }
    };
    const handleModifierSetting = () => { // 處理修飾鍵設定提示 / Handle modifier key setting prompt
        const t = i18n[LANG];
        const options = t.modifierOptions;
        const choice = prompt(`${t.promptModifier}${Object.values(options).join('')}`,
            CONFIG.modifierKey
        );
        if (choice && options[choice]) {
            CONFIG.modifierKey = parseInt(choice);
            saveConfig(CONFIG);
            alert(t.saveAlert);
        }
    };
    const handleKeyFunctionSetting = () => { // 處理按鍵7/9功能設定提示 / Handle key 7/9 function setting prompt
        const t = i18n[LANG];
        const baseOptions = {...t.keyFuncOptions};
        if (!['YOUTUBE', 'BILIBILI'].includes(PLATFORM)) delete baseOptions[4];
        const getChoice = (msgKey, currentVal) => {
            const message = `${msgKey === 'key7' ? t.promptKey7 : t.promptKey9}${Object.values(baseOptions).join('')}`;
            return prompt(message, currentVal);
        };
        const choice7 = getChoice('key7', CONFIG.key7Function);
        if (choice7 && baseOptions[choice7]) CONFIG.key7Function = parseInt(choice7);
        const choice9 = getChoice('key9', CONFIG.key9Function);
        if (choice9 && baseOptions[choice9]) CONFIG.key9Function = parseInt(choice9);
        saveConfig(CONFIG);
    };
    const handleFullscreenModeSetting = () => { // 處理YouTube全螢幕模式設定提示 / Handle YouTube fullscreen mode setting prompt
        const t = i18n[LANG];
        const options = t.fullscreenModeOptions;
        const choice = prompt(`${t.promptFullscreen}${Object.values(options).join('')}`,
            CONFIG.youtubeFullscreenMode
        );
        if (choice && options[choice]) {
            CONFIG.youtubeFullscreenMode = parseInt(choice);
            saveConfig(CONFIG);
            alert(t.saveAlert);
        }
    };
    const getDomainId = () => { // 獲取標準化域名標識 / Get standardized domain ID
        const hostParts = location.hostname.split('.');
        return hostParts.length > 2 ? hostParts.slice(-2).join('_') : hostParts.join('_');
    };
    const PLATFORM = (() => { // 平台檢測 / Platform detection
        const host = location.hostname;
        if (/youtube\.com|youtu\.be/.test(host)) return "YOUTUBE";
        if (/www.bilibili\.com/.test(host)) return "BILIBILI";
        if (/twitch\.tv/.test(host)) return "TWITCH";
        if (/steam(community|powered)\.com/.test(host)) return "STEAM";
        return "GENERIC";
    })();
    const CONFIG_STORAGE_KEY = 'ScrollVolumeDxConfig'; // 配置存儲鍵名 / Configuration storage key name
    const DEFAULT_CONFIG = { // 預設配置 / Default configuration
        stepTime: 5,
        stepTimeLong: 30,
        stepVolume: 10,
        key7Function: ['YOUTUBE', 'BILIBILI'].includes(PLATFORM) ? 4 : 1,
        key9Function: ['YOUTUBE', 'BILIBILI'].includes(PLATFORM) ? 4 : 1,
        modifierKey: 5, // 1=Alt 2=Ctrl 3=Shift 4=Meta 5=None
        fineVolumeStep: 1, // 微調音量步進值 / Fine volume adjustment step value
        youtubeFullscreenMode: 2 // YouTube全螢幕模式預設值改為2 (1=原生按鈕, 2=原生API, 3=網頁全螢幕) / YouTube fullscreen mode default value changed to 2
    };
    const getConfig = () => { // 獲取配置 / Get configuration
        const savedConfig = GM_getValue(CONFIG_STORAGE_KEY, {});
        const domainId = getDomainId();
        return {
            ...DEFAULT_CONFIG,
            ...(savedConfig[domainId] || {})
        };
    };
    const saveConfig = (config) => { // 保存配置 / Save configuration
        const savedConfig = GM_getValue(CONFIG_STORAGE_KEY, {});
        const domainId = getDomainId();
        const currentConfig = { ...config };
        const isDefault = Object.keys(DEFAULT_CONFIG).every(key =>
            currentConfig[key] === DEFAULT_CONFIG[key]
        );
        if (isDefault) {
            if (savedConfig[domainId]) {
                delete savedConfig[domainId];
                GM_setValue(CONFIG_STORAGE_KEY, savedConfig);
            }
            return;
        }
        const diffConfig = {};
        Object.keys(currentConfig).forEach(key => {
            if (currentConfig[key] !== DEFAULT_CONFIG[key]) {
                diffConfig[key] = currentConfig[key];
            }
        });
        savedConfig[domainId] = diffConfig;
        GM_setValue(CONFIG_STORAGE_KEY, savedConfig);
    };
    const CONFIG = (() => { // 初始化配置 / Initialize configuration
        const config = getConfig();
        saveConfig(config);
        return config;
    })();
    registerMenuCommands(); // 初始化選單 / Initialize menu
    let cachedVideo = null; // 緩存視頻元素 / Cache video element
    let lastVideoCheck = 0; // 最後檢查時間 / Last check time
    let videoElements = []; // 視頻元素列表 / Video element list
    let currentVideoIndex = 0; // 當前視頻索引 / Current video index
    let activeVideoId = null; // 激活視頻ID / Active video ID
    let videoDoubleClickHandler = null; // 用於存儲 YouTube 模式2和3的雙擊處理函數 / Used to store the double-click handler for YouTube mode 2 and 3
    const videoStateMap = new WeakMap(); // 視頻狀態映射 / Video state map
    function getVideoState(video) { // 獲取視頻狀態 / Get video state
        if (!videoStateMap.has(video)) {
            videoStateMap.set(video, {
                lastCustomRate: 1.0,
                isDefaultRate: true
            });
        }
        return videoStateMap.get(video);
    }
    const generateVideoId = (video) => // 生成視頻唯一ID / Generate unique video ID
        `${video.src}_${video.clientWidth}x${video.clientHeight}`;
    function getVideoElement() { // 獲取視頻元素 (防抖改進) / Get video element (debounce improvement)
        if (activeVideoId) {
            const activeVideo = videoElements.find(v => generateVideoId(v) === activeVideoId);
            if (activeVideo && document.contains(activeVideo)) {
                cachedVideo = activeVideo;
                return cachedVideo;
            }
        }
        if (cachedVideo && document.contains(cachedVideo) && (Date.now() - lastVideoCheck < 300)) { // 常規檢測邏輯
            return cachedVideo;
        }
        const handler = PLATFORM_HANDLERS[PLATFORM] || PLATFORM_HANDLERS.GENERIC;
        cachedVideo = handler.getVideo();
        lastVideoCheck = Date.now();
        updateVideoElements(); // 更新視頻元素列表和當前索引
        if (cachedVideo && videoElements.length > 0) {
            currentVideoIndex = videoElements.indexOf(cachedVideo);
            if (currentVideoIndex === -1) currentVideoIndex = 0;
            activeVideoId = generateVideoId(cachedVideo);
        }
        return cachedVideo;
    }
    function updateVideoElements() { // 更新視頻元素列表 / Update video element list
        videoElements = Array.from(document.querySelectorAll('video'))
            .filter(v => v.offsetParent !== null && v.readyState > 0);
    }
    function switchToNextVideo() { // 切換到下一個視頻 / Switch to next video
        if (videoElements.length < 2) return null;
        currentVideoIndex = (currentVideoIndex + 1) % videoElements.length;
        cachedVideo = videoElements[currentVideoIndex];
        activeVideoId = generateVideoId(cachedVideo);
        lastVideoCheck = Date.now();
        return cachedVideo;
    }
    function switchToPrevVideo() { // 切換到上一個視頻 / Switch to previous video
        if (videoElements.length < 2) return null;
        currentVideoIndex = (currentVideoIndex - 1 + videoElements.length) % videoElements.length;
        cachedVideo = videoElements[currentVideoIndex];
        activeVideoId = generateVideoId(cachedVideo);
        lastVideoCheck = Date.now();
        return cachedVideo;
    }
    function commonAdjustVolume(video, delta) { // 通用音量調整 / Common volume adjustment
        const isFineAdjust = Math.abs(delta) === CONFIG.fineVolumeStep;
        const actualDelta = isFineAdjust ? delta : (delta > 0 ? CONFIG.stepVolume : -CONFIG.stepVolume);
        const newVolume = clampVolume((video.volume * 100) + actualDelta);
        video.volume = newVolume / 100;
        showVolume(newVolume);
        return newVolume;
    }
    function clampVolume(vol) { // 音量限制 / Volume clamping
        return Math.round(Math.max(0, Math.min(100, vol)) * 100) / 100;
    }
    let isWebFullscreened = false; // Web全螢幕狀態標記 / Web fullscreen state flag
    let originalVideoParent = null; // 儲存原始父元素 / Store original parent element
    let originalVideoStyles = {}; // 儲存原始視頻樣式 / Store original video styles
    let originalParentStyles = {}; // 儲存原始父元素樣式 / Store original parent styles
    let webFullscreenContainer = null; // Web全螢幕容器 / Web fullscreen container
    function toggleWebFullscreen(video) { // Web全螢幕切換 / Web fullscreen toggle
        if (!video) return;
        if (isWebFullscreened) {
            if (webFullscreenContainer && webFullscreenContainer.contains(video)) { webFullscreenContainer.removeChild(video); } // 1. 從容器中移除視頻 / Remove video from container
            if (webFullscreenContainer && document.body.contains(webFullscreenContainer)) { document.body.removeChild(webFullscreenContainer); webFullscreenContainer = null; } // 2. 移除容器 / Remove container
            if (originalVideoParent && !originalVideoParent.contains(video)) { originalVideoParent.appendChild(video); } // 3. 將視頻移回原始父元素 / Move video back to original parent
            Object.assign(video.style, originalVideoStyles); // 4. 恢復原始視頻樣式 / Restore original video styles
            if (originalVideoParent) { Object.assign(originalVideoParent.style, originalParentStyles); } // 5. 恢復原始父元素樣式 / Restore original parent styles
            isWebFullscreened = false; // 6. 重置狀態標記 / Reset state flag
            originalVideoParent = null;
        } else {
            originalVideoParent = video.parentElement; // 儲存原始狀態 / Store original state
            if (!originalVideoParent) return; // 如果沒有父元素,無法操作 / If no parent element, cannot operate
            originalVideoStyles = {
                position: video.style.position,
                top: video.style.top,
                left: video.style.left,
                width: video.style.width,
                height: video.style.height,
                zIndex: video.style.zIndex,
                objectFit: video.style.objectFit,
                objectPosition: video.style.objectPosition
            };
            originalParentStyles = {
                position: originalVideoParent.style.position,
                overflow: originalVideoParent.style.overflow
            };
            if (!webFullscreenContainer) { // 創建容器 / Create container
                webFullscreenContainer = document.createElement('div');
                webFullscreenContainer.id = 'web-fullscreen-container';
                Object.assign(webFullscreenContainer.style, {
                    position: 'relative', // 修正:使用 relative 而非 fixed,使其在正常文件流中 / Fixed: Use relative instead of fixed to place it in normal document flow
                    zIndex: '2147483645', // 略低於顯示層,確保按鍵事件正常 / Slightly below display layer to ensure key events work
                    backgroundColor: 'black', // 黑色背景 / Black background
                    display: 'flex',
                    alignItems: 'center',
                    justifyContent: 'center',
                    margin: '0 auto', // 置中容器 / Center container
                    maxWidth: '100%', // 限制最大寬度 / Limit max width
                    maxHeight: '100vh' // 限制最大高度為視窗高度 / Limit max height to viewport height
                });
                webFullscreenContainer.addEventListener('click', () => { // 為容器添加點擊事件以切換播放/暫停 / Add click event to container to toggle play/pause
                    if (video && !video.paused) {
                        video.pause();
                    } else if (video) {
                        video.play().catch(() => {}); // 捕獲可能的錯誤 / Catch potential errors
                    }
                });
            }
            Object.assign(originalVideoParent.style, { // 應用父元素樣式 / Apply parent element styles
                position: 'static', // 重置父元素定位,避免干擾 / Reset parent element positioning to avoid interference
                overflow: 'visible'
            });
            originalVideoParent.removeChild(video); // 將視頻移入容器 / Move video into container
            webFullscreenContainer.appendChild(video);
            document.body.insertBefore(webFullscreenContainer, document.body.firstChild);
            video.style.position = '';
            video.style.top = '';
            video.style.left = '';
            video.style.width = '100%';
            video.style.height = 'auto'; // 高度自適應 / Height auto-adjust
            video.style.maxHeight = '100vh';
            video.style.zIndex = '';
            video.style.objectFit = 'contain'; // 保持比例 / Maintain aspect ratio
            video.style.objectPosition = 'center'; // 置中 / Center
            isWebFullscreened = true;
        }
    } // --- 結束 Web Fullscreen 相關函數 / End Web Fullscreen related functions ---
    const PLATFORM_HANDLERS = { // 平台處理器 / Platform handlers
        YOUTUBE: {
            getVideo: () => document.querySelector('video, ytd-player video') || findVideoInIframes(),
            adjustVolume: (video, delta) => {
                const ytPlayer = document.querySelector('#movie_player');
                if (ytPlayer?.getVolume) {
                    const currentVol = ytPlayer.getVolume();
                    const newVol = clampVolume(currentVol + delta);
                    ytPlayer.setVolume(newVol);
                    video.volume = newVol / 100;
                    showVolume(newVol);
                } else {
                    commonAdjustVolume(video, delta);
                }
            },
            toggleFullscreen: (video) => { // 根據設定切換模式 / Switch mode based on setting
                switch(CONFIG.youtubeFullscreenMode) {
                    case 1: // 原生最大化 / Native maximization
                        document.querySelector('.ytp-fullscreen-button')?.click();
                        break;
                    case 2: // 原生API最大化 / Native API maximization
                        toggleNativeFullscreen(video);
                        break;
                    case 3: // 網頁全螢幕 / Web fullscreen
                        toggleWebFullscreen(video);
                        break;
                }
            },
            setupVideoEventOverrides: (video) => { // 設置視頻事件覆蓋 / Set up video event overrides
                if (videoDoubleClickHandler) {
                    video.removeEventListener('dblclick', videoDoubleClickHandler);
                }
                if (CONFIG.youtubeFullscreenMode === 3) {
                    videoDoubleClickHandler = (e) => {
                        e.preventDefault();
                        e.stopPropagation();
                        toggleWebFullscreen(video);
                    };
                } else if (CONFIG.youtubeFullscreenMode === 2) {
                    videoDoubleClickHandler = (e) => {
                        e.preventDefault();
                        e.stopPropagation();
                        toggleNativeFullscreen(video);
                    };
                } else {
                    videoDoubleClickHandler = null; // 其他模式不處理雙擊 / Do not handle double-click for other modes
                    return; // 如果不處理,則不添加監聽器 / If not handling, do not add listener
                }
                video.addEventListener('dblclick', videoDoubleClickHandler);
            },
            specialKeys: {
                'Space': () => {},
                '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': () => {},
                '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: () => {
                const videos = Array.from(document.querySelectorAll('video'));
                const playingVideo = videos.find(v => v.offsetParent !== null && !v.paused);
                if (playingVideo) return playingVideo;
                const visibleVideo = videos.find(v => v.offsetParent !== null);
                if (visibleVideo) return visibleVideo;
                return 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?.();
                }
            },
            handleWheel: function(e) {
                if (isInputElement(e.target)) return;
                const video = this.getVideo();
                if (!video) return;
                const rect = video.getBoundingClientRect();
                const inVideoArea =
                      e.clientX >= rect.left - 50 && e.clientX <= rect.right + 50 &&
                      e.clientY >= rect.top - 30 && e.clientY <= rect.bottom + 30;
                if (inVideoArea) {
                    e.preventDefault();
                    e.stopPropagation();
                    const delta = -Math.sign(e.deltaY);
                    this.adjustVolume(video, delta * CONFIG.stepVolume);
                    showVolume(video.volume * 100);
                }
            }
        },
        GENERIC: {
            getVideo: () => {
                return document.querySelector('video.player') ||
                       findVideoInIframes() ||
                       document.querySelector('video, .video-player video, .video-js video, .player-container video');
            },
            adjustVolume: commonAdjustVolume,
            toggleFullscreen: (video) => toggleNativeFullscreen(video),
        }
    };
    function findVideoInIframes() { // 在iframe中尋找視頻 / Find video in iframes
        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) { // 原生全螢幕切換 / Native fullscreen toggle
        if (!video) return;
        try {
            if (document.fullscreenElement) {
                document.exitFullscreen();
            } else {
                let elementToFullscreen = video;
                for (let i = 0; i < 2; i++) {
                    elementToFullscreen = elementToFullscreen.parentElement || elementToFullscreen;
                }
                elementToFullscreen.requestFullscreen?.() ||
                elementToFullscreen.webkitRequestFullscreen?.() ||
                elementToFullscreen.msRequestFullscreen?.() ||
                video.requestFullscreen?.() ||
                video.webkitRequestFullscreen?.() ||
                video.msRequestFullscreen?.();
            }
        } catch (e) {
            console.error('Fullscreen error:', e);
        }
    }
    function simulateKeyPress(key) { // 模擬按鍵 / Simulate key press
        document.dispatchEvent(new KeyboardEvent('keydown', {key, bubbles: true}));
    }
    function isInputElement(target) { // 檢查是否為輸入元素 / Check if target is an input element
        return /^(INPUT|TEXTAREA|SELECT)$/.test(target.tagName) || target.isContentEditable;
    }
    function adjustRate(video, changeValue) { // 調整播放速率 / Adjust playback rate
        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) { // 切換播放速率 / Toggle playback rate
        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) { // 顯示音量 / Show volume
        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() { // 創建音量顯示 / Create volume display
        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) { // 處理視頻滾輪事件 / Handle video wheel event
        e.preventDefault();
        e.stopPropagation();
        const video = e.target;
        const normalizedDelta = -Math.sign(e.deltaY);
        PLATFORM_HANDLERS[PLATFORM].adjustVolume(video, normalizedDelta * CONFIG.stepVolume);
    }
function handleTwitchWheel(e) { // 處理Twitch滾輪事件 / Handle Twitch wheel event
    if (isInputElement(e.target)) return;
    const video = getVideoElement();
    if (!video) return;
    const rect = video.getBoundingClientRect();
    const inVideoArea =
        e.clientX >= rect.left - 50 && e.clientX <= rect.right + 50 &&
        e.clientY >= rect.top - 30 && e.clientY <= rect.bottom + 30;
    if (inVideoArea) {
        e.preventDefault();
        e.stopPropagation();
        const delta = -Math.sign(e.deltaY);
        const volumeChange = delta * CONFIG.stepVolume;
        PLATFORM_HANDLERS.TWITCH.adjustVolume(video, volumeChange);
        showVolume(video.volume * 100);
    }
}
    function handleKeyEvent(e) { // 處理鍵盤事件 / Handle keyboard event
        if (isInputElement(e.target)) return;
        const video = getVideoElement();
        const handler = PLATFORM_HANDLERS[PLATFORM];
        const isCustomModifier = (() => {
            if (CONFIG.modifierKey === 5) return false;
            const requiredModifier = {
                1: 'altKey',
                2: 'ctrlKey',
                3: 'shiftKey',
                4: 'metaKey'
            }[CONFIG.modifierKey];
            const otherModifiers = ['altKey','ctrlKey','shiftKey','metaKey']
            .filter(k => k !== requiredModifier)
            .some(k => e[k]);
            return e[requiredModifier] && !otherModifiers;
        })();
        const hasOtherModifiers = e.altKey || e.ctrlKey || e.shiftKey || e.metaKey;
        if (!isCustomModifier && hasOtherModifiers) {
            return;
        }
        if (isCustomModifier) {
            const volumeActions = {
                'Numpad8': () => handler.adjustVolume(video, CONFIG.fineVolumeStep),
                'Numpad2': () => handler.adjustVolume(video, -CONFIG.fineVolumeStep)
            };
            if (volumeActions[e.code]) {
                volumeActions[e.code]();
                e.preventDefault();
                return;
            }
            return;
        }
        if (e.code === 'Numpad7') {
            switch (CONFIG.key7Function) {
                case 1:
                    video && (video.currentTime -= CONFIG.stepTimeLong);
                    break;
                case 2:
                    history.back();
                    break;
                case 3:
                    switchToPrevVideo()?.play().catch(() => {});
                    break;
                case 4:
                    if (handler.specialKeys?.Numpad7) {
                        handler.specialKeys.Numpad7();
                    }
                    break;
            }
            e.preventDefault();
            return;
        }
        if (e.code === 'Numpad9') {
            switch (CONFIG.key9Function) {
                case 1:
                    video && (video.currentTime += CONFIG.stepTimeLong);
                    break;
                case 2:
                    history.forward();
                    break;
                case 3:
                    switchToNextVideo()?.play().catch(() => {});
                    break;
                case 4:
                    if (handler.specialKeys?.Numpad9) {
                        handler.specialKeys.Numpad9();
                    }
                    break;
            }
            e.preventDefault();
            return;
        }
        if (handler.specialKeys?.[e.code]) {
            handler.specialKeys[e.code]();
            e.preventDefault();
            return;
        }
        const actions = {
            'Space': () => video && video[video.paused ? 'play' : 'pause'](),
            'Numpad5': () => video && video[video.paused ? 'play' : 'pause'](),
            'NumpadEnter': () => handler.toggleFullscreen(video), // 使用平台特定的切換函數 / Use platform-specific toggle function
            'NumpadAdd': () => video && (video.currentTime += video.duration * 0.1),
            'NumpadSubtract': () => video && (video.currentTime -= video.duration * 0.1),
            'Numpad0': () => video && togglePlaybackRate(video),
            'Numpad1': () => video && adjustRate(video, -0.1),
            'Numpad3': () => video && adjustRate(video, 0.1),
            'Numpad8': () => video && handler.adjustVolume(video, CONFIG.stepVolume),
            'Numpad2': () => video && handler.adjustVolume(video, -CONFIG.stepVolume),
            'Numpad4': () => video && (video.currentTime -= CONFIG.stepTime),
            'Numpad6': () => video && (video.currentTime += CONFIG.stepTime)
        };
        if (actions[e.code]) {
            actions[e.code]();
            e.preventDefault();
        }
    }
    function bindVideoEvents() { // 綁定視頻事件 / Bind video events
        if (PLATFORM === 'TWITCH' || PLATFORM === 'STEAM') return;
        document.querySelectorAll('video').forEach(video => {
            if (!video.dataset.volumeBound) {
                video.addEventListener('wheel', handleVideoWheel, { passive: false });
                video.dataset.volumeBound = 'true';
            }
            // 為 YouTube 模式2和3設置雙擊覆蓋 / Set up double-click override for YouTube mode 2 and 3
            if (PLATFORM === 'YOUTUBE') {
                PLATFORM_HANDLERS.YOUTUBE.setupVideoEventOverrides(video);
            }
        });
    }
    function init() { // 初始化 / Initialization
        bindVideoEvents();
        document.addEventListener('keydown', handleKeyEvent, true);
        if (PLATFORM === 'STEAM') {
            document.addEventListener('wheel',
           PLATFORM_HANDLERS.STEAM.handleWheel.bind(PLATFORM_HANDLERS.STEAM),
           { capture: true, passive: false }
        );
        }
        if (PLATFORM === 'TWITCH') {
            document.addEventListener('wheel', handleTwitchWheel, { capture: true, passive: false });
        }
        updateVideoElements();
        new MutationObserver(() => { // 監聽DOM變化 / Listen for DOM changes
            bindVideoEvents();
            updateVideoElements();
            if (activeVideoId && !videoElements.some(v => generateVideoId(v) === activeVideoId)) {
                activeVideoId = null;
            }
        }).observe(document.body, { childList: true, subtree: true });
    }
    if (document.readyState !== 'loading') init();
    else document.addEventListener('DOMContentLoaded', init);
})();