TimerHooker Mobile (Standalone)

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

Ekde 2026/05/24. Vidu La ĝisdata versio.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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