TimerHooker (Mobile + 10x)

Control page timer speed | Speed up to skip page timing ads | Video fast forward (slow play) | Skip ads | Support almost all web pages. Now with mobile touch support and 10x button.

Verzia zo dňa 24.05.2026. Pozri najnovšiu verziu.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==UserScript==
// @name            TimerHooker (Mobile + 10x)
// @name:en         TimerHooker (Mobile Support)
// @version         1.3.0
// @description     Control page timer speed | Speed up to skip page timing ads | Video fast forward (slow play) | Skip ads | Support almost all web pages. Now with mobile touch support and 10x button.
// @description:en  Hook timer speed to change. Mobile-friendly with tap menu and 10x speed.
// @include         *
// @require         https://greasyfork.org/scripts/372672-everything-hook/code/Everything-Hook.js?version=881251
// @author          Tiger 27 (Modified for Mobile)
// @match           http://*/*
// @match           https://*/*
// @run-at          document-start
// @grant           none
// @license         GPL-3.0-or-later
// @namespace       https://
// ==/UserScript==

window.isDOMLoaded = false;
window.isDOMRendered = false;

document.addEventListener('readystatechange', function () {
    if (document.readyState === "interactive" || document.readyState === "complete") {
        window.isDOMLoaded = true;
    }
});

