Greasy Fork is available in English.
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.
À partir de
// ==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);