Greasy Fork is available in English.

滚动音量 Dx 版

新增单一入口设置菜单,长按功能、全域/独立设定切换、滚轮范围缩减、局部功能停用。Key5 长按来回变速,Key1/3 长按非线性加速。新增初始音量记忆、影片切换焦点。音量储存防抖、设定清除功能。强制解除静音保护。修复音量修饰键于滚轮事件之支持。优化长按判定时间与按键1/3重复频率。

您需要先安装一款用户脚本管理器扩展,例如 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      10.38
// @description:zh-TW  新增單一入口設定選單,長按功能、全域/獨立設定切換、滾輪範圍縮減、局部功能停用。Key5 長按來回變速,Key1/3 長按非線性加速。新增初始音量記憶、影片切換焦點。音量儲存防抖、設定清除功能。強制解除靜音保護。修復音量修飾鍵於滾輪事件之支援。優化長按判定時間與按鍵1/3重複頻率。
// @description:zh-CN 新增单一入口设置菜单,长按功能、全域/独立设定切换、滚轮范围缩减、局部功能停用。Key5 长按来回变速,Key1/3 长按非线性加速。新增初始音量记忆、影片切换焦点。音量储存防抖、设定清除功能。强制解除静音保护。修复音量修饰键于滚轮事件之支持。优化长按判定时间与按键1/3重复频率。
// @description  Added single-entry settings menu, long-press functions, Global/Independent settings toggle, Scroll range reduction, Partial feature disable. Key5 long-press toggle speed, Key1/3 long-press non-linear acceleration. Added initial volume memory, video switch focus. Volume save debounce, clear settings. Force unmute protection. Fixed volume modifier key support for wheel events. Optimized long-press detection time and Key 1/3 repeat rate.
// @match        https://*/*
// @match        https://www.youtube.com/*
// @match        https://www.youtube-nocookie.com/*
// @match        https://www.bilibili.com/*
// @match        https://live.bilibili.com/*
// @match        https://www.twitch.tv/*
// @match        https://store.steampowered.com/*
// @match        https://www.facebook.com/*
// @exclude      https://www.youtube.com/live_chat*
// @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';
    const i18n = {
        zh: {
            menuTitle: '🚀 滾動音量 Dx 設定選單',
            menuMode: '1. 切換設定模式 (當前:',
            menuStep: '2. 步進設定',
            menuInitVol: '3. 初始音量開關 (當前:',
            menuVolume: '4. 音量設定',
            menuKeySettings: '5. 按鍵設定',
            menuScrollRange: '6. 滾輪調節範圍 (%)',
            menuFullscreen: '7. 全螢幕模式',
            menuDisable: '8. 停用功能',
            menuClear: '9. 清除設定',
            menuExit: '0. 退出設定',
            modeGlobal: '全域)',
            modeDomain: '獨立)',
            promptConfirm: '設定已保存',
            promptClearConfirm: '確定清除當前模式設定?',
            promptCleared: '設定已清除',
            keySettingsMenu: '按鍵設定選單\n1. 設定鍵 1/3 (變速)\n2. 設定鍵 4 (倒退)\n3. 設定鍵 6 (加速)\n4. 設定鍵 5 (來回變速)\n5. 設定鍵 7/9 (長按行為)\n6. 設定鍵 / * (切換行為)\n7. 所有鍵長按判定時間 (毫秒)\n0. 返回',
            lp13Menu: '鍵 1/3 變速設定 (當前:',
            lp13Opts: { 1: '1. 固定倍率 (每次 10%)', 2: '2. 預定義加速 (分段)', 3: '3. 自定義加減速' },
            lp13Custom: '輸入自定義倍率 (逗號分隔):',
            lp13Interval: '鍵 1/3 長按間隔 (毫秒):',
            lp4Step: '按鍵 4 倒退步進 (秒)',
            lp4Interval: '按鍵 4 倒退間隔 (毫秒)',
            lp6Start: '按鍵 6 起始倍率 (1.0=正常)',
            lp6Interval: '按鍵 6 加速間隔 (毫秒)',
            lp6Step: '按鍵 6 逐步加速倍率 (0=禁用自動加速 1=100%)',
            lp6Max: '按鍵 6 加速上限倍率 (0=無上限)',
            lp5Rate: '按鍵 5 長按變速倍率',
            lp79Menu: '鍵 7/9 長按行為設定 (當前:',
            lp79Opts: { 1: '1. 重複 X 倍長步進', 2: '2. 切換播放清單', 3: '3. 網頁返回' },
            lp79Interval: '重複間隔 (毫秒)',
            lp79Mult: '長步進倍率 (X 倍):',
            divMulMenu: '鍵 / * 切換行為設定 (當前:',
            divMulOpts: { 1: '1. 切換播放清單', 2: '2. 切換影片元素', 3: '3. 網頁返回' },
            lpDetectTime: '所有鍵長按判定時間 (毫秒) [建議不低於 150ms]:',
            disableMenu: '停用功能選單\n1. 停用滾輪',
            disableMenu2: '\n2. 停用鍵盤',
            disableMenu3: '\n3. 停用指定鍵',
            disableMenu0: '\n0. 返回',
            keyMapPrompt: '輸入停用鍵編號 (逗號分隔):\n0:Numpad0(切換倍率), 1:Numpad1(減速), 2:Numpad2(音量 -)\n3:Numpad3(加速), 4:Numpad4(倒退), 5:Numpad5(播放)\n6:Numpad6(前進), 7:Numpad7(長步進), 8:Numpad8(音量 +)\n9:Numpad9(長步進), 10:Space(播放), 11:Enter(全螢幕)\n12:+(進度 +), 13:-(進度 -), 14:Left, 15:Right\n16:Up, 17:Down, 18:NumpadDivide(上一影片), 19:NumpadMultiply(下一影片)',
            modifierPrompt: '選擇音量微調修飾鍵:',
            modifierOptions: { 1: '1. Alt 鍵', 2: '2. Ctrl 鍵', 3: '3. Shift 鍵', 4: '4. Meta 鍵 (⌘)', 5: '5. 關閉此功能' },
            fullscreenPrompt: '選擇全螢幕模式:',
            fullscreenOpts: { 1: '1. 原生按鈕點擊', 2: '2. 原生 API 切換', 3: '3. 網頁全螢幕 (Web Fullscreen)' },
            stepMenu: '步進設定\n1. 步進 (秒): ',
            stepMenuLong: '\n2. 長步進 (秒): ',
            volumeMenu: '音量設定',
            volumeStep: '1. 音量步進 (%): ',
            volumeModifier: '2. 音量修飾鍵',
            volumeInitVal: '3. 初始音量預設值 (%)',
            volumeLocked: ' (🔒 平台衝突,強制關閉)',
            statusOn: '啟用)',
            statusOff: '停用)',
            videoSwitched: '已切換至影片 %d/%d',
            fsModeWarning: '注意:此平台不支持模式 1,將自動使用模式 2,但不會修改您的全域設定值',
            divMulModeWarning: '注意:此平台不支持播放清單切換,將自動使用影片元素切換,但不會修改您的全域設定值'
        },
        en: {
            menuTitle: '🚀 Scroll Volume Dx Settings',
            menuMode: '1. Toggle Setting Mode (Current: ',
            menuStep: '2. Step Settings',
            menuInitVol: '3. Initial Volume Toggle (Current: ',
            menuVolume: '4. Volume Settings',
            menuKeySettings: '5. Key Settings',
            menuScrollRange: '6. Scroll Trigger Range (%)',
            menuFullscreen: '7. Fullscreen Mode',
            menuDisable: '8. Disable Features',
            menuClear: '9. Clear Settings',
            menuExit: '0. Exit',
            modeGlobal: 'Global)',
            modeDomain: 'Independent)',
            promptConfirm: 'Settings Saved',
            promptClearConfirm: 'Confirm clear current mode settings?',
            promptCleared: 'Settings Cleared',
            keySettingsMenu: 'Key Settings Menu\n1. Key 1/3 (Rate)\n2. Key 4 (Rewind)\n3. Key 6 (Speedup)\n4. Key 5 (Toggle Speed)\n5. Key 7/9 (Long Press)\n6. Key / * (Switch)\n7. Long-press Detect Time (ms)\n0. Back',
            lp13Menu: 'Key 1/3 Rate Settings (Current: ',
            lp13Opts: { 1: '1. Fixed Rate (10% per step)', 2: '2. Pre-defined Acceleration', 3: '3. Custom Rates' },
            lp13Custom: 'Enter custom rates (comma separated):',
            lp13Interval: 'Key 1/3 Long-press Interval (ms):',
            lp4Step: 'Key 4 rewind step (seconds)',
            lp4Interval: 'Key 4 rewind interval (milliseconds)',
            lp6Start: 'Key 6 start rate (1.0=normal)',
            lp6Interval: 'Key 6 acceleration interval (milliseconds)',
            lp6Step: 'Key 6 rate step per interval (0=disable auto-accel, 1=100%)',
            lp6Max: 'Key 6 max rate limit (0=no limit)',
            lp5Rate: 'Key 5 long-press toggle rate',
            lp79Menu: 'Key 7/9 Long Press Behavior (Current: ',
            lp79Opts: { 1: '1. Repeat X times Long Step', 2: '2. Switch Playlist', 3: '3. Browser Back/Forward' },
            lp79Interval: 'Repeat Interval (ms)',
            lp79Mult: 'Long Step Multiplier (X times):',
            divMulMenu: 'Key / * Switch Behavior (Current: ',
            divMulOpts: { 1: '1. Switch Playlist', 2: '2. Switch Video Element', 3: '3. Browser Back/Forward' },
            lpDetectTime: 'All Keys Long-press Detect Time (ms) [Recommended >= 150ms]:',
            disableMenu: 'Disable Features\n1. Disable Wheel',
            disableMenu2: '\n2. Disable Keyboard',
            disableMenu3: '\n3. Disable Specific Keys',
            disableMenu0: '\n0. Back',
            keyMapPrompt: 'Enter key IDs to disable (comma separated):\n0:Numpad0(Toggle Rate), 1:Numpad1(Rate-), 2:Numpad2(Vol-)\n3:Numpad3(Rate+), 4:Numpad4(Rewind), 5:Numpad5(Play)\n6:Numpad6(Forward), 7:Numpad7(Long Step), 8:Numpad8(Vol+)\n9:Numpad9(Long Step), 10:Space(Play), 11:Enter(Fullscreen)\n12:+(Prog+), 13:-(Prog-), 14:Left, 15:Right\n16:Up, 17:Down, 18:NumpadDivide(Prev Video), 19:NumpadMultiply(Next Video)',
            modifierPrompt: 'Select volume modifier key:',
            modifierOptions: { 1: '1. Alt key', 2: '2. Ctrl key', 3: '3. Shift key', 4: '4. Meta key (⌘)', 5: '5. Disable feature' },
            fullscreenPrompt: 'Select fullscreen mode:',
            fullscreenOpts: { 1: '1. Native Button Click', 2: '2. Native API toggle', 3: '3. Web Fullscreen' },
            stepMenu: 'Step Settings\n1. Step Time (s): ',
            stepMenuLong: '\n2. Long Step Time (s): ',
            volumeMenu: 'Volume Settings',
            volumeStep: '1. Volume Step (%): ',
            volumeModifier: '2. Volume Modifier',
            volumeInitVal: '3. Initial Volume Default (%)',
            volumeLocked: ' (🔒 Platform conflict, forced off)',
            statusOn: 'ON)',
            statusOff: 'OFF)',
            videoSwitched: 'Switched to video %d/%d',
            fsModeWarning: 'Note: This platform does not support mode 1, will use mode 2 automatically without modifying your global setting',
            divMulModeWarning: 'Note: This platform does not support playlist switch, will use video element switch automatically without modifying your global setting'
        }
    };
    const STORAGE_MODE_KEY = 'SV_SettingsMode';
    const STORAGE_GLOBAL_KEY = 'SV_Config';
    const STORAGE_DOMAIN_INIT_VOL = 'SV_DomainInitVol';
    const getDomainId = () => location.hostname.split('.').slice(-2).join('_');
    const STORAGE_DOMAIN_KEY = (domain) => `SV_Config_${domain}`;
    const DEFAULT_CONFIG = {
        stepTime: 5,
        stepTimeLong: 30,
        stepVolume: 10,
        modifierKey: 5,
        fineVolumeStep: 1,
        fullscreenMode: 1,
        scrollTriggerRange: 100,
        disableWheel: false,
        disableKeyboard: false,
        disabledKeys: [],
        longPress4Step: 1,
        longPress4Interval: 100,
        longPress6StartRate: 2.0,
        longPress6Interval: 1000,
        longPress6Step: 1.0,
        longPress6MaxRate: 10,
        longPress5SlowRate: 0.5,
        longPress13Mode: 1,
        longPress13CustomRates: '0.5,1.0,1.5,2.0',
        longPress13Acceleration: 0.1,
        longPress13Interval: 100,
        longPress79Mode: 1,
        longPress79Interval: 500,
        longPress79Multiplier: 1,
        longPressDivMulMode: 1,
        initialVolumeEnabled: false,
        initialVolumeValue: 10,
        longPressDetectTime: 200
    };
    let CONFIG = {};
    let isDomainMode = false;
    let globalConfig = {};
    let domainInitVols = {};
    let volumeSaveTimer = null;
    let lastRectUpdate = 0;
    const isBilibili = /www\.bilibili\.com/.test(location.hostname);
    const isSteam = /steam(community|powered)\.com/.test(location.hostname);
    const isVolumeLocked = isBilibili || isSteam;
    const PLATFORM = (() => {
        const host = location.hostname;
        if (/youtube\.com|youtu\.be|youtube-nocookie\.com/.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";
        if (/facebook\.com|fb\.watch/.test(host)) return "FACEBOOK";
        return "GENERIC";
    })();
    let cachedVideo = null;
    let lastVideoCheck = 0;
    let videoElements = [];
    let currentVideoIndex = 0;
    let activeVideoId = null;
    const videoStateMap = new WeakMap();
    const generateVideoId = (video) => `${video.nodeName}_${video.src}_${video.clientWidth}x${video.clientHeight}`;
    function getStorageKey() {
        return isDomainMode ? STORAGE_DOMAIN_KEY(getDomainId()) : STORAGE_GLOBAL_KEY;
    }
    function loadConfig() {
        const mode = GM_getValue(STORAGE_MODE_KEY, {});
        const domain = getDomainId();
        isDomainMode = (mode[domain] === true);
        globalConfig = { ...DEFAULT_CONFIG, ...(GM_getValue(STORAGE_GLOBAL_KEY, {}) || {}) };
        domainInitVols = GM_getValue(STORAGE_DOMAIN_INIT_VOL, {});
        if (isDomainMode) {
            const domainConfig = GM_getValue(STORAGE_DOMAIN_KEY(domain), {});
            CONFIG = { ...globalConfig, ...domainConfig };
        } else {
            CONFIG = { ...globalConfig };
        }
        if (isVolumeLocked) {
            CONFIG.initialVolumeEnabled = false;
        }
    }
    function saveConfig() {
        if (isDomainMode) {
            const domain = getDomainId();
            const toSave = {};
            Object.keys(CONFIG).forEach(k => {
                if (CONFIG[k] !== globalConfig[k]) toSave[k] = CONFIG[k];
            });
            GM_setValue(STORAGE_DOMAIN_KEY(domain), toSave);
        } else {
            const toSave = {};
            Object.keys(CONFIG).forEach(k => {
                if (CONFIG[k] !== DEFAULT_CONFIG[k]) toSave[k] = CONFIG[k];
            });
            GM_setValue(STORAGE_GLOBAL_KEY, toSave);
            globalConfig = { ...DEFAULT_CONFIG, ...toSave };
        }
    }
    function saveDomainInitVol(vol) {
        if (isVolumeLocked) return;
        const domain = getDomainId();
        domainInitVols[domain] = vol;
        if (volumeSaveTimer) clearTimeout(volumeSaveTimer);
        volumeSaveTimer = setTimeout(() => {
            GM_setValue(STORAGE_DOMAIN_INIT_VOL, domainInitVols);
        }, 500);
    }
    function getDomainInitVol() {
        if (isVolumeLocked) return CONFIG.initialVolumeValue;
        const domain = getDomainId();
        return domainInitVols[domain] !== undefined ? domainInitVols[domain] : CONFIG.initialVolumeValue;
    }
    function clearSettings() {
        if (confirm(i18n[LANG].promptClearConfirm)) {
            if (isDomainMode) {
                GM_setValue(STORAGE_DOMAIN_KEY(getDomainId()), {});
            } else {
                GM_setValue(STORAGE_GLOBAL_KEY, {});
            }
            loadConfig();
            alert(i18n[LANG].promptCleared);
        }
    }
    function toggleConfigMode() {
        const domain = getDomainId();
        const modes = GM_getValue(STORAGE_MODE_KEY, {});
        modes[domain] = !isDomainMode;
        GM_setValue(STORAGE_MODE_KEY, modes);
        isDomainMode = !isDomainMode;
        loadConfig();
        alert(i18n[LANG].promptConfirm);
    }
    function openMainMenu() {
        let loop = true;
        while (loop) {
            const t = i18n[LANG];
            const modeText = isDomainMode ? t.modeDomain : t.modeGlobal;
            const initVolStatus = CONFIG.initialVolumeEnabled ? t.statusOn : t.statusOff;
            const lockedText = isVolumeLocked ? t.volumeLocked : '';
            const menuStr = `${t.menuTitle}\n${t.menuMode}${modeText}\n${t.menuStep}\n${t.menuInitVol}${initVolStatus}${lockedText}\n${t.menuVolume}\n${t.menuKeySettings}\n${t.menuScrollRange}\n${t.menuFullscreen}\n${t.menuDisable}\n${t.menuClear}\n${t.menuExit}`;
            const choice = prompt(menuStr, '0');
            if (choice === null) break;
            switch (choice) {
                case '1': toggleConfigMode(); break;
                case '2': showStepMenu(); break;
                case '3':
                    if (!isVolumeLocked) {
                        CONFIG.initialVolumeEnabled = !CONFIG.initialVolumeEnabled;
                        saveConfig();
                        alert(t.promptConfirm);
                    } else {
                        alert('此平台因衝突強制關閉此功能');
                    }
                    break;
                case '4': showVolumeMenu(); break;
                case '5': showKeySettings(); break;
                case '6': setNumConfig('scrollTriggerRange', t.menuScrollRange, 0, 100); break;
                case '7': setFullscreenMode(); break;
                case '8': showDisableMenu(); break;
                case '9': clearSettings(); break;
                case '0': loop = false; break;
            }
        }
    }
    function showStepMenu() {
        const t = i18n[LANG];
        const stepVal = prompt(`${t.stepMenu}${CONFIG.stepTime}`, CONFIG.stepTime);
        if (stepVal !== null && !isNaN(stepVal)) {
            CONFIG.stepTime = Math.max(0, parseFloat(stepVal));
            const stepLongVal = prompt(`${t.stepMenu}${CONFIG.stepTime}${t.stepMenuLong}`, CONFIG.stepTimeLong);
            if (stepLongVal !== null && !isNaN(stepLongVal)) {
                CONFIG.stepTimeLong = Math.max(0, parseFloat(stepLongVal));
                saveConfig();
                alert(t.promptConfirm);
            }
        }
    }
    function showVolumeMenu() {
        let loop = true;
        while (loop) {
            const t = i18n[LANG];
            const choice = prompt(`${t.volumeMenu}\n${t.volumeStep}${CONFIG.stepVolume}\n${t.volumeModifier}\n${t.volumeInitVal}\n0. 返回`, '0');
            if (choice === null) break;
            switch (choice) {
                case '1':
                    const stepVal = prompt(t.volumeStep, CONFIG.stepVolume);
                    if (stepVal !== null && !isNaN(stepVal)) {
                        CONFIG.stepVolume = Math.max(0, Math.min(100, parseFloat(stepVal)));
                        saveConfig();
                        alert(t.promptConfirm);
                    }
                    break;
                case '2':
                    const modChoice = prompt(`${t.modifierPrompt}\n${Object.values(t.modifierOptions).join('\n')}`, CONFIG.modifierKey);
                    if (modChoice && t.modifierOptions[modChoice]) {
                        CONFIG.modifierKey = parseInt(modChoice);
                        const fine = prompt('Set Fine Volume Step (%)', CONFIG.fineVolumeStep);
                        if (fine && !isNaN(fine)) CONFIG.fineVolumeStep = parseFloat(fine);
                        saveConfig();
                        alert(t.promptConfirm);
                    }
                    break;
                case '3':
                    const val = prompt(t.volumeInitVal, CONFIG.initialVolumeValue);
                    if (val !== null && !isNaN(val)) {
                        CONFIG.initialVolumeValue = Math.max(0, Math.min(100, parseFloat(val)));
                        saveConfig();
                        alert(t.promptConfirm);
                    }
                    break;
                case '0': loop = false; break;
            }
        }
    }
    function setNumConfig(key, promptText, min = 0, max = 9999) {
        const val = prompt(promptText, CONFIG[key]);
        if (val !== null && !isNaN(val)) {
            let num = parseFloat(val);
            if (num < min) num = min;
            if (num > max) num = max;
            CONFIG[key] = num;
            saveConfig();
            alert(i18n[LANG].promptConfirm);
        }
    }
    function setFullscreenMode() {
        const t = i18n[LANG];
        const handler = PLATFORM_HANDLERS[PLATFORM];
        let options = {};
        if (handler.hasNativeFullscreenBtn) {
            options[1] = t.fullscreenOpts[1];
        }
        options[2] = t.fullscreenOpts[2];
        options[3] = t.fullscreenOpts[3];
        const currentMode = CONFIG.fullscreenMode;
        const displayMode = options[currentMode] ? currentMode : 2;
        const choice = prompt(`${t.fullscreenPrompt}\n${Object.values(options).join('\n')}${!handler.hasNativeFullscreenBtn && currentMode === 1 ? '\n' + t.fsModeWarning : ''}`, displayMode);
        if (choice && options[choice]) {
            CONFIG.fullscreenMode = parseInt(choice);
            saveConfig();
            alert(t.promptConfirm);
        }
    }
    function showKeySettings() {
        let loop = true;
        while (loop) {
            const t = i18n[LANG];
            const handler = PLATFORM_HANDLERS[PLATFORM];
            const choice = prompt(t.keySettingsMenu, '0');
            if (choice === null) break;
            switch (choice) {
                case '1':
                    const mode13 = prompt(`${t.lp13Menu}${CONFIG.longPress13Mode})\n${Object.values(t.lp13Opts).join('\n')}`, CONFIG.longPress13Mode);
                    if (mode13 && t.lp13Opts[mode13]) {
                        CONFIG.longPress13Mode = parseInt(mode13);
                        if (CONFIG.longPress13Mode === 3) {
                            const rates = prompt(t.lp13Custom, CONFIG.longPress13CustomRates);
                            if (rates) CONFIG.longPress13CustomRates = rates;
                        }
                        const interval13 = prompt(t.lp13Interval, CONFIG.longPress13Interval);
                        if (interval13 !== null && !isNaN(interval13)) {
                            CONFIG.longPress13Interval = Math.max(50, parseInt(interval13));
                        }
                        saveConfig();
                        alert(t.promptConfirm);
                    }
                    break;
                case '2':
                    const s4 = prompt(t.lp4Step, CONFIG.longPress4Step);
                    const i4 = prompt(t.lp4Interval, CONFIG.longPress4Interval);
                    if (s4 && i4) { CONFIG.longPress4Step = parseFloat(s4); CONFIG.longPress4Interval = parseFloat(i4); saveConfig(); alert(t.promptConfirm); }
                    break;
                case '3':
                    const r6 = prompt(t.lp6Start, CONFIG.longPress6StartRate);
                    const i6 = prompt(t.lp6Interval, CONFIG.longPress6Interval);
                    const s6 = prompt(t.lp6Step, CONFIG.longPress6Step);
                    const m6 = prompt(t.lp6Max, CONFIG.longPress6MaxRate);
                    if (r6 && i6 && s6 && m6 !== null) {
                        CONFIG.longPress6StartRate = parseFloat(r6);
                        CONFIG.longPress6Interval = parseFloat(i6);
                        CONFIG.longPress6Step = parseFloat(s6);
                        CONFIG.longPress6MaxRate = parseFloat(m6);
                        saveConfig(); alert(t.promptConfirm);
                    }
                    break;
                case '4':
                    const r5 = prompt(t.lp5Rate, CONFIG.longPress5SlowRate);
                    if (r5) { CONFIG.longPress5SlowRate = parseFloat(r5); saveConfig(); alert(t.promptConfirm); }
                    break;
                case '5':
                    const mode79 = prompt(`${t.lp79Menu}${CONFIG.longPress79Mode})\n${Object.values(t.lp79Opts).join('\n')}`, CONFIG.longPress79Mode);
                    if (mode79 && t.lp79Opts[mode79]) {
                        CONFIG.longPress79Mode = parseInt(mode79);
                        const interval = prompt(t.lp79Interval, CONFIG.longPress79Interval);
                        if (interval && !isNaN(interval)) CONFIG.longPress79Interval = parseInt(interval);
                        if (CONFIG.longPress79Mode === 1) {
                            const mult = prompt(t.lp79Mult, CONFIG.longPress79Multiplier);
                            if (mult && !isNaN(mult)) CONFIG.longPress79Multiplier = parseFloat(mult);
                        }
                        saveConfig(); alert(t.promptConfirm);
                    }
                    break;
                case '6':
                    const availableOpts = {};
                    if (handler.hasPlaylist) { availableOpts[1] = t.divMulOpts[1]; }
                    availableOpts[2] = t.divMulOpts[2];
                    availableOpts[3] = t.divMulOpts[3];
                    const currentMode = CONFIG.longPressDivMulMode;
                    const displayMode = availableOpts[currentMode] ? currentMode : 2;
                    const modeDM = prompt(`${t.divMulMenu}${displayMode})\n${Object.values(availableOpts).join('\n')}${!handler.hasPlaylist && currentMode === 1 ? '\n' + t.divMulModeWarning : ''}`, displayMode);
                    if (modeDM && availableOpts[modeDM]) {
                        CONFIG.longPressDivMulMode = parseInt(modeDM);
                        saveConfig();
                        alert(t.promptConfirm);
                    }
                    break;
                case '7':
                    const detTime = prompt(t.lpDetectTime, CONFIG.longPressDetectTime);
                    if (detTime !== null && !isNaN(detTime)) {
                        let num = parseInt(detTime);
                        if (num < 150) {
                            if (!confirm('警告:設定低於 150ms 可能導致誤觸短按與長按切換,是否繼續?')) break;
                        }
                        CONFIG.longPressDetectTime = Math.max(50, num);
                        saveConfig();
                        alert(t.promptConfirm);
                    }
                    break;
                case '0': loop = false; break;
            }
        }
    }
    function showDisableMenu() {
        let loop = true;
        while (loop) {
            const t = i18n[LANG];
            const wStatus = CONFIG.disableWheel ? '(Enabled)' : '(Disabled)';
            const kStatus = CONFIG.disableKeyboard ? '(Enabled)' : '(Disabled)';
            const choice = prompt(`${t.disableMenu}${wStatus}${t.disableMenu2}${kStatus}${t.disableMenu3}${t.disableMenu0}`, '0');
            if (choice === null) break;
            switch (choice) {
                case '1': CONFIG.disableWheel = !CONFIG.disableWheel; saveConfig(); alert(t.promptConfirm); break;
                case '2': CONFIG.disableKeyboard = !CONFIG.disableKeyboard; saveConfig(); alert(t.promptConfirm); break;
                case '3':
                    const keys = prompt(t.keyMapPrompt, CONFIG.disabledKeys.join(','));
                    if (keys !== null) {
                        CONFIG.disabledKeys = keys.split(',').map(k => parseInt(k.trim())).filter(k => !isNaN(k));
                        saveConfig();
                        alert(t.promptConfirm);
                    }
                    break;
                case '0': loop = false; break;
            }
        }
    }
    function getVideoState(video) {
        if (!videoStateMap.has(video)) {
            videoStateMap.set(video, { lastCustomRate: 1.0, isDefaultRate: true, lastManualRate: 1.0 });
        }
        return videoStateMap.get(video);
    }
    function setActiveVideo(video, autoPlay = true) {
        if (!video) return false;
        const idx = videoElements.indexOf(video);
        if (idx !== -1) {
            currentVideoIndex = idx;
            activeVideoId = generateVideoId(video);
            cachedVideo = video;
            lastVideoCheck = performance.now();
            applyInitialVolume(video);
            if (autoPlay && video.paused && PLATFORM !== 'BILIBILI') {
                video.play().catch(() => {});
            }
            showVolume(video.volume * 100, video);
            return true;
        }
        return false;
    }
    function applyInitialVolume(video) {
        if (!CONFIG.initialVolumeEnabled || !video) return;
        const vol = getDomainInitVol();
        if (video.muted) {
            video.muted = false;
        }
        if (Math.abs(video.volume - vol / 100) > 0.01) {
            video.volume = vol / 100;
        }
        showVolume(vol, video);
    }
    function getVideoElement() {
        updateVideoElements();
        if (activeVideoId) {
            const activeVideo = videoElements.find(v => generateVideoId(v) === activeVideoId);
            if (activeVideo && document.contains(activeVideo)) { cachedVideo = activeVideo; return cachedVideo; }
        }
        if (cachedVideo && document.contains(cachedVideo) && (performance.now() - lastVideoCheck < 300)) { return cachedVideo; }
        const handler = PLATFORM_HANDLERS[PLATFORM] || PLATFORM_HANDLERS.GENERIC;
        cachedVideo = handler.getVideo();
        lastVideoCheck = performance.now();
        if (cachedVideo) {
            const idx = videoElements.indexOf(cachedVideo);
            if (idx === -1) {
                updateVideoElements();
                currentVideoIndex = videoElements.indexOf(cachedVideo);
                if (currentVideoIndex === -1) currentVideoIndex = 0;
                activeVideoId = generateVideoId(cachedVideo);
            } else {
                currentVideoIndex = idx;
                activeVideoId = generateVideoId(cachedVideo);
            }
            applyInitialVolume(cachedVideo);
        }
        return cachedVideo;
    }
    function updateVideoElements() {
        const allVideos = Array.from(document.querySelectorAll('video'));
        videoElements = allVideos.filter(v => {
            const rect = v.getBoundingClientRect();
            if (PLATFORM === 'STEAM') {
                return v.offsetParent !== null && rect.width > 50 && rect.height > 50;
            }
            return v.offsetParent !== null && v.readyState > 0 && rect.width > 50 && rect.height > 50;
        });
        if (videoElements.length === 0 && allVideos.length > 0) {
            videoElements = allVideos;
        }
    }
    function switchToNextVideo() {
        if (cachedVideo && !cachedVideo.paused) {
            cachedVideo.pause();
        }
        updateVideoElements();
        if (videoElements.length < 2) {
            return null;
        }
        currentVideoIndex = (currentVideoIndex + 1) % videoElements.length;
        cachedVideo = videoElements[currentVideoIndex];
        activeVideoId = generateVideoId(cachedVideo);
        lastVideoCheck = performance.now();
        applyInitialVolume(cachedVideo);
        if (PLATFORM === 'STEAM') {
            cachedVideo.focus();
            cachedVideo.click();
        }
        if (cachedVideo.paused && PLATFORM !== 'BILIBILI') {
            cachedVideo.play().catch(() => {});
        }
        showVolume(i18n[LANG].videoSwitched.replace('%d/%d', `${currentVideoIndex + 1}/${videoElements.length}`), cachedVideo);
        return cachedVideo;
    }
    function switchToPrevVideo() {
        if (cachedVideo && !cachedVideo.paused) {
            cachedVideo.pause();
        }
        updateVideoElements();
        if (videoElements.length < 2) {
            return null;
        }
        currentVideoIndex = (currentVideoIndex - 1 + videoElements.length) % videoElements.length;
        cachedVideo = videoElements[currentVideoIndex];
        activeVideoId = generateVideoId(cachedVideo);
        lastVideoCheck = performance.now();
        applyInitialVolume(cachedVideo);
        if (PLATFORM === 'STEAM') {
            cachedVideo.focus();
            cachedVideo.click();
        }
        if (cachedVideo.paused && PLATFORM !== 'BILIBILI') {
            cachedVideo.play().catch(() => {});
        }
        showVolume(i18n[LANG].videoSwitched.replace('%d/%d', `${currentVideoIndex + 1}/${videoElements.length}`), cachedVideo);
        return cachedVideo;
    }
    function clampVolume(vol) { return Math.round(Math.max(0, Math.min(100, vol)) * 100) / 100; }
    function commonAdjustVolume(video, delta) {
        if (!video) return;
        if (video.muted) video.muted = false;
        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;
        saveDomainInitVol(newVolume);
        showVolume(newVolume, video);
        return newVolume;
    }
    let volumeDisplay = null;
    function showVolume(vol, video) {
        if (!volumeDisplay) volumeDisplay = createVolumeDisplay();
        const text = typeof vol === 'string' ? vol : `${Math.round(vol)}%`;
        if (volumeDisplay.textContent === text && volumeDisplay.style.opacity === '1') return;
        volumeDisplay.textContent = text;
        volumeDisplay.style.opacity = '1';
        const now = performance.now();
        if (video && video.isConnected && (now - lastRectUpdate > 100)) {
            const rect = video.getBoundingClientRect();
            volumeDisplay.style.position = 'fixed';
            volumeDisplay.style.left = `${rect.left + rect.width / 2}px`;
            volumeDisplay.style.top = `${rect.top + rect.height / 2}px`;
            volumeDisplay.style.transform = 'translate(-50%, -50%)';
            const fontSize = Math.max(12, Math.min(48, Math.floor(rect.width / 20)));
            volumeDisplay.style.fontSize = `${fontSize}px`;
            lastRectUpdate = now;
        } else if (!video) {
            volumeDisplay.style.position = 'fixed';
            volumeDisplay.style.left = '50%';
            volumeDisplay.style.top = '50%';
            volumeDisplay.style.transform = 'translate(-50%, -50%)';
            volumeDisplay.style.fontSize = '24px';
        }
        if (volumeDisplay.hideTimer) clearTimeout(volumeDisplay.hideTimer);
        volumeDisplay.hideTimer = setTimeout(() => volumeDisplay.style.opacity = '0', 1000);
    }
    function createVolumeDisplay() {
        const display = document.createElement('div');
        display.id = 'dynamic-volume-display';
        Object.assign(display.style, {
            zIndex: 2147483647, padding: '10px 20px', borderRadius: '8px',
            backgroundColor: 'rgba(0, 0, 0, 0.7)', color: '#fff', fontFamily: 'Arial, sans-serif',
            opacity: '0', transition: 'opacity 0.3s', pointerEvents: 'none', textAlign: 'center'
        });
        document.body.appendChild(display);
        return display;
    }
    let isWebFullscreened = false;
    let originalElement = null;
    let originalParent = null;
    let originalStyles = {};
    let originalParentStyles = {};
    let webFullscreenContainer = null;
    function toggleWebFullscreen(video, handler) {
        if (!video) return;
        const fsElement = handler.getFullscreenElement(video);
        if (!fsElement) return;
        if (isWebFullscreened) {
            if (webFullscreenContainer && webFullscreenContainer.contains(fsElement)) { webFullscreenContainer.removeChild(fsElement); }
            if (webFullscreenContainer && document.body.contains(webFullscreenContainer)) { document.body.removeChild(webFullscreenContainer); webFullscreenContainer = null; }
            if (originalParent && !originalParent.contains(fsElement)) { originalParent.appendChild(fsElement); }
            Object.assign(fsElement.style, originalStyles);
            if (originalParent) { Object.assign(originalParent.style, originalParentStyles); }
            isWebFullscreened = false;
            originalElement = null;
            originalParent = null;
        } else {
            originalElement = fsElement;
            originalParent = fsElement.parentElement;
            if (!originalParent) return;
            originalStyles = { position: fsElement.style.position, top: fsElement.style.top, left: fsElement.style.left, width: fsElement.style.width, height: fsElement.style.height, zIndex: fsElement.style.zIndex, objectFit: fsElement.style.objectFit, objectPosition: fsElement.style.objectPosition };
            originalParentStyles = { position: originalParent.style.position, overflow: originalParent.style.overflow };
            if (!webFullscreenContainer) {
                webFullscreenContainer = document.createElement('div');
                webFullscreenContainer.id = 'web-fullscreen-container';
                Object.assign(webFullscreenContainer.style, {
                    position: 'fixed', zIndex: '2147483645', backgroundColor: 'black',
                    display: 'flex', alignItems: 'flex-start', justifyContent: 'center',
                    top: '0', left: '0', width: '100%', height: '100%',
                    margin: '0', padding: '0', overflow: 'hidden'
                });
            }
            Object.assign(originalParent.style, { position: 'static', overflow: 'visible' });
            originalParent.removeChild(fsElement);
            webFullscreenContainer.appendChild(fsElement);
            document.body.appendChild(webFullscreenContainer);
            fsElement.style.position = 'relative';
            fsElement.style.top = ''; fsElement.style.left = '';
            fsElement.style.width = '100%'; fsElement.style.height = '100%';
            fsElement.style.maxHeight = '100vh'; fsElement.style.zIndex = '';
            fsElement.style.objectFit = 'contain'; fsElement.style.objectPosition = 'center';
            isWebFullscreened = true;
        }
    }
    function toggleNativeFullscreen(video) {
        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 simulateAltT() {
        const video = document.querySelector('.video-ref video') || document.querySelector('video');
        if (video) {
            video.focus();
            video.click();
        }
        const eventInit = {
            key: 't',
            code: 'KeyT',
            keyCode: 84,
            which: 84,
            altKey: true,
            bubbles: true,
            cancelable: true
        };
        const keydownEvent = new KeyboardEvent('keydown', eventInit);
        const keyupEvent = new KeyboardEvent('keyup', eventInit);
        const target = video || document.body;
        target.dispatchEvent(keydownEvent);
        setTimeout(() => {
            target.dispatchEvent(keyupEvent);
        }, 50);
    }
    function getEffectiveFullscreenMode(handler) {
        if (CONFIG.fullscreenMode === 1 && !handler.hasNativeFullscreenBtn) {
            return 2;
        }
        return CONFIG.fullscreenMode;
    }
    function getEffectiveDivMulMode(handler) {
        if (CONFIG.longPressDivMulMode === 1 && !handler.hasPlaylist) {
            return 2;
        }
        return CONFIG.longPressDivMulMode;
    }
    const PLATFORM_HANDLERS = {
        YOUTUBE: {
            getVideo: () => {
                const embedVideo = document.querySelector('.html5-main-video.video-stream');
                if (embedVideo) return embedVideo;
                return document.querySelector('video, ytd-player video') || findVideoInIframes();
            },
            getFullscreenElement: (video) => document.querySelector('#movie_player') || video,
            hasNativeFullscreenBtn: true,
            adjustVolume: (video, delta) => {
                const ytPlayer = document.querySelector('#movie_player');
                if (video) video.muted = false;
                if (ytPlayer?.getVolume) {
                    const currentVol = ytPlayer.getVolume();
                    const newVol = clampVolume(currentVol + delta);
                    ytPlayer.setVolume(newVol);
                    if (video) video.volume = newVol / 100;
                    saveDomainInitVol(newVol);
                    showVolume(newVol, video);
                } else if (video) { commonAdjustVolume(video, delta); }
            },
            toggleFullscreen: (video, handler) => {
                const effectiveMode = getEffectiveFullscreenMode(handler);
                const currentVideo = getVideoElement();
                switch (effectiveMode) {
                    case 1: document.querySelector('.ytp-fullscreen-button')?.click(); break;
                    case 2: toggleNativeFullscreen(currentVideo); break;
                    case 3: toggleWebFullscreen(currentVideo, handler); break;
                }
            },
            specialKeys: {
                'Space': () => {},
                'Numpad7': () => document.querySelector('.ytp-prev-button')?.click(),
                'Numpad9': () => document.querySelector('.ytp-next-button')?.click()
            },
            hasPlaylist: true,
            switchPlaylistPrev: () => document.querySelector('.ytp-prev-button')?.click(),
            switchPlaylistNext: () => document.querySelector('.ytp-next-button')?.click()
        },
        BILIBILI: {
            getVideo: () => document.querySelector('.bpx-player-video-wrap video') || findVideoInIframes(),
            getFullscreenElement: (video) => document.querySelector('.bpx-player-container') || video,
            hasNativeFullscreenBtn: true,
            adjustVolume: commonAdjustVolume,
            toggleFullscreen: (video, handler) => {
                const effectiveMode = getEffectiveFullscreenMode(handler);
                const currentVideo = getVideoElement();
                switch (effectiveMode) {
                    case 1: document.querySelector('.bpx-player-ctrl-full')?.click(); break;
                    case 2: toggleNativeFullscreen(currentVideo); break;
                    case 3:
                        const webBtnEnter = document.querySelector('.bpx-player-ctrl-web.bpx-player-ctrl-btn:not(.bpx-state-entered)');
                        const webBtnExit = document.querySelector('.bpx-state-entered.bpx-player-ctrl-web.bpx-player-ctrl-btn');
                        if (webBtnExit) {
                            webBtnExit.click();
                        } else if (webBtnEnter) {
                            webBtnEnter.click();
                        }
                        break;
                }
            },
            specialKeys: {
                'Space': () => {},
                'Numpad2': () => {},
                'Numpad8': () => {},
                'Numpad4': () => {},
                'Numpad6': () => {},
                'Numpad7': () => document.querySelector('.bpx-player-ctrl-prev')?.click(),
                'Numpad9': () => document.querySelector('.bpx-player-ctrl-next')?.click()
            },
            hasPlaylist: true,
            switchPlaylistPrev: () => document.querySelector('.bpx-player-ctrl-prev')?.click(),
            switchPlaylistNext: () => document.querySelector('.bpx-player-ctrl-next')?.click()
        },
        TWITCH: {
            getVideo: () => document.querySelector('.video-ref video') || findVideoInIframes(),
            getFullscreenElement: (video) => video.parentElement || video,
            hasNativeFullscreenBtn: true,
            adjustVolume: commonAdjustVolume,
            toggleFullscreen: (video, handler) => {
                const effectiveMode = getEffectiveFullscreenMode(handler);
                const currentVideo = getVideoElement();
                switch (effectiveMode) {
                    case 1: document.querySelector('[data-a-target="player-fullscreen-button"]')?.click(); break;
                    case 2: toggleNativeFullscreen(currentVideo); break;
                    case 3: simulateAltT(); break;
                }
            },
            specialKeys: {
                'Numpad7': () => simulateKeyPress('ArrowLeft'),
                'Numpad9': () => simulateKeyPress('ArrowRight')
            },
            hasPlaylist: false
        },
        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();
            },
            getFullscreenElement: (video) => {
                return video.closest('.game_hover_activated') || video.parentElement || video;
            },
            hasNativeFullscreenBtn: false,
            adjustVolume: commonAdjustVolume,
            toggleFullscreen: (video, handler) => {
                const effectiveMode = getEffectiveFullscreenMode(handler);
                const currentVideo = getVideoElement();
                if (!currentVideo) return;
                if (effectiveMode === 2) {
                    const container = currentVideo.closest('.game_hover_activated') || currentVideo.parentElement;
                    if (container && !document.fullscreenElement) {
                        container.requestFullscreen?.().catch(() => currentVideo.requestFullscreen?.());
                    } else {
                        document.exitFullscreen?.();
                    }
                } else if (effectiveMode === 3) {
                    toggleWebFullscreen(currentVideo, handler);
                }
            },
            handleWheel: function(e) {
                if (CONFIG.disableWheel || isInputElement(e.target)) return;
                const video = this.getVideo();
                if (!video) return;
                if (!checkScrollRange(e, video)) return;
                e.preventDefault();
                e.stopPropagation();
                const delta = -Math.sign(e.deltaY);
                const modifierDelta = getModifierDelta(e);
                this.adjustVolume(video, delta * modifierDelta);
                showVolume(video.volume * 100, video);
            },
            hasPlaylist: true,
            switchPlaylistPrev: () => {
                const btn = document.querySelector('div.KkkFLdIE4YTPfdeoIQoo2._1EBlS2FQmejekYhQDC-Kmh[data-keepcontrols="true"]');
                if (btn) btn.click();
            },
            switchPlaylistNext: () => {
                const btn = document.querySelector('div.KkkFLdIE4YTPfdeoIQoo2._2XmwxD18W2cEXHlvhYbcyJ[data-keepcontrols="true"]');
                if (btn) btn.click();
            }
        },
        FACEBOOK: {
            getVideo: () => document.querySelector('video[data-video-container]') || document.querySelector('video[playsinline]') || findVideoInIframes(),
            getFullscreenElement: (video) => video.closest('[data-pagelet="MainFeed"]') || video.parentElement || video,
            hasNativeFullscreenBtn: false,
            adjustVolume: commonAdjustVolume,
            toggleFullscreen: (video, handler) => {
                const effectiveMode = getEffectiveFullscreenMode(handler);
                const currentVideo = getVideoElement();
                if (effectiveMode === 2) {
                    toggleNativeFullscreen(currentVideo);
                } else if (effectiveMode === 3) {
                    toggleWebFullscreen(currentVideo, handler);
                }
            },
            hasPlaylist: false
        },
        GENERIC: {
            getVideo: () => document.querySelector('video.player') || findVideoInIframes() || document.querySelector('video, .video-player video, .video-js video, .player-container video'),
            getFullscreenElement: (video) => video,
            hasNativeFullscreenBtn: false,
            adjustVolume: commonAdjustVolume,
            toggleFullscreen: (video, handler) => {
                const effectiveMode = getEffectiveFullscreenMode(handler);
                const currentVideo = getVideoElement();
                if (effectiveMode === 2) {
                    toggleNativeFullscreen(currentVideo);
                } else if (effectiveMode === 3) {
                    toggleWebFullscreen(currentVideo, handler);
                }
            },
            hasPlaylist: false
        }
    };
    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 simulateKeyPress(key) {
        document.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }));
    }
    function isInputElement(target) {
        return /^(INPUT|TEXTAREA|SELECT)$/.test(target.tagName) || target.isContentEditable;
    }
    function checkScrollRange(e, video) {
        if (CONFIG.scrollTriggerRange >= 100) return true;
        const rect = video.getBoundingClientRect();
        const marginX = (rect.width * (100 - CONFIG.scrollTriggerRange)) / 200;
        const marginY = (rect.height * (100 - CONFIG.scrollTriggerRange)) / 200;
        return e.clientX >= rect.left + marginX && e.clientX <= rect.right - marginX &&
            e.clientY >= rect.top + marginY && e.clientY <= rect.bottom - marginY;
    }
    function getModifierDelta(e) {
        if (CONFIG.modifierKey === 5) return CONFIG.stepVolume;
        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]);
        if (e[requiredModifier] && !otherModifiers) {
            return CONFIG.fineVolumeStep;
        }
        return CONFIG.stepVolume;
    }
    const longPressTimers = {};
    const longPressIntervals = {};
    const longPressState = {};
    const prePressState = {};
    let lastKey0Time = 0;
    function getCustomRateIndex(rates, currentRate) {
        let closestIdx = 0;
        let minDiff = Math.abs(rates[0] - currentRate);
        for (let i = 1; i < rates.length; i++) {
            const diff = Math.abs(rates[i] - currentRate);
            if (diff < minDiff) {
                minDiff = diff;
                closestIdx = i;
            }
        }
        return closestIdx;
    }
    function adjustRate13(video, direction) {
        const state = getVideoState(video);
        let newRate = video.playbackRate;
        if (CONFIG.longPress13Mode === 3) {
            const rates = CONFIG.longPress13CustomRates.split(',').map(r => parseFloat(r.trim())).filter(r => !isNaN(r));
            if (rates.length > 0) {
                const idx = getCustomRateIndex(rates, newRate);
                let newIdx = idx + direction;
                if (newIdx < 0) newIdx = 0;
                if (newIdx >= rates.length) newIdx = rates.length - 1;
                newRate = rates[newIdx];
            }
        } else {
            newRate = Math.max(0.1, Math.min(16, newRate + (0.1 * direction)));
        }
        video.playbackRate = parseFloat(newRate.toFixed(1));
        state.lastCustomRate = video.playbackRate;
        state.isDefaultRate = (video.playbackRate === 1.0);
        state.lastManualRate = video.playbackRate;
        showVolume(video.playbackRate * 100, video);
    }
    function startLongPress(key, video, handler) {
        if (longPressTimers[key]) return;
        longPressTimers[key] = setTimeout(() => {
            longPressState[key] = true;
            if (key === 'Numpad4') {
                prePressState[key] = { wasPlaying: !video.paused };
                if (!video.paused) video.pause();
                longPressIntervals[key] = setInterval(() => {
                    if (video) video.currentTime = Math.max(0, video.currentTime - CONFIG.longPress4Step);
                }, CONFIG.longPress4Interval);
            } else if (key === 'Numpad6') {
                prePressState[key] = { wasPlaying: !video.paused };
                if (video.paused) video.play();
                let rate = CONFIG.longPress6StartRate;
                video.playbackRate = rate;
                longPressIntervals[key] = setInterval(() => {
                    rate += CONFIG.longPress6Step;
                    if (CONFIG.longPress6MaxRate > 0 && rate > CONFIG.longPress6MaxRate) rate = CONFIG.longPress6MaxRate;
                    if (video) video.playbackRate = rate;
                }, CONFIG.longPress6Interval);
            } else if (key === 'Numpad5') {
                prePressState[key] = { paused: video.paused, rate: video.playbackRate };
                video.playbackRate = CONFIG.longPress5SlowRate;
                if (video.paused) video.play();
            } else if (key === 'Numpad1' || key === 'Numpad3') {
                const direction = (key === 'Numpad3') ? 1 : -1;
                if (CONFIG.longPress13Mode === 3) {
                    const rates = CONFIG.longPress13CustomRates.split(',').map(r => parseFloat(r.trim())).filter(r => !isNaN(r));
                    if (rates.length > 0) {
                        let idx = getCustomRateIndex(rates, video.playbackRate);
                        longPressIntervals[key] = setInterval(() => {
                            idx += direction;
                            if (idx < 0) idx = 0;
                            if (idx >= rates.length) idx = rates.length - 1;
                            if (video) {
                                video.playbackRate = rates[idx];
                                showVolume(video.playbackRate * 100, video);
                            }
                        }, CONFIG.longPress13Interval);
                    }
                } else if (CONFIG.longPress13Mode === 2) {
                    let rate = video.playbackRate;
                    longPressIntervals[key] = setInterval(() => {
                        let step = 0.1;
                        if (rate >= 2.0 && rate < 3.0) step = 0.2;
                        else if (rate >= 3.0) step = 1.0;
                        rate = Math.max(0.1, Math.min(16, rate + (step * direction)));
                        if (video) {
                            video.playbackRate = rate;
                            showVolume(rate * 100, video);
                        }
                    }, CONFIG.longPress13Interval);
                } else {
                    let rate = video.playbackRate;
                    longPressIntervals[key] = setInterval(() => {
                        rate = Math.max(0.1, Math.min(16, rate + (0.1 * direction)));
                        if (video) {
                            video.playbackRate = rate;
                            showVolume(rate * 100, video);
                        }
                    }, CONFIG.longPress13Interval);
                }
            } else if (key === 'Numpad7' || key === 'Numpad9') {
                const direction = (key === 'Numpad9') ? 1 : -1;
                const mode = CONFIG.longPress79Mode;
                prePressState[key] = { wasPlaying: !video.paused };
                if (video.paused) video.play();
                if (mode === 1) {
                    const step = CONFIG.stepTimeLong * CONFIG.longPress79Multiplier;
                    longPressIntervals[key] = setInterval(() => {
                        if (video) video.currentTime += direction * step;
                    }, CONFIG.longPress79Interval);
                } else if (mode === 2 && handler.hasPlaylist) {
                    if (handler.specialKeys?.[key]) {
                        handler.specialKeys[key]();
                    }
                } else if (mode === 3) {
                    if (direction === 1) {
                        history.forward();
                    } else {
                        history.back();
                    }
                }
            }
        }, CONFIG.longPressDetectTime);
    }
    function cancelLongPress(key, video) {
        if (longPressTimers[key]) {
            clearTimeout(longPressTimers[key]);
            delete longPressTimers[key];
            if (!longPressState[key]) {
                handleShortPress(key, video);
            }
        }
        if (longPressState[key]) {
            if (longPressIntervals[key]) {
                clearInterval(longPressIntervals[key]);
                delete longPressIntervals[key];
            }
            if (key === 'Numpad4' && prePressState[key]) {
                if (prePressState[key].wasPlaying && video.paused) video.play();
                delete prePressState[key];
            } else if (key === 'Numpad6' && prePressState[key]) {
                if (!prePressState[key].wasPlaying && !video.paused) video.pause();
                if (video) video.playbackRate = 1.0;
                delete prePressState[key];
            } else if (key === 'Numpad5' && prePressState[key]) {
                const state = getVideoState(video);
                const restoreRate = state.lastManualRate !== undefined ? state.lastManualRate : 1.0;
                video.playbackRate = restoreRate;
                if (prePressState[key].paused) { video.pause(); } else { video.play(); }
                delete prePressState[key];
            } else if (key === 'Numpad1' || key === 'Numpad3') {
                const state = getVideoState(video);
                state.lastCustomRate = video.playbackRate;
                state.isDefaultRate = (video.playbackRate === 1.0);
            } else if (key === 'Numpad7' || key === 'Numpad9') {
                if (prePressState[key]) {
                    if (!prePressState[key].wasPlaying && !video.paused) video.pause();
                    delete prePressState[key];
                }
            }
            delete longPressState[key];
        }
    }
    function handleShortPress(key, video) {
        if (!video) return;
        if (key === 'Numpad4') video.currentTime = Math.max(0, video.currentTime - CONFIG.stepTime);
        if (key === 'Numpad6') video.currentTime += CONFIG.stepTime;
        if (key === 'Numpad7') video.currentTime -= CONFIG.stepTimeLong;
        if (key === 'Numpad9') video.currentTime += CONFIG.stepTimeLong;
        if (key === 'Numpad1' || key === 'Numpad3') {
            adjustRate13(video, key === 'Numpad3' ? 1 : -1);
        }
        if (key === 'Numpad5' || key === 'Space') video && video[video.paused ? 'play' : 'pause']();
    }
    function handleDivMulAction(key, handler) {
        const effectiveMode = getEffectiveDivMulMode(handler);
        const direction = (key === 'NumpadMultiply') ? 1 : -1;
        if (effectiveMode === 1 && handler.hasPlaylist) {
            if (direction === 1) {
                handler.switchPlaylistNext?.();
            } else {
                handler.switchPlaylistPrev?.();
            }
        } else if (effectiveMode === 2 || !handler.hasPlaylist) {
            if (direction === 1) {
                switchToNextVideo();
            } else {
                switchToPrevVideo();
            }
        } else if (effectiveMode === 3) {
            if (direction === 1) {
                history.forward();
            } else {
                history.back();
            }
        }
    }
    function handleKeyEvent(e) {
        if (CONFIG.disableKeyboard || isInputElement(e.target)) return;
        if (PLATFORM === "FACEBOOK" && e.code === "NumpadDivide") {
            e.preventDefault();
            e.stopPropagation();
        }
        const keyMap = {
            'Numpad0': 0, 'Numpad1': 1, 'Numpad2': 2, 'Numpad3': 3, 'Numpad4': 4, 'Numpad5': 5,
            'Numpad6': 6, 'Numpad7': 7, 'Numpad8': 8, 'Numpad9': 9, 'Space': 10, 'Enter': 11,
            'NumpadAdd': 12, 'NumpadSubtract': 13, 'ArrowLeft': 14, 'ArrowRight': 15, 'ArrowUp': 16, 'ArrowDown': 17,
            'NumpadDivide': 18, 'NumpadMultiply': 19
        };
        let keyId = keyMap[e.code];
        if (CONFIG.disabledKeys.includes(keyId)) return;
        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;
        const longPressKeys = ['Numpad4', 'Numpad6', 'Numpad1', 'Numpad3', 'Numpad7', 'Numpad9', 'Numpad5'];
        if (longPressKeys.includes(e.code)) {
            const video = getVideoElement();
            if (e.type === 'keydown') {
                startLongPress(e.code, video, handler);
                e.preventDefault();
            } else if (e.type === 'keyup') {
                cancelLongPress(e.code, video);
                e.preventDefault();
            }
            return;
        }
        if (e.type !== 'keydown') return;
        if (handler.specialKeys?.[e.code]) { handler.specialKeys[e.code](); e.preventDefault(); return; }
        if (e.code === 'NumpadDivide' || e.code === 'NumpadMultiply') {
            e.preventDefault();
            handleDivMulAction(e.code, handler);
            return;
        }
        if (e.code === 'Numpad0') {
            const now = performance.now();
            if (now - lastKey0Time < 300) { e.preventDefault(); return; }
            lastKey0Time = now;
            const video = getVideoElement();
            if (video) togglePlaybackRate(video);
            e.preventDefault();
            return;
        }
        if (e.code === 'Enter' || e.code === 'NumpadEnter') {
            const currentVideo = getVideoElement();
            handler.toggleFullscreen(currentVideo, handler);
            e.preventDefault();
            return;
        }
        const video = getVideoElement();
        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; }
        }
        const actions = {
            'NumpadAdd': () => video && (video.currentTime += video.duration * 0.1),
            'NumpadSubtract': () => video && (video.currentTime -= video.duration * 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 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;
        }
        state.lastManualRate = video.playbackRate;
        showVolume(video.playbackRate * 100, video);
    }
    function handleVideoWheel(e) {
        if (CONFIG.disableWheel || isInputElement(e.target)) return;
        const video = e.target.closest('video');
        if (!video) return;
        if (!checkScrollRange(e, video)) return;
        e.preventDefault();
        e.stopPropagation();
        const normalizedDelta = -Math.sign(e.deltaY);
        const modifierDelta = getModifierDelta(e);
        PLATFORM_HANDLERS[PLATFORM].adjustVolume(video, normalizedDelta * modifierDelta);
    }
    function handleTwitchWheel(e) {
        if (CONFIG.disableWheel || isInputElement(e.target)) return;
        const video = getVideoElement();
        if (!video) return;
        if (!checkScrollRange(e, video)) return;
        e.preventDefault();
        e.stopPropagation();
        const delta = -Math.sign(e.deltaY);
        const modifierDelta = getModifierDelta(e);
        const volumeChange = delta * modifierDelta;
        PLATFORM_HANDLERS.TWITCH.adjustVolume(video, volumeChange);
        showVolume(video.volume * 100, video);
    }
    function setupEventDelegation() {
        document.addEventListener('wheel', (e) => {
            if (PLATFORM === 'TWITCH') {
                handleTwitchWheel(e);
            } else if (PLATFORM === 'STEAM') {
                PLATFORM_HANDLERS.STEAM.handleWheel(e);
            } else if (PLATFORM === 'YOUTUBE') {
                const embedVideo = document.querySelector('.html5-main-video.video-stream');
                if (embedVideo && e.target.closest('.html5-main-video, .ytp-chrome-bottom, .player-controls-content, .player-controls-background')) {
                    if (CONFIG.disableWheel || isInputElement(e.target)) return;
                    if (!checkScrollRange(e, embedVideo)) return;
                    e.preventDefault();
                    e.stopPropagation();
                    const delta = -Math.sign(e.deltaY);
                    const modifierDelta = getModifierDelta(e);
                    PLATFORM_HANDLERS.YOUTUBE.adjustVolume(embedVideo, delta * modifierDelta);
                }
            } else {
                handleVideoWheel(e);
            }
        }, { passive: false, capture: true });
        document.addEventListener('click', (e) => {
            const video = e.target.closest('video');
            if (video && PLATFORM !== 'BILIBILI') {
                setActiveVideo(video, false);
            }
        }, true);
    }
    function init() {
        loadConfig();
        GM_registerMenuCommand(i18n[LANG].menuTitle, openMainMenu);
        document.addEventListener('keydown', handleKeyEvent, true);
        document.addEventListener('keyup', handleKeyEvent, true);
        setupEventDelegation();
        const observer = new MutationObserver(() => {
            if (observer.timer) clearTimeout(observer.timer);
            observer.timer = setTimeout(() => {
                updateVideoElements();
            }, 200);
        });
        observer.observe(document.body, { childList: true, subtree: true });
        setInterval(() => {
            updateVideoElements();
        }, 5000);
        updateVideoElements();
    }
    init();
})();