Control page timers, video speed, skip ads – mobile friendly with 10x button.
目前為
// ==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);
})();