TimerHooker (English, Modern UI)

Toggle 1x/3x timer speed. UI docks at edge, undocks on interaction, draggable, 5S code, fully adaptive to light/dark mode.

// ==UserScript==
// @name         TimerHooker (English, Modern UI)
// @version      4.1.1
// @description  Toggle 1x/3x timer speed. UI docks at edge, undocks on interaction, draggable, 5S code, fully adaptive to light/dark mode.
// @include      *
// @match        http://*/*
// @match        https://*/*
// @require      https://gf.qytechs.cn/scripts/372672-everything-hook/code/Everything-Hook.js?version=881251
// @author       Tiger 27, Perplexity AI
// @run-at       document-start
// @grant        none
// @license      MIT
// @namespace https://gf.qytechs.cn/users/1356925
// ==/UserScript==

(function (global) {
    'use strict';

    /*** 5S: SORT - Group related functions and variables together ***/

    // --- UI Constants ---
    const UI = {
        BTN_SIZE: 64,
        CIRCLE_SIZE: 56,
        ICON_SIZE: 36,
        DOCK_OPACITY: 0.6,
        UNDOCK_OPACITY: 1,
        DOCK_TIMEOUT: 3000, // ms
        DOCK_MARGIN: 10,
        INIT_TOP: 0.2, // 20% from top
    };

    // --- Timer Constants ---
    const SPEED_NORMAL = 1.0;
    const SPEED_FAST = 1 / 3; // 3x faster (intervals are 1/3 original)

    /*** 5S: SET IN ORDER - Clear naming, logical order, modularity ***/

    // --- Timer Context ---
    const timerContext = {
        _intervalIds: {},
        _timeoutIds: {},
        _uniqueId: 1,
        __percentage: SPEED_NORMAL,
        _setInterval: window.setInterval,
        _clearInterval: window.clearInterval,
        _setTimeout: window.setTimeout,
        _clearTimeout: window.clearTimeout,
        _Date: window.Date,
        __lastDatetime: Date.now(),
        __lastMDatetime: Date.now(),
        genUniqueId() { return this._uniqueId++; },
        notifyExec(uniqueId) {
            if (!uniqueId) return;
            Object.values(this._timeoutIds)
                .filter(info => info.uniqueId === uniqueId)
                .forEach(info => {
                    this._clearTimeout.call(window, info.nowId);
                    delete this._timeoutIds[info.originId];
                });
        },
        get _percentage() { return this.__percentage; },
        set _percentage(val) {
            if (val === this.__percentage) return;
            percentageChangeHandler(val, this);
            this.__percentage = val;
        }
    };

    // --- Global Timer API ---
    global.timer = {
        change(percentage) {
            timerContext.__lastMDatetime = timerContext._mDate.now();
            timerContext.__lastDatetime = timerContext._Date.now();
            timerContext._percentage = percentage;
        }
    };

    /*** 5S: SHINE - Keep code clean, readable, and well-commented ***/

    // --- UI Creation ---
    function createStyles() {
        const style = `
        :root {
            --th-bg-light: rgba(245,245,245,0.95);
            --th-bg-dark: rgba(30,30,30,0.95);
            --th-fg-light: #222;
            --th-fg-dark: #fafafa;
            --th-shadow: 0 2px 12px 0 rgba(0,0,0,0.20);
            --th-accent: #4e91ff;
        }
        @media (prefers-color-scheme: dark) {
            :root {
                --th-bg: var(--th-bg-dark);
                --th-fg: var(--th-fg-dark);
            }
        }
        @media (prefers-color-scheme: light) {
            :root {
                --th-bg: var(--th-bg-light);
                --th-fg: var(--th-fg-light);
            }
        }
        .th-move-btn {
            position: fixed;
            left: 0; top: 20%;
            z-index: 100000;
            background: none;
            border: none;
            outline: none;
            box-shadow: none;
            cursor: grab;
            padding: 0;
            margin: 0;
            width: ${UI.BTN_SIZE}px; height: ${UI.BTN_SIZE}px;
            display: flex;
            align-items: center;
            justify-content: center;
            opacity: ${UI.DOCK_OPACITY};
            border-radius: 50%;
            user-select: none;
            transition: left 0.4s cubic-bezier(.4,2,.6,1), right 0.4s cubic-bezier(.4,2,.6,1), opacity 0.2s, transform 0.4s cubic-bezier(.4,2,.6,1);
            transform: translateX(-50%);
        }
        .th-move-btn.undocked {
            opacity: ${UI.UNDOCK_OPACITY} !important;
            transform: translateX(0) !important;
        }
        .th-move-btn:active {
            cursor: grabbing;
            filter: brightness(0.85);
        }
        .th-circle {
            width: ${UI.CIRCLE_SIZE}px; height: ${UI.CIRCLE_SIZE}px;
            border-radius: 50%;
            background: var(--th-bg, #eee);
            box-shadow: var(--th-shadow);
            display: flex;
            align-items: center;
            justify-content: center;
            pointer-events: none;
            position: absolute;
            left: 4px; top: 4px;
            transition: background 0.3s;
        }
        .th-icon {
            width: ${UI.ICON_SIZE}px; height: ${UI.ICON_SIZE}px;
            display: block;
            fill: var(--th-fg, #222);
            pointer-events: none;
            user-select: none;
            position: relative;
            transition: fill 0.3s;
        }
        `;
        const stylenode = document.createElement('style');
        stylenode.type = "text/css";
        stylenode.appendChild(document.createTextNode(style));
        document.head.appendChild(stylenode);
    }

    function getIconSVG(isFast) {
        // Play icon for 1x, lightning for 3x
        return isFast
            ? `<svg class="th-icon" viewBox="0 0 48 48"><polygon points="20,7 42,24 28,24 28,41 6,24 20,24" style="fill:var(--th-accent,#4e91ff)"/><polygon points="20,7 42,24 28,24 28,41 6,24 20,24" style="fill-opacity:0.3;fill:var(--th-fg,#fafafa)"/></svg>`
            : `<svg class="th-icon" viewBox="0 0 48 48"><polygon points="15,10 39,24 15,38"/></svg>`;
    }

    function createUI() {
        createStyles();
        const html = `
            <button class="th-move-btn" id="th_move_btn" type="button">
                <span class="th-circle"></span>
                <span id="th_icon_container"></span>
            </button>
        `;
        const node = document.createElement('div');
        node.innerHTML = html;
        document.body.appendChild(node);

        // --- UI State ---
        const moveBtn = document.getElementById('th_move_btn');
        const iconContainer = document.getElementById('th_icon_container');
        let isFast = false;
        let isDragging = false;
        let dragStartX = 0, dragStartY = 0;
        let origLeft = 0, origTop = 0;
        let dockedSide = 'left'; // or 'right'
        let docked = true;
        let hideTimeout;

        // --- UI Functions ---
        function dockUI() {
            docked = true;
            moveBtn.classList.remove('undocked');
            moveBtn.style.opacity = UI.DOCK_OPACITY;
            moveBtn.style.top = (parseFloat(moveBtn.style.top) || window.innerHeight * UI.INIT_TOP) + 'px';
            if (dockedSide === 'left') {
                moveBtn.style.left = '0px';
                moveBtn.style.right = 'auto';
                moveBtn.style.transform = 'translateX(-50%)';
            } else {
                moveBtn.style.right = '0px';
                moveBtn.style.left = 'auto';
                moveBtn.style.transform = 'translateX(50%)';
            }
        }
        function undockUI() {
            docked = false;
            moveBtn.classList.add('undocked');
            moveBtn.style.opacity = UI.UNDOCK_OPACITY;
            moveBtn.style.transform = 'translateX(0)';
            moveBtn.style.top = (parseFloat(moveBtn.style.top) || window.innerHeight * UI.INIT_TOP) + 'px';
            if (dockedSide === 'left') {
                moveBtn.style.left = '0px';
                moveBtn.style.right = 'auto';
            } else {
                moveBtn.style.right = '0px';
                moveBtn.style.left = 'auto';
            }
        }
        function scheduleDock() {
            clearTimeout(hideTimeout);
            hideTimeout = setTimeout(() => {
                // Find closest edge
                const rect = moveBtn.getBoundingClientRect();
                const centerX = rect.left + rect.width / 2;
                dockedSide = (centerX < window.innerWidth / 2) ? 'left' : 'right';
                dockUI();
            }, UI.DOCK_TIMEOUT);
        }
        function onInteraction() {
            if (docked) undockUI();
            scheduleDock();
        }
        function setSpeed(fast) {
            isFast = fast;
            iconContainer.innerHTML = getIconSVG(isFast);
            global.timer.change(isFast ? SPEED_FAST : SPEED_NORMAL);
            onInteraction();
        }

        // --- UI Event Listeners ---
        moveBtn.addEventListener('mousedown', e => {
            if (e.button !== 0) return;
            isDragging = true;
            dragStartX = e.clientX;
            dragStartY = e.clientY;
            origLeft = parseFloat(moveBtn.style.left) || 0;
            origTop = parseFloat(moveBtn.style.top) || window.innerHeight * UI.INIT_TOP;
            document.body.style.userSelect = "none";
            onInteraction();
        });
        document.addEventListener('mousemove', e => {
            if (!isDragging) return;
            const dx = e.clientX - dragStartX;
            const dy = e.clientY - dragStartY;
            const maxLeft = window.innerWidth - moveBtn.offsetWidth - UI.DOCK_MARGIN;
            const maxTop = window.innerHeight - moveBtn.offsetHeight - UI.DOCK_MARGIN;
            let newLeft = origLeft + dx;
            let newTop = origTop + dy;
            newLeft = Math.min(Math.max(newLeft, UI.DOCK_MARGIN), maxLeft);
            newTop = Math.min(Math.max(newTop, UI.DOCK_MARGIN), maxTop);
            moveBtn.classList.add('undocked');
            moveBtn.style.opacity = UI.UNDOCK_OPACITY;
            moveBtn.style.transform = 'translateX(0)';
            moveBtn.style.left = newLeft + 'px';
            moveBtn.style.right = 'auto';
            moveBtn.style.top = newTop + 'px';
            docked = false;
            scheduleDock();
        });
        document.addEventListener('mouseup', () => {
            if (!isDragging) return;
            isDragging = false;
            document.body.style.userSelect = "";
            scheduleDock();
        });
        moveBtn.addEventListener('touchstart', e => {
            isDragging = true;
            const touch = e.touches[0];
            dragStartX = touch.clientX;
            dragStartY = touch.clientY;
            origLeft = parseFloat(moveBtn.style.left) || 0;
            origTop = parseFloat(moveBtn.style.top) || window.innerHeight * UI.INIT_TOP;
            document.body.style.userSelect = "none";
            onInteraction();
        });
        document.addEventListener('touchmove', e => {
            if (!isDragging) return;
            const touch = e.touches[0];
            const dx = touch.clientX - dragStartX;
            const dy = touch.clientY - dragStartY;
            const maxLeft = window.innerWidth - moveBtn.offsetWidth - UI.DOCK_MARGIN;
            const maxTop = window.innerHeight - moveBtn.offsetHeight - UI.DOCK_MARGIN;
            let newLeft = origLeft + dx;
            let newTop = origTop + dy;
            newLeft = Math.min(Math.max(newLeft, UI.DOCK_MARGIN), maxLeft);
            newTop = Math.min(Math.max(newTop, UI.DOCK_MARGIN), maxTop);
            moveBtn.classList.add('undocked');
            moveBtn.style.opacity = UI.UNDOCK_OPACITY;
            moveBtn.style.transform = 'translateX(0)';
            moveBtn.style.left = newLeft + 'px';
            moveBtn.style.right = 'auto';
            moveBtn.style.top = newTop + 'px';
            docked = false;
            scheduleDock();
        }, { passive: false });
        document.addEventListener('touchend', () => {
            if (!isDragging) return;
            isDragging = false;
            document.body.style.userSelect = "";
            scheduleDock();
        });

        moveBtn.addEventListener('click', () => {
            if (isDragging) return;
            setSpeed(!isFast);
        });

        ['mouseenter', 'touchstart', 'mousedown'].forEach(ev => {
            moveBtn.addEventListener(ev, onInteraction);
        });

        // --- UI Initial State ---
        dockedSide = 'left';
        moveBtn.style.left = '0px';
        moveBtn.style.right = 'auto';
        moveBtn.style.top = window.innerHeight * UI.INIT_TOP + 'px';
        moveBtn.style.transform = 'translateX(-50%)';
        moveBtn.style.opacity = UI.DOCK_OPACITY;
        docked = true;
        scheduleDock();
        setTimeout(() => setSpeed(false), 100);
    }

    /*** 5S: STANDARDIZE - Use clear patterns for hooking and timer management ***/

    function applyHooking(ctx) {
        const eHookContext = global.eHook;
        eHookContext.hookReplace(window, 'setInterval', setInterval => getHookedTimerFunction('interval', setInterval, ctx));
        eHookContext.hookReplace(window, 'setTimeout', setTimeout => getHookedTimerFunction('timeout', setTimeout, ctx));
        eHookContext.hookBefore(window, 'clearInterval', (method, args) => redirectNewestId(args, ctx));
        eHookContext.hookBefore(window, 'clearTimeout', (method, args) => redirectNewestId(args, ctx));
        eHookContext.hookClass(window, 'Date', getHookedDateConstructor(ctx), '_innerDate', ['now']);
        Date.now = () => new Date().getTime();
        ctx._mDate = window.Date;
    }

    function getHookedDateConstructor(ctx) {
        return function (...args) {
            if (args.length === 1) {
                Object.defineProperty(this, '_innerDate', {
                    configurable: false, enumerable: false,
                    value: new ctx._Date(args[0]), writable: false
                });
                return;
            } else if (args.length > 1) {
                let definedValue;
                switch (args.length) {
                    case 2: definedValue = new ctx._Date(args[0], args[1]); break;
                    case 3: definedValue = new ctx._Date(args[0], args[1], args[2]); break;
                    case 4: definedValue = new ctx._Date(args[0], args[1], args[2], args[3]); break;
                    case 5: definedValue = new ctx._Date(args[0], args[1], args[2], args[3], args[4]); break;
                    case 6: definedValue = new ctx._Date(args[0], args[1], args[2], args[3], args[4], args[5]); break;
                    default: definedValue = new ctx._Date(args[0], args[1], args[2], args[3], args[4], args[5], args[6]);
                }
                Object.defineProperty(this, '_innerDate', {
                    configurable: false, enumerable: false,
                    value: definedValue, writable: false
                });
                return;
            }
            const now = ctx._Date.now();
            const passTime = now - ctx.__lastDatetime;
            const hookPassTime = passTime * (1 / ctx._percentage);
            Object.defineProperty(this, '_innerDate', {
                configurable: false, enumerable: false,
                value: new ctx._Date(ctx.__lastMDatetime + hookPassTime), writable: false
            });
        };
    }

    function getHookedTimerFunction(type, timer, ctx) {
        const property = '_' + type + 'Ids';
        return function (...args) {
            const uniqueId = ctx.genUniqueId();
            let callback = args[0];
            if (typeof callback === 'string') {
                callback += `;timer.notifyExec(${uniqueId})`;
                args[0] = callback;
            }
            if (typeof callback === 'function') {
                args[0] = function () {
                    const returnValue = callback.apply(this, arguments);
                    ctx.notifyExec(uniqueId);
                    return returnValue;
                };
            }
            const originMS = args[1];
            args[1] *= ctx._percentage;
            const resultId = timer.apply(window, args);
            ctx[property][resultId] = {
                args,
                originMS,
                originId: resultId,
                nowId: resultId,
                uniqueId,
                oldPercentage: ctx._percentage,
                exceptNextFireTime: ctx._Date.now() + originMS,
            };
            return resultId;
        };
    }

    function redirectNewestId(args, ctx) {
        const id = args[0];
        if (ctx._intervalIds[id]) {
            args[0] = ctx._intervalIds[id].nowId;
            delete ctx._intervalIds[id];
        }
        if (ctx._timeoutIds[id]) {
            args[0] = ctx._timeoutIds[id].nowId;
            delete ctx._timeoutIds[id];
        }
    }

    function percentageChangeHandler(percentage, ctx) {
        Object.values(ctx._intervalIds).forEach(idObj => {
            idObj.args[1] = Math.floor((idObj.originMS || 1) * percentage);
            ctx._clearInterval.call(window, idObj.nowId);
            idObj.nowId = ctx._setInterval.apply(window, idObj.args);
        });
        Object.values(ctx._timeoutIds).forEach(idObj => {
            const now = ctx._Date.now();
            let time = idObj.exceptNextFireTime - now;
            if (time < 0) time = 0;
            const changedTime = Math.floor((percentage / idObj.oldPercentage) * time);
            idObj.args[1] = changedTime;
            idObj.exceptNextFireTime = now + changedTime;
            idObj.oldPercentage = percentage;
            ctx._clearTimeout.call(window, idObj.nowId);
            idObj.nowId = ctx._setTimeout.apply(window, idObj.args);
        });
    }

    /*** 5S: SUSTAIN - Keep code maintainable, modular, and documented ***/

    function main() {
        applyHooking(timerContext);
        if (document.readyState === 'complete' || document.readyState === 'interactive') {
            createUI();
        } else {
            document.addEventListener('DOMContentLoaded', createUI);
        }
    }

    if (global.eHook) main();
})(window);

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址