您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Toggle 1x/3x timer speed. UI docks at edge, undocks on interaction, draggable, 5S code, fully adaptive to light/dark mode.
// ==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);
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址