Universal Video Temp Speed + Double-Tap 6×/4×

Double-tap , → 6× | Double-tap . → 4× | Hold , → 3× | Hold . → 2× | Full-screen OK

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Universal Video Temp Speed + Double-Tap 6×/4×
// @namespace    http://tampermonkey.net/
// @version      1.9
// @description  Double-tap , → 6× | Double-tap . → 4× | Hold , → 3× | Hold . → 2× | Full-screen OK
// @author       LGJA
// @match        *://*/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    const CONFIG = {
        MIN_SPEED: 0.10,
        MAX_SPEED: 16.0,
        SENSITIVITY: 150,
        TEMP_3X: 3.0,     // Hold ,
        TEMP_2X: 2.0,     // Hold .
        DOUBLE_6X: 6.0,   // Double-tap ,
        DOUBLE_4X: 4.0    // Double-tap .
    };

    let isDragging = false;
    let isTempSpeed = false;
    let originalSpeed = 1;
    let startX = 0;
    let startSpeed = 1;
    let lastTap = 0;
    let holdTimeout = null;
    let customSpeed = 1;
    let isHolding = false;
    let tempKey = null;
    let lastKey = null;
    let tapCount = 0;
    let doubleTapTimer = null;
    let badge = null;

    function getVideo() {
        return document.querySelector('video');
    }

    function getContainer() {
        const video = getVideo();
        if (!video) return null;
        return video.closest('div, section, [class*="player"], [class*="container"]') || document.body;
    }

    function ensureBadge() {
        if (badge) return badge;
        badge = document.createElement('div');
        badge.id = 'universal-speed-badge';
        Object.assign(badge.style, {
            position: 'fixed',
            top: '10%',
            left: '50%',
            transform: 'translateX(-50%)',
            background: 'rgba(0, 0, 0, 0.75)',
            color: '#fff',
            font: '500 14px/1 "YouTube Noto", Roboto, Arial, sans-serif',
            padding: '6px 12px',
            borderRadius: '4px',
            zIndex: '2147483647',
            pointerEvents: 'none',
            opacity: '0',
            transition: 'opacity 0.15s ease',
            whiteSpace: 'nowrap',
            boxShadow: '0 2px 8px rgba(0,0,0,0.3)'
        });
        return badge;
    }

    function showBadge(speed) {
        ensureBadge();
        updateBadgeParent();

        const display = speed === CONFIG.DOUBLE_6X ? '6×' :
                       speed === CONFIG.DOUBLE_4X ? '4×' :
                       speed === CONFIG.TEMP_3X ? '3×' :
                       speed === CONFIG.TEMP_2X ? '2×' :
                       `${speed.toFixed(1)}×`;

        badge.textContent = display;
        badge.style.opacity = '1';
    }

    function hideBadge() {
        if (badge) badge.style.opacity = '0';
    }

    function updateBadgeParent() {
        if (!badge) return;
        const container = getContainer();
        if (container && badge.parentElement !== container) {
            container.appendChild(badge);
        }
    }

    function setSpeed(speed) {
        const video = getVideo();
        if (!video) return;
        speed = Math.max(CONFIG.MIN_SPEED, Math.min(CONFIG.MAX_SPEED, speed));
        video.playbackRate = speed;
        if (isTempSpeed) showBadge(speed);
    }

    // ——— TEMP SPEED CONTROL ———
    const startTempSpeed = (multiplier) => {
        const video = getVideo();
        if (!video || isTempSpeed) return;
        originalSpeed = video.playbackRate;
        isTempSpeed = true;
        setSpeed(originalSpeed * multiplier);
    };

    const endTempSpeed = () => {
        if (!isTempSpeed) return;
        isTempSpeed = false;
        setSpeed(originalSpeed);
        hideBadge();
    };

    // ——— KEY HANDLING WITH DOUBLE-TAP ———
    const onKeyDown = (e) => {
        if (isEditable(document.activeElement) || isDragging) return;
        const video = getVideo();
        if (!video) return;

        const now = Date.now();
        const key = e.key;

        if (key === ',' || key === '.') {
            e.preventDefault();

            // Double-tap detection
            if (lastKey === key && now - lastTap < 300) {
                tapCount++;
                clearTimeout(doubleTapTimer);
            } else {
                tapCount = 1;
                lastKey = key;
            }

            lastTap = now;

            doubleTapTimer = setTimeout(() => {
                if (tapCount >= 2) {
                    // DOUBLE-TAP: 6× or 4×
                    tempKey = null;
                    if (key === ',') {
                        startTempSpeed(CONFIG.DOUBLE_6X);
                    } else if (key === '.') {
                        startTempSpeed(CONFIG.DOUBLE_4X);
                    }
                } else {
                    // SINGLE PRESS & HOLD: 3× or 2×
                    if (!tempKey) {
                        tempKey = key;
                        if (key === ',') {
                            startTempSpeed(CONFIG.TEMP_3X);
                        } else if (key === '.') {
                            startTempSpeed(CONFIG.TEMP_2X);
                        }
                    }
                }
                tapCount = 0;
            }, 300);
        }
    };

    const onKeyUp = (e) => {
        if (e.key === ',' || e.key === '.') {
            if (tempKey === e.key || (tapCount >= 2 && lastKey === e.key)) {
                endTempSpeed();
                tempKey = null;
                clearTimeout(doubleTapTimer);
                tapCount = 0;
            }
        }
    };

    // ——— DRAG TO SET SPEED ———
    const onMouseDown = e => {
        const video = getVideo();
        if (!video || isTempSpeed) return;
        const container = e.target.closest('video') || e.target.closest('div, section, [class*="player"]');
        if (!container || !container.querySelector('video')) return;

        const now = Date.now();
        if (now - lastTap < 300) {
            isDragging = true;
            startX = e.clientX;
            startSpeed = video.playbackRate;
            customSpeed = startSpeed * 2;
            holdTimeout = setTimeout(() => {
                isHolding = true;
                setSpeed(customSpeed);
            }, 400);
            e.preventDefault();
        } else {
            isDragging = true;
            startX = e.clientX;
            startSpeed = video.playbackRate;
        }
        lastTap = now;
    };

    const onMouseMove = e => {
        if (!isDragging || isTempSpeed) return;
        const delta = e.clientX - startX;
        let newSpeed = startSpeed + delta / CONFIG.SENSITIVITY;
        newSpeed = Math.max(CONFIG.MIN_SPEED, Math.min(CONFIG.MAX_SPEED, newSpeed));
        if (isHolding) customSpeed = newSpeed;
        else setSpeed(newSpeed);
    };

    const onMouseUp = () => {
        if (isDragging && holdTimeout) clearTimeout(holdTimeout);
        isDragging = false;
        isHolding = false;
    };

    // ——— INPUT DETECTION ———
    const isEditable = el => {
        if (!el) return false;
        const tag = el.tagName?.toUpperCase();
        return (tag === 'INPUT' && !['checkbox', 'radio'].includes(el.type)) ||
               tag === 'TEXTAREA' ||
               el.isContentEditable;
    };

    // ——— EVENT LISTENERS ———
    document.addEventListener('keydown', onKeyDown, true);
    document.addEventListener('keyup', onKeyUp, true);
    document.addEventListener('mousedown', onMouseDown, true);
    document.addEventListener('mousemove', onMouseMove, true);
    document.addEventListener('mouseup', onMouseUp, true);

    // ——— FULL-SCREEN & DOM OBSERVER ———
    const observer = new MutationObserver(updateBadgeParent);
    observer.observe(document.body, { childList: true, subtree: true });

    document.addEventListener('fullscreenchange', () => {
        setTimeout(updateBadgeParent, 100);
    });

    // Initial setup
    ensureBadge();
    updateBadgeParent();

})();