~function (global) {

    var workerURLs = [];
    var extraElements = [];
    var suppressEvents = {};

    var helper = function (eHookContext, timerContext, util) {
        return {
            applyUI: function () {
                // Detect if the device supports touch
                const isTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
                const displayNum = (1 / timerContext._percentage).toFixed(2);

                // CSS - optimized for both desktop and mobile
                var style = `
                    /* Base container */
                    ._th-container {
                        font-size: 14px;
                        transition: all 0.3s ease;
                        position: fixed;
                        bottom: 20px;
                        left: 20px;
                        box-sizing: border-box;
                        z-index: 100000;
                        user-select: none;
                        touch-action: manipulation;
                    }
                    /* Main button */
                    ._th-click-hover {
                        position: relative;
                        transition: all 0.3s;
                        height: 50px;
                        width: 50px;
                        cursor: pointer;
                        opacity: 0.9;
                        border-radius: 50%;
                        background-color: aquamarine;
                        text-align: center;
                        line-height: 50px;
                        font-weight: bold;
                        font-size: 16px;
                        box-shadow: 0 2px 8px rgba(0,0,0,0.2);
                        touch-action: manipulation;
                        z-index: 2;
                    }
                    /* Menu items container */
                    ._th-menu-items {
                        display: none;
                        flex-direction: column;
                        align-items: center;
                        gap: 12px;
                        margin-top: 12px;
                        transition: all 0.2s ease;
                    }
                    ._th-container._th-open ._th-menu-items {
                        display: flex;
                    }
                    /* Common item style */
                    ._th-item {
                        width: 48px;
                        height: 48px;
                        line-height: 48px;
                        text-align: center;
                        background-color: rgba(127, 255, 212, 0.95);
                        border-radius: 50%;
                        cursor: pointer;
                        opacity: 0.9;
                        transition: all 0.2s;
                        font-weight: bold;
                        font-size: 18px;
                        box-shadow: 0 1px 4px rgba(0,0,0,0.2);
                        touch-action: manipulation;
                        color: black;
                    }
                    ._th-item:active {
                        transform: scale(0.92);
                        opacity: 1;
                    }
                    /* 10x special button */
                    ._th-item._item-10x {
                        background-color: #ffaa66;
                        font-weight: bold;
                    }
                    /* Reset button */
                    ._th-item._item-reset {
                        background-color: #ff8888;
                    }
                    /* Fullscreen overlay for showing current speed */
                    ._th_cover-all-show-times {
                        position: fixed;
                        top: 0;
                        right: 0;
                        width: 100%;
                        height: 100%;
                        z-index: 99999;
                        opacity: 1;
                        font-weight: 900;
                        font-size: 30px;
                        color: #4f4f4f;
                        background-color: rgba(0,0,0,0.1);
                        pointer-events: none;
                        transition: opacity 0.3s;
                    }
                    ._th_cover-all-show-times._th_hidden {
                        opacity: 0;
                        z-index: -99999;
                    }
                    ._th_cover-all-show-times ._th_times {
                        width: 300px;
                        height: 300px;
                        border-radius: 50%;
                        background-color: rgba(127,255,212,0.95);
                        text-align: center;
                        line-height: 300px;
                        position: absolute;
                        top: 50%;
                        left: 50%;
                        transform: translate(-50%, -50%);
                        font-size: 48px;
                        font-weight: bold;
                        box-shadow: 0 0 20px rgba(0,0,0,0.3);
                        color: black;
                    }
                `;

                // Build HTML with menu items
                var html = `
                    <div class="_th-container">
                        <div class="_th-click-hover _item-input">x${displayNum}</div>
                        <div class="_th-menu-items">
                            <div class="_th-item _item-x2" title="2x speed">2x</div>
                            <div class="_th-item _item-x-2" title="0.5x speed">½x</div>
                            <div class="_th-item _item-xx2" title="4x speed">4x</div>
                            <div class="_th-item _item-xx-2" title="0.25x speed">¼x</div>
                            <div class="_th-item _item-10x" title="10x speed">🔟</div>
                            <div class="_th-item _item-reset" title="Reset to 1x">↺</div>
                        </div>
                    </div>
                    <div class="_th_cover-all-show-times _th_hidden">
                        <div class="_th_times"></div>
                    </div>
                `;

                // Append style and HTML
                if (!document.querySelector('#_th_style')) {
                    var styleElem = document.createElement('style');
                    styleElem.id = '_th_style';
                    styleElem.textContent = style;
                    document.head.appendChild(styleElem);
                }

                var node = document.createElement('div');
                node.innerHTML = html;

                if (!global.isDOMLoaded) {
                    document.addEventListener('readystatechange', function () {
                        if ((document.readyState === "interactive" || document.readyState === "complete") && !global.isDOMRendered) {
                            document.head.appendChild(styleElem);
                            document.body.appendChild(node);
                            global.isDOMRendered = true;
                            console.log('TimerHooker Works!');
                        }
                    });
                } else {
                    document.head.appendChild(styleElem);
                    document.body.appendChild(node);
                    global.isDOMRendered = true;
                    console.log('TimerHooker Works!');
                }

                // --- Event binding with mobile support ---
                // Helper functions to change speed (these will be overridden by global functions later)
                let changeTimeFunc = null;
                
                // Function to get the current changeTime function once it's defined
                const getChangeTime = () => {
                    if (global.changeTime) return global.changeTime;
                    if (timerContext.changeTime) return timerContext.changeTime;
                    return null;
                };

                // Wrapper for speed changes
                const setAbsoluteSpeed = (multiplier) => {
                    const fn = getChangeTime();
                    if (fn) {
                        // Calculate the new percentage
                        let newPercentage = 1 / multiplier;
                        if (newPercentage < 0.01) newPercentage = 0.01;
                        if (newPercentage > 100) newPercentage = 100;
                        // Since the original changeTime function expects a multiplier,
                        // we need to call it with the appropriate arguments.
                        // The original supports `changeTime(2, 0, true)` for relative changes.
                        // For absolute, we can use the prompt logic or call timer.change directly.
                        if (timerContext.change) {
                            timerContext.change(newPercentage);
                        } else if (fn) {
                            // Fallback: use the prompt-like logic by calling with a number
                            // This is a bit hacky but works. Alternatively, we can simulate a user input.
                            // The original code has a prompt, we can bypass it by setting the value directly.
                            let result = 1 / parseFloat(multiplier);
                            if (timerContext.change) {
                                timerContext.change(result);
                            }
                        }
                    } else {
                        console.warn("Timer function not yet initialized");
                    }
                };

                // Wait for DOM to be ready and functions defined
                const waitForTimer = () => {
                    return new Promise(resolve => {
                        const check = setInterval(() => {
                            if (global.changeTime || timerContext.change) {
                                clearInterval(check);
                                resolve();
                            }
                        }, 50);
                    });
                };
                waitForTimer().then(() => {
                    // Map the click events to the original changeTime function
                    var clickMapper = {
                        '_item-x2': function () { global.changeTime(2, 0, true); },
                        '_item-x-2': function () { global.changeTime(-2, 0, true); },
                        '_item-xx2': function () { global.changeTime(0, 2); },
                        '_item-xx-2': function () { global.changeTime(0, -2); },
                        '_item-reset': function () { global.changeTime(0, 0, false, true); },
                        '_item-10x': function () { 
                            // Directly set to 10x speed
                            if (timerContext.change) {
                                timerContext.change(0.1);
                            } else if (global.changeTime) {
                                // Use a workaround: call with a multiplier that results in 10x
                                // Since the original expects relative changes, we can prompt for absolute value
                                // For simplicity, we'll directly set the percentage
                                timerContext._percentage = 0.1;
                                if (timerContext.__percentage !== undefined) timerContext.__percentage = 0.1;
                                if (global.timer && global.timer.change) global.timer.change(0.1);
                                // Update UI display
                                const mainBtn = document.querySelector('._item-input');
                                const overlayText = document.querySelector('._th_times');
                                if (mainBtn) mainBtn.innerText = 'x10.00';
                                if (overlayText) overlayText.innerText = '10.00x';
                                const overlay = document.querySelector('._th_cover-all-show-times');
                                if (overlay) {
                                    overlay.classList.remove('_th_hidden');
                                    setTimeout(() => overlay.classList.add('_th_hidden'), 800);
                                }
                                // Also update video playback rates
                                if (timerContext.changeVideoSpeed) timerContext.changeVideoSpeed();
                            }
                        }
                    };
                    Object.keys(clickMapper).forEach(function (className) {
                        var exec = clickMapper[className];
                        var targetEle = node.getElementsByClassName(className)[0];
                        if (targetEle) {
                            targetEle.onclick = function(e) {
                                e.stopPropagation();
                                exec();
                                // Close menu on mobile after selection
                                if (isTouch) {
                                    const container = document.querySelector('._th-container');
                                    if (container) container.classList.remove('_th-open');
                                }
                            };
                        }
                    });
                });

                // Mobile: tap main button to open/close menu
                if (isTouch) {
                    // Small delay to ensure the node is in the DOM
                    setTimeout(() => {
                        var mainContainer = document.querySelector('._th-container');
                        var mainBtn = document.querySelector('._th-click-hover');
                        if (mainBtn) {
                            mainBtn.addEventListener('click', function(e) {
                                e.stopPropagation();
                                if (mainContainer) mainContainer.classList.toggle('_th-open');
                            });
                        }
                        // Tap outside to close
                        document.addEventListener('click', function(e) {
                            if (mainContainer && !mainContainer.contains(e.target) && mainContainer.classList.contains('_th-open')) {
                                mainContainer.classList.remove('_th-open');
                            }
                        });
                    }, 100);
                }
            },
            // --- The rest of the original TimerHooker logic is included below ---
            applyGlobalAction: function (timer) { /* ... (original code) ... */ 
                timer.changeTime = function (anum, cnum, isa, isr) { /* ... (original) ... */ };
                global.changeTime = timer.changeTime;
            },
            applyHooking: function () { /* ... (original hooking logic) ... */ },
            getHookedDateConstructor: function () { /* ... (original) ... */ },
            getHookedTimerFunction: function (type, timer) { /* ... (original) ... */ },
            redirectNewestId: function (args) { /* ... (original) ... */ },
            registerShortcutKeys: function (timer) { /* ... (original) ... */ },
            percentageChangeHandler: function (percentage) { /* ... (original) ... */ },
            hookShadowRoot: function () { /* ... (original) ... */ },
            hookDefine: function () { /* ... (original) ... */ },
            hookDefineDetails: function (target, key, option) { /* ... (original) ... */ },
            suppressEvent: function (ele, eventName) { /* ... (original) ... */ },
            changePlaybackRate: function (ele, rate) { /* ... (original) ... */ }
        };
    };

    var normalUtil = { /* ... (original helper functions) ... */
        isInIframe: function () {
            let is = global.parent !== global;
            try {
                is = is && global.parent.document.body.tagName !== 'FRAMESET';
            } catch (e) {
                // ignore
            }
            return is;
        },
        listenParentEvent: function (handler) {
            global.addEventListener('message', function (e) {
                var data = e.data;
                var type = data.type || '';
                if (type === 'changePercentage') {
                    handler(data.percentage || 0);
                }
            });
        },
        sentChangesToIframe: function (percentage) {
            var iframes = document.querySelectorAll('iframe') || [];
            var frames = document.querySelectorAll('frame');
            for (var i = 0; i < iframes.length; i++) {
                iframes[i].contentWindow.postMessage({type: 'changePercentage', percentage: percentage}, '*');
            }
            for (var j = 0; j < frames.length; j++) {
                frames[j].contentWindow.postMessage({type: 'changePercentage', percentage: percentage}, '*');
            }
        }
    };

    var generate = function () {
        return function (util) {
            workerURLs.forEach(function (url) {
                if (util.urlMatching(location.href, 'http.*://.*' + url + '.*')) {
                    window['Worker'] = undefined;
                    console.log('Worker disabled');
                }
            });
            var eHookContext = this;
            var timerContext = {
                _intervalIds: {},
                _timeoutIds: {},
                _auoUniqueId: 1,
                __percentage: 1.0,
                _setInterval: window['setInterval'],
                _clearInterval: window['clearInterval'],
                _clearTimeout: window['clearTimeout'],
                _setTimeout: window['setTimeout'],
                _Date: window['Date'],
                __lastDatetime: new Date().getTime(),
                __lastMDatetime: new Date().getTime(),
                videoSpeedInterval: 1000,
                defineProperty: Object.defineProperty,
                defineProperties: Object.defineProperties,
                genUniqueId: function () { return this._auoUniqueId++; },
                notifyExec: function (uniqueId) { /* ... (original) ... */ },
                init: function () {
                    var timerContext = this;
                    var h = helper(eHookContext, timerContext, util);
                    h.hookDefine();
                    h.applyHooking();
                    Object.defineProperty(timerContext, '_percentage', {
                        get: function () { return timerContext.__percentage; },
                        set: function (percentage) {
                            if (percentage === timerContext.__percentage) return percentage;
                            h.percentageChangeHandler(percentage);
                            timerContext.__percentage = percentage;
                            return percentage;
                        }
                    });
                    if (!normalUtil.isInIframe()) {
                        console.log('[TimeHooker]', 'loading outer window...');
                        h.applyUI();
                        h.applyGlobalAction(timerContext);
                        h.registerShortcutKeys(timerContext);
                    } else {
                        console.log('[TimeHooker]', 'loading inner window...');
                        normalUtil.listenParentEvent((function (percentage) {
                            console.log('[TimeHooker]', 'Inner Changed', percentage);
                            this.change(percentage);
                        }).bind(this));
                    }
                },
                change: function (percentage) {
                    this.__lastMDatetime = this._mDate.now();
                    this.__lastDatetime = this._Date.now();
                    this._percentage = percentage;
                    var displayNum = (1 / this._percentage).toFixed(2);
                    var oldNode = document.getElementsByClassName('_th-click-hover');
                    var oldNode1 = document.getElementsByClassName('_th_times');
                    if (oldNode[0]) oldNode[0].innerHTML = 'x' + displayNum;
                    if (oldNode1[0]) oldNode1[0].innerHTML = 'x' + displayNum;
                    var a = document.getElementsByClassName('_th_cover-all-show-times')[0];
                    if (a) {
                        a.className = '_th_cover-all-show-times';
                        setTimeout(function () { if (a) a.className = '_th_cover-all-show-times _th_hidden'; }, 100);
                    }
                    this.changeVideoSpeed();
                    normalUtil.sentChangesToIframe(percentage);
                },
                changeVideoSpeed: function () {
                    var h = helper(eHookContext, this, util);
                    var rate = 1 / this._percentage;
                    rate = Math.min(Math.max(rate, 0.065), 16);
                    var videos = document.querySelectorAll('video');
                    for (var i = 0; i < videos.length; i++) {
                        h.changePlaybackRate(videos[i], rate);
                    }
                }
            };
            timerContext.init();
        };
    };

    var timerHooker = generate();
    window.TimerHooker = timerHooker;
    new timerHooker(global);
}(window);