TimerHooker Mobile (Standalone)

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

2026/05/24のページです。最新版はこちら

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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);
})();