// ==UserScript==
// @name TimerHooker (English, Modern UI)
// @version 4.1.1
// @description Toggle 1x/3x timer speed. UI docks at edge, undocks on interaction, draggable, 5S code, fully adaptive to light/dark mode.
// @include *
// @match http://*/*
// @match https://*/*
// @require https://gf.qytechs.cn/scripts/372672-everything-hook/code/Everything-Hook.js?version=881251
// @author Tiger 27, Perplexity AI
// @run-at document-start
// @grant none
// @license MIT
// @namespace https://gf.qytechs.cn/users/1356925
// ==/UserScript==
(function (global) {
'use strict';
/*** 5S: SORT - Group related functions and variables together ***/
// --- UI Constants ---
const UI = {
BTN_SIZE: 64,
CIRCLE_SIZE: 56,
ICON_SIZE: 36,
DOCK_OPACITY: 0.6,
UNDOCK_OPACITY: 1,
DOCK_TIMEOUT: 3000, // ms
DOCK_MARGIN: 10,
INIT_TOP: 0.2, // 20% from top
};
// --- Timer Constants ---
const SPEED_NORMAL = 1.0;
const SPEED_FAST = 1 / 3; // 3x faster (intervals are 1/3 original)
/*** 5S: SET IN ORDER - Clear naming, logical order, modularity ***/
// --- Timer Context ---
const timerContext = {
_intervalIds: {},
_timeoutIds: {},
_uniqueId: 1,
__percentage: SPEED_NORMAL,
_setInterval: window.setInterval,
_clearInterval: window.clearInterval,
_setTimeout: window.setTimeout,
_clearTimeout: window.clearTimeout,
_Date: window.Date,
__lastDatetime: Date.now(),
__lastMDatetime: Date.now(),
genUniqueId() { return this._uniqueId++; },
notifyExec(uniqueId) {
if (!uniqueId) return;
Object.values(this._timeoutIds)
.filter(info => info.uniqueId === uniqueId)
.forEach(info => {
this._clearTimeout.call(window, info.nowId);
delete this._timeoutIds[info.originId];
});
},
get _percentage() { return this.__percentage; },
set _percentage(val) {
if (val === this.__percentage) return;
percentageChangeHandler(val, this);
this.__percentage = val;
}
};
// --- Global Timer API ---
global.timer = {
change(percentage) {
timerContext.__lastMDatetime = timerContext._mDate.now();
timerContext.__lastDatetime = timerContext._Date.now();
timerContext._percentage = percentage;
}
};
/*** 5S: SHINE - Keep code clean, readable, and well-commented ***/
// --- UI Creation ---
function createStyles() {
const style = `
:root {
--th-bg-light: rgba(245,245,245,0.95);
--th-bg-dark: rgba(30,30,30,0.95);
--th-fg-light: #222;
--th-fg-dark: #fafafa;
--th-shadow: 0 2px 12px 0 rgba(0,0,0,0.20);
--th-accent: #4e91ff;
}
@media (prefers-color-scheme: dark) {
:root {
--th-bg: var(--th-bg-dark);
--th-fg: var(--th-fg-dark);
}
}
@media (prefers-color-scheme: light) {
:root {
--th-bg: var(--th-bg-light);
--th-fg: var(--th-fg-light);
}
}
.th-move-btn {
position: fixed;
left: 0; top: 20%;
z-index: 100000;
background: none;
border: none;
outline: none;
box-shadow: none;
cursor: grab;
padding: 0;
margin: 0;
width: ${UI.BTN_SIZE}px; height: ${UI.BTN_SIZE}px;
display: flex;
align-items: center;
justify-content: center;
opacity: ${UI.DOCK_OPACITY};
border-radius: 50%;
user-select: none;
transition: left 0.4s cubic-bezier(.4,2,.6,1), right 0.4s cubic-bezier(.4,2,.6,1), opacity 0.2s, transform 0.4s cubic-bezier(.4,2,.6,1);
transform: translateX(-50%);
}
.th-move-btn.undocked {
opacity: ${UI.UNDOCK_OPACITY} !important;
transform: translateX(0) !important;
}
.th-move-btn:active {
cursor: grabbing;
filter: brightness(0.85);
}
.th-circle {
width: ${UI.CIRCLE_SIZE}px; height: ${UI.CIRCLE_SIZE}px;
border-radius: 50%;
background: var(--th-bg, #eee);
box-shadow: var(--th-shadow);
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
position: absolute;
left: 4px; top: 4px;
transition: background 0.3s;
}
.th-icon {
width: ${UI.ICON_SIZE}px; height: ${UI.ICON_SIZE}px;
display: block;
fill: var(--th-fg, #222);
pointer-events: none;
user-select: none;
position: relative;
transition: fill 0.3s;
}
`;
const stylenode = document.createElement('style');
stylenode.type = "text/css";
stylenode.appendChild(document.createTextNode(style));
document.head.appendChild(stylenode);
}
function getIconSVG(isFast) {
// Play icon for 1x, lightning for 3x
return isFast
? `<svg class="th-icon" viewBox="0 0 48 48"><polygon points="20,7 42,24 28,24 28,41 6,24 20,24" style="fill:var(--th-accent,#4e91ff)"/><polygon points="20,7 42,24 28,24 28,41 6,24 20,24" style="fill-opacity:0.3;fill:var(--th-fg,#fafafa)"/></svg>`
: `<svg class="th-icon" viewBox="0 0 48 48"><polygon points="15,10 39,24 15,38"/></svg>`;
}
function createUI() {
createStyles();
const html = `
<button class="th-move-btn" id="th_move_btn" type="button">
<span class="th-circle"></span>
<span id="th_icon_container"></span>
</button>
`;
const node = document.createElement('div');
node.innerHTML = html;
document.body.appendChild(node);
// --- UI State ---
const moveBtn = document.getElementById('th_move_btn');
const iconContainer = document.getElementById('th_icon_container');
let isFast = false;
let isDragging = false;
let dragStartX = 0, dragStartY = 0;
let origLeft = 0, origTop = 0;
let dockedSide = 'left'; // or 'right'
let docked = true;
let hideTimeout;
// --- UI Functions ---
function dockUI() {
docked = true;
moveBtn.classList.remove('undocked');
moveBtn.style.opacity = UI.DOCK_OPACITY;
moveBtn.style.top = (parseFloat(moveBtn.style.top) || window.innerHeight * UI.INIT_TOP) + 'px';
if (dockedSide === 'left') {
moveBtn.style.left = '0px';
moveBtn.style.right = 'auto';
moveBtn.style.transform = 'translateX(-50%)';
} else {
moveBtn.style.right = '0px';
moveBtn.style.left = 'auto';
moveBtn.style.transform = 'translateX(50%)';
}
}
function undockUI() {
docked = false;
moveBtn.classList.add('undocked');
moveBtn.style.opacity = UI.UNDOCK_OPACITY;
moveBtn.style.transform = 'translateX(0)';
moveBtn.style.top = (parseFloat(moveBtn.style.top) || window.innerHeight * UI.INIT_TOP) + 'px';
if (dockedSide === 'left') {
moveBtn.style.left = '0px';
moveBtn.style.right = 'auto';
} else {
moveBtn.style.right = '0px';
moveBtn.style.left = 'auto';
}
}
function scheduleDock() {
clearTimeout(hideTimeout);
hideTimeout = setTimeout(() => {
// Find closest edge
const rect = moveBtn.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
dockedSide = (centerX < window.innerWidth / 2) ? 'left' : 'right';
dockUI();
}, UI.DOCK_TIMEOUT);
}
function onInteraction() {
if (docked) undockUI();
scheduleDock();
}
function setSpeed(fast) {
isFast = fast;
iconContainer.innerHTML = getIconSVG(isFast);
global.timer.change(isFast ? SPEED_FAST : SPEED_NORMAL);
onInteraction();
}
// --- UI Event Listeners ---
moveBtn.addEventListener('mousedown', e => {
if (e.button !== 0) return;
isDragging = true;
dragStartX = e.clientX;
dragStartY = e.clientY;
origLeft = parseFloat(moveBtn.style.left) || 0;
origTop = parseFloat(moveBtn.style.top) || window.innerHeight * UI.INIT_TOP;
document.body.style.userSelect = "none";
onInteraction();
});
document.addEventListener('mousemove', e => {
if (!isDragging) return;
const dx = e.clientX - dragStartX;
const dy = e.clientY - dragStartY;
const maxLeft = window.innerWidth - moveBtn.offsetWidth - UI.DOCK_MARGIN;
const maxTop = window.innerHeight - moveBtn.offsetHeight - UI.DOCK_MARGIN;
let newLeft = origLeft + dx;
let newTop = origTop + dy;
newLeft = Math.min(Math.max(newLeft, UI.DOCK_MARGIN), maxLeft);
newTop = Math.min(Math.max(newTop, UI.DOCK_MARGIN), maxTop);
moveBtn.classList.add('undocked');
moveBtn.style.opacity = UI.UNDOCK_OPACITY;
moveBtn.style.transform = 'translateX(0)';
moveBtn.style.left = newLeft + 'px';
moveBtn.style.right = 'auto';
moveBtn.style.top = newTop + 'px';
docked = false;
scheduleDock();
});
document.addEventListener('mouseup', () => {
if (!isDragging) return;
isDragging = false;
document.body.style.userSelect = "";
scheduleDock();
});
moveBtn.addEventListener('touchstart', e => {
isDragging = true;
const touch = e.touches[0];
dragStartX = touch.clientX;
dragStartY = touch.clientY;
origLeft = parseFloat(moveBtn.style.left) || 0;
origTop = parseFloat(moveBtn.style.top) || window.innerHeight * UI.INIT_TOP;
document.body.style.userSelect = "none";
onInteraction();
});
document.addEventListener('touchmove', e => {
if (!isDragging) return;
const touch = e.touches[0];
const dx = touch.clientX - dragStartX;
const dy = touch.clientY - dragStartY;
const maxLeft = window.innerWidth - moveBtn.offsetWidth - UI.DOCK_MARGIN;
const maxTop = window.innerHeight - moveBtn.offsetHeight - UI.DOCK_MARGIN;
let newLeft = origLeft + dx;
let newTop = origTop + dy;
newLeft = Math.min(Math.max(newLeft, UI.DOCK_MARGIN), maxLeft);
newTop = Math.min(Math.max(newTop, UI.DOCK_MARGIN), maxTop);
moveBtn.classList.add('undocked');
moveBtn.style.opacity = UI.UNDOCK_OPACITY;
moveBtn.style.transform = 'translateX(0)';
moveBtn.style.left = newLeft + 'px';
moveBtn.style.right = 'auto';
moveBtn.style.top = newTop + 'px';
docked = false;
scheduleDock();
}, { passive: false });
document.addEventListener('touchend', () => {
if (!isDragging) return;
isDragging = false;
document.body.style.userSelect = "";
scheduleDock();
});
moveBtn.addEventListener('click', () => {
if (isDragging) return;
setSpeed(!isFast);
});
['mouseenter', 'touchstart', 'mousedown'].forEach(ev => {
moveBtn.addEventListener(ev, onInteraction);
});
// --- UI Initial State ---
dockedSide = 'left';
moveBtn.style.left = '0px';
moveBtn.style.right = 'auto';
moveBtn.style.top = window.innerHeight * UI.INIT_TOP + 'px';
moveBtn.style.transform = 'translateX(-50%)';
moveBtn.style.opacity = UI.DOCK_OPACITY;
docked = true;
scheduleDock();
setTimeout(() => setSpeed(false), 100);
}
/*** 5S: STANDARDIZE - Use clear patterns for hooking and timer management ***/
function applyHooking(ctx) {
const eHookContext = global.eHook;
eHookContext.hookReplace(window, 'setInterval', setInterval => getHookedTimerFunction('interval', setInterval, ctx));
eHookContext.hookReplace(window, 'setTimeout', setTimeout => getHookedTimerFunction('timeout', setTimeout, ctx));
eHookContext.hookBefore(window, 'clearInterval', (method, args) => redirectNewestId(args, ctx));
eHookContext.hookBefore(window, 'clearTimeout', (method, args) => redirectNewestId(args, ctx));
eHookContext.hookClass(window, 'Date', getHookedDateConstructor(ctx), '_innerDate', ['now']);
Date.now = () => new Date().getTime();
ctx._mDate = window.Date;
}
function getHookedDateConstructor(ctx) {
return function (...args) {
if (args.length === 1) {
Object.defineProperty(this, '_innerDate', {
configurable: false, enumerable: false,
value: new ctx._Date(args[0]), writable: false
});
return;
} else if (args.length > 1) {
let definedValue;
switch (args.length) {
case 2: definedValue = new ctx._Date(args[0], args[1]); break;
case 3: definedValue = new ctx._Date(args[0], args[1], args[2]); break;
case 4: definedValue = new ctx._Date(args[0], args[1], args[2], args[3]); break;
case 5: definedValue = new ctx._Date(args[0], args[1], args[2], args[3], args[4]); break;
case 6: definedValue = new ctx._Date(args[0], args[1], args[2], args[3], args[4], args[5]); break;
default: definedValue = new ctx._Date(args[0], args[1], args[2], args[3], args[4], args[5], args[6]);
}
Object.defineProperty(this, '_innerDate', {
configurable: false, enumerable: false,
value: definedValue, writable: false
});
return;
}
const now = ctx._Date.now();
const passTime = now - ctx.__lastDatetime;
const hookPassTime = passTime * (1 / ctx._percentage);
Object.defineProperty(this, '_innerDate', {
configurable: false, enumerable: false,
value: new ctx._Date(ctx.__lastMDatetime + hookPassTime), writable: false
});
};
}
function getHookedTimerFunction(type, timer, ctx) {
const property = '_' + type + 'Ids';
return function (...args) {
const uniqueId = ctx.genUniqueId();
let callback = args[0];
if (typeof callback === 'string') {
callback += `;timer.notifyExec(${uniqueId})`;
args[0] = callback;
}
if (typeof callback === 'function') {
args[0] = function () {
const returnValue = callback.apply(this, arguments);
ctx.notifyExec(uniqueId);
return returnValue;
};
}
const originMS = args[1];
args[1] *= ctx._percentage;
const resultId = timer.apply(window, args);
ctx[property][resultId] = {
args,
originMS,
originId: resultId,
nowId: resultId,
uniqueId,
oldPercentage: ctx._percentage,
exceptNextFireTime: ctx._Date.now() + originMS,
};
return resultId;
};
}
function redirectNewestId(args, ctx) {
const id = args[0];
if (ctx._intervalIds[id]) {
args[0] = ctx._intervalIds[id].nowId;
delete ctx._intervalIds[id];
}
if (ctx._timeoutIds[id]) {
args[0] = ctx._timeoutIds[id].nowId;
delete ctx._timeoutIds[id];
}
}
function percentageChangeHandler(percentage, ctx) {
Object.values(ctx._intervalIds).forEach(idObj => {
idObj.args[1] = Math.floor((idObj.originMS || 1) * percentage);
ctx._clearInterval.call(window, idObj.nowId);
idObj.nowId = ctx._setInterval.apply(window, idObj.args);
});
Object.values(ctx._timeoutIds).forEach(idObj => {
const now = ctx._Date.now();
let time = idObj.exceptNextFireTime - now;
if (time < 0) time = 0;
const changedTime = Math.floor((percentage / idObj.oldPercentage) * time);
idObj.args[1] = changedTime;
idObj.exceptNextFireTime = now + changedTime;
idObj.oldPercentage = percentage;
ctx._clearTimeout.call(window, idObj.nowId);
idObj.nowId = ctx._setTimeout.apply(window, idObj.args);
});
}
/*** 5S: SUSTAIN - Keep code maintainable, modular, and documented ***/
function main() {
applyHooking(timerContext);
if (document.readyState === 'complete' || document.readyState === 'interactive') {
createUI();
} else {
document.addEventListener('DOMContentLoaded', createUI);
}
}
if (global.eHook) main();
})(window);