TimerHooker Mobile (Standalone)

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

À partir de 2026-05-24. Voir la dernière version.

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

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