TimerHooker Mobile (Standalone)

Control page timers, video speed, skip ads – mobile friendly with 10x button.

当前为 2026-05-24 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         TimerHooker Mobile (Standalone)
// @namespace    http://tampermonkey.net/
// @version      1.4.0
// @description  Control page timers, video speed, skip ads – mobile friendly with 10x button.
// @author       Tiger 27 (adapted for Safari)
// @match        *://*/*
// @grant        none
// @run-at       document-start
// @inject-into  content
// ==/UserScript==

(function() {
    'use strict';

    // ---- 1. Timer Hooking Logic (standalone) ----
    let originalSetTimeout = window.setTimeout;
    let originalClearTimeout = window.clearTimeout;
    let originalSetInterval = window.setInterval;
    let originalClearInterval = window.clearInterval;
    let originalDateNow = Date.now;
    let originalDateConstructor = Date;

    let speedFactor = 1.0;        // multiplier: 1 = normal, 0.5 = half speed, 2 = double speed
    let targetSpeed = 1.0;        // desired speed (user setting)
    let activeTimeouts = new Map(); // id -> {callback, delay, args, isInterval?}
    let activeIntervals = new Map();

    // Helper: adjust delay based on current speedFactor
    function adjustDelay(delay, isInterval) {
        if (delay === undefined || delay === null) return delay;
        let adjusted = delay / speedFactor;
        // never go below 1ms for intervals, and avoid negative
        if (isInterval && adjusted < 1) adjusted = 1;
        if (adjusted < 0) adjusted = 0;
        return Math.round(adjusted);
    }

    // Override setTimeout
    window.setTimeout = function(callback, delay, ...args) {
        let adjustedDelay = adjustDelay(delay, false);
        let id = originalSetTimeout(() => {
            if (activeTimeouts.has(id)) {
                activeTimeouts.delete(id);
                try {
                    callback(...args);
                } catch(e) { console.error(e); }
            }
        }, adjustedDelay);
        activeTimeouts.set(id, { callback, delay, args, isInterval: false });
        return id;
    };

    window.clearTimeout = function(id) {
        if (activeTimeouts.has(id)) {
            activeTimeouts.delete(id);
        }
        originalClearTimeout(id);
    };

    // Override setInterval
    window.setInterval = function(callback, delay, ...args) {
        let adjustedDelay = adjustDelay(delay, true);
        let id = originalSetInterval(() => {
            if (activeIntervals.has(id)) {
                try {
                    callback(...args);
                } catch(e) { console.error(e); }
            }
        }, adjustedDelay);
        activeIntervals.set(id, { callback, delay, args, isInterval: true });
        return id;
    };

    window.clearInterval = function(id) {
        if (activeIntervals.has(id)) {
            activeIntervals.delete(id);
        }
        originalClearInterval(id);
    };

    // Override Date.now() and Date constructor
    let timeOffset = 0; // milliseconds offset to simulate time passing faster/slower
    let lastRealTime = Date.now();
    let lastFakeTime = lastRealTime;

    function updateTimeOffset() {
        let now = originalDateNow();
        let realDelta = now - lastRealTime;
        let fakeDelta = realDelta * speedFactor;
        lastFakeTime += fakeDelta;
        lastRealTime = now;
        timeOffset = lastFakeTime - now;
    }

    // Poll every 50ms to keep offset accurate
    setInterval(() => { updateTimeOffset(); }, 50);

    Date.now = function() {
        return originalDateNow() + timeOffset;
    };

    // Replace Date constructor (for new Date())
    function HackedDate(...args) {
        if (args.length === 0) {
            // current time with offset
            return new originalDateConstructor(originalDateNow() + timeOffset);
        }
        return new originalDateConstructor(...args);
    }
    HackedDate.prototype = originalDateConstructor.prototype;
    HackedDate.now = Date.now;
    window.Date = HackedDate;

    // ---- 2. Change speed function ----
    function setSpeed(multiplier) {
        // multiplier: 2 = twice as fast, 0.5 = half speed
        let newSpeed = multiplier;
        if (newSpeed < 0.05) newSpeed = 0.05;
        if (newSpeed > 16) newSpeed = 16;
        targetSpeed = newSpeed;
        speedFactor = targetSpeed;

        // Recalculate time offset immediately
        lastRealTime = originalDateNow();
        lastFakeTime = originalDateNow() + timeOffset;
        updateTimeOffset();

        // Also adjust all active timeouts and intervals by restarting them? (optional, simpler: just change speed factor for future)
        // For existing intervals, we could recalc, but it's okay.

        // Update video playback rates
        let videos = document.querySelectorAll('video, audio');
        videos.forEach(v => { v.playbackRate = targetSpeed; });

        // Update UI display
        updateUISpeed();
        showSpeedPopup();
    }

    function resetSpeed() {
        setSpeed(1);
    }

    // ---- 3. Mobile UI ----
    let uiContainer = null;

    function updateUISpeed() {
        let btn = document.querySelector('._th-click-hover');
        if (btn) btn.innerText = 'x' + targetSpeed.toFixed(2);
    }

    function showSpeedPopup() {
        let overlay = document.querySelector('._th_cover-all-show-times');
        if (!overlay) return;
        let textDiv = overlay.querySelector('._th_times');
        if (textDiv) textDiv.innerText = targetSpeed.toFixed(2) + 'x';
        overlay.classList.remove('_th_hidden');
        setTimeout(() => {
            overlay.classList.add('_th_hidden');
        }, 800);
    }

    function createUI() {
        if (uiContainer) return;
        const style = document.createElement('style');
        style.textContent = `
            ._th-container {
                position: fixed;
                bottom: 20px;
                left: 20px;
                z-index: 999999;
                font-family: sans-serif;
                touch-action: manipulation;
            }
            ._th-click-hover {
                width: 50px;
                height: 50px;
                background: aquamarine;
                border-radius: 50%;
                text-align: center;
                line-height: 50px;
                font-weight: bold;
                font-size: 16px;
                box-shadow: 0 2px 8px rgba(0,0,0,0.3);
                cursor: pointer;
                transition: 0.2s;
            }
            ._th-menu-items {
                display: none;
                flex-direction: column;
                gap: 10px;
                margin-top: 10px;
            }
            ._th-container._th-open ._th-menu-items {
                display: flex;
            }
            ._th-item {
                width: 48px;
                height: 48px;
                background: rgba(127,255,212,0.95);
                border-radius: 50%;
                text-align: center;
                line-height: 48px;
                font-weight: bold;
                font-size: 18px;
                box-shadow: 0 1px 4px rgba(0,0,0,0.2);
                cursor: pointer;
                transition: 0.1s;
            }
            ._th-item:active {
                transform: scale(0.95);
            }
            ._th-item._item-10x { background: #ffaa66; }
            ._th-item._item-reset { background: #ff8888; }
            ._th_cover-all-show-times {
                position: fixed;
                top: 0;
                left: 0;
                width: 100%;
                height: 100%;
                background: rgba(0,0,0,0.3);
                display: flex;
                align-items: center;
                justify-content: center;
                z-index: 999999;
                pointer-events: none;
                transition: opacity 0.3s;
            }
            ._th_cover-all-show-times._th_hidden {
                opacity: 0;
                z-index: -1;
            }
            ._th_times {
                background: aquamarine;
                padding: 30px 50px;
                border-radius: 20px;
                font-size: 48px;
                font-weight: bold;
                box-shadow: 0 0 20px black;
            }
        `;
        document.head.appendChild(style);

        const html = `
            <div class="_th-container">
                <div class="_th-click-hover">x1.00</div>
                <div class="_th-menu-items">
                    <div class="_th-item _item-x2">2x</div>
                    <div class="_th-item _item-x-2">½x</div>
                    <div class="_th-item _item-xx2">4x</div>
                    <div class="_th-item _item-xx-2">¼x</div>
                    <div class="_th-item _item-10x">10x</div>
                    <div class="_th-item _item-reset">1x</div>
                </div>
            </div>
            <div class="_th_cover-all-show-times _th_hidden">
                <div class="_th_times"></div>
            </div>
        `;
        const wrapper = document.createElement('div');
        wrapper.innerHTML = html;
        document.body.appendChild(wrapper);
        uiContainer = wrapper;

        // Attach events
        const mainBtn = wrapper.querySelector('._th-click-hover');
        const containerDiv = wrapper.querySelector('._th-container');
        const menuItems = wrapper.querySelectorAll('._th-item');

        mainBtn.addEventListener('click', (e) => {
            e.stopPropagation();
            containerDiv.classList.toggle('_th-open');
        });

        function applySpeed(mult) {
            setSpeed(mult);
            containerDiv.classList.remove('_th-open');
        }

        wrapper.querySelector('._item-x2').addEventListener('click', () => applySpeed(2));
        wrapper.querySelector('._item-x-2').addEventListener('click', () => applySpeed(0.5));
        wrapper.querySelector('._item-xx2').addEventListener('click', () => applySpeed(4));
        wrapper.querySelector('._item-xx-2').addEventListener('click', () => applySpeed(0.25));
        wrapper.querySelector('._item-10x').addEventListener('click', () => applySpeed(10));
        wrapper.querySelector('._item-reset').addEventListener('click', () => applySpeed(1));

        // Close menu when tapping outside
        document.addEventListener('click', (e) => {
            if (!containerDiv.contains(e.target) && containerDiv.classList.contains('_th-open')) {
                containerDiv.classList.remove('_th-open');
            }
        });
    }

    // ---- 4. Initialization ----
    function init() {
        if (document.body) {
            createUI();
            // Set initial speed
            setSpeed(1);
        } else {
            document.addEventListener('DOMContentLoaded', init);
        }
    }

    init();

    // ---- 5. Also hook video elements dynamically ----
    const observer = new MutationObserver(() => {
        let videos = document.querySelectorAll('video, audio');
        videos.forEach(v => { v.playbackRate = targetSpeed; });
    });
    observer.observe(document.documentElement, { childList: true, subtree: true });

    // Fix initial video speed
    setSpeed(1);
})();