Scroll Volume Dx Edition

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.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==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();
})();