Greasy Fork 还支持 简体中文。

Torn Chain Timer - Enhanced

Chain timer with reliable updates, better UI, and sound alerts

  1. // ==UserScript==
  2. // @name Torn Chain Timer - Enhanced
  3. // @namespace http://tampermonkey.net/
  4. // @version 3.6
  5. // @description Chain timer with reliable updates, better UI, and sound alerts
  6. // @author lilha [2630451] & KillerCleat [2842410]
  7. // @match https://www.torn.com/*
  8. // @grant GM_setValue
  9. // @grant GM_getValue
  10. // @grant GM_addStyle
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. // Configuration
  17. const config = {
  18. minSize: 20,
  19. maxSize: 200,
  20. defaultSize: 40,
  21. updateInterval: 250,
  22. criticalThreshold: 45,
  23. warningThreshold: 90,
  24. soundEnabled: GM_getValue('soundEnabled', true),
  25. fontSize: GM_getValue('fontSize', 40),
  26. boxPosition: GM_getValue('boxPosition', { left: 20, top: 20 })
  27. };
  28.  
  29. // Hitting 1 Sound
  30. let beepAudio = new Audio('https://www.myinstants.com/media/sounds/hitting-1.mp3');
  31.  
  32. // Add base CSS
  33. GM_addStyle(`
  34. #chain-timer-container {
  35. position: fixed;
  36. left: ${config.boxPosition.left}px;
  37. top: ${config.boxPosition.top}px;
  38. padding: 10px;
  39. background: rgba(0, 0, 0, 0.8);
  40. color: white;
  41. font-weight: bold;
  42. border-radius: 5px;
  43. z-index: 9999;
  44. cursor: move;
  45. user-select: none;
  46. transition: all 0.3s ease;
  47. min-width: 120px;
  48. }
  49. #chain-timer {
  50. font-size: ${config.fontSize}px;
  51. text-align: center;
  52. margin-bottom: 5px;
  53. transition: font-size 0.2s ease;
  54. }
  55. .timer-controls {
  56. display: flex;
  57. gap: 5px;
  58. justify-content: center;
  59. align-items: center;
  60. flex-wrap: wrap;
  61. }
  62. .timer-btn {
  63. cursor: pointer;
  64. padding: 2px 5px;
  65. background: rgba(255, 255, 255, 0.2);
  66. border-radius: 3px;
  67. font-size: 12px;
  68. }
  69. .timer-btn:hover {
  70. background: rgba(255, 255, 255, 0.3);
  71. }
  72. #size-slider {
  73. width: 80px;
  74. margin: 0 5px;
  75. cursor: pointer;
  76. accent-color: #ffffff;
  77. }
  78. #chain-timer-container.warning {
  79. background: orange;
  80. }
  81. #chain-timer-container.critical {
  82. background: red;
  83. animation: flashScreen 0.5s infinite alternate;
  84. }
  85. @keyframes flashScreen {
  86. 0% { opacity: 1; }
  87. 100% { opacity: 0.3; }
  88. }
  89. .size-control {
  90. display: flex;
  91. align-items: center;
  92. background: rgba(255, 255, 255, 0.1);
  93. padding: 2px 5px;
  94. border-radius: 3px;
  95. }
  96. .size-label {
  97. font-size: 10px;
  98. opacity: 0.8;
  99. margin-right: 5px;
  100. }
  101. #chain-timer-container.fullscreen {
  102. position: fixed !important;
  103. left: 0 !important;
  104. top: 0 !important;
  105. width: 100vw !important;
  106. height: 100vh !important;
  107. display: flex;
  108. flex-direction: column;
  109. justify-content: center;
  110. align-items: center;
  111. background: rgba(0, 0, 0, 0.95);
  112. z-index: 10000;
  113. padding: 20px;
  114. }
  115. #chain-timer-container.fullscreen #chain-timer {
  116. font-size: calc(min(120px, 15vh)) !important;
  117. }
  118. `);
  119.  
  120. // Create timer container
  121. const timerContainer = document.createElement('div');
  122. timerContainer.id = 'chain-timer-container';
  123. timerContainer.innerHTML = `
  124. <div id="chain-timer">--:--</div>
  125. <div class="timer-controls">
  126. <div class="size-control">
  127. <span class="size-label">Size</span>
  128. <input type="range" id="size-slider"
  129. min="${config.minSize}"
  130. max="${config.maxSize}"
  131. value="${config.fontSize}">
  132. </div>
  133. <span class="timer-btn" id="timer-fullscreen">⛶</span>
  134. <span class="timer-btn" id="timer-minimize">_</span>
  135. <span class="timer-btn" id="toggle-sound">${config.soundEnabled ? '🔊' : '🔇'}</span>
  136. </div>
  137. `;
  138. document.body.appendChild(timerContainer);
  139.  
  140. let lastTime = '';
  141. let observer;
  142. let interval;
  143. let warningBeepPlayed = false;
  144. let criticalBeepPlayed = false;
  145. let isMinimized = false;
  146. let isFullscreen = false;
  147.  
  148. function initObserver() {
  149. if (observer) observer.disconnect();
  150. clearInterval(interval);
  151.  
  152. // Try both old and new selectors
  153. const timerElement = document.querySelector('.chain-box-timeleft, .bar-timeleft___B9RGV');
  154.  
  155. if (timerElement) {
  156. observer = new MutationObserver(() => updateTimer());
  157. observer.observe(timerElement, { characterData: true, childList: true, subtree: true });
  158. interval = setInterval(updateTimer, config.updateInterval);
  159. updateTimer();
  160. } else {
  161. setTimeout(initObserver, 1000);
  162. document.getElementById('chain-timer').textContent = '--:--';
  163. }
  164. }
  165.  
  166. function updateTimer() {
  167. const timerElement = document.querySelector('.chain-box-timeleft, .bar-timeleft___B9RGV');
  168. const displayElement = document.getElementById('chain-timer');
  169.  
  170. if (timerElement) {
  171. const newTime = timerElement.textContent.trim();
  172.  
  173. if (!/^\d+:\d{2}$/.test(newTime)) return;
  174.  
  175. if (newTime !== lastTime) {
  176. lastTime = newTime;
  177. displayElement.textContent = newTime;
  178.  
  179. const [mins, secs] = newTime.split(':').map(Number);
  180. const totalSeconds = mins * 60 + secs;
  181.  
  182. timerContainer.classList.remove('warning', 'critical');
  183.  
  184. if (totalSeconds <= config.criticalThreshold) {
  185. timerContainer.classList.add('critical');
  186. if (config.soundEnabled && !criticalBeepPlayed) {
  187. beepAudio.play().catch(err => console.error("Beep failed to play:", err));
  188. criticalBeepPlayed = true;
  189. }
  190. } else if (totalSeconds <= config.warningThreshold) {
  191. timerContainer.classList.add('warning');
  192. if (config.soundEnabled && !warningBeepPlayed) {
  193. beepAudio.play().catch(err => console.error("Beep failed to play:", err));
  194. warningBeepPlayed = true;
  195. }
  196. } else {
  197. warningBeepPlayed = false;
  198. criticalBeepPlayed = false;
  199. }
  200. }
  201. } else {
  202. displayElement.textContent = '--:--';
  203. timerContainer.classList.remove('warning', 'critical');
  204. }
  205. }
  206.  
  207. // Size slider functionality
  208. const sizeSlider = document.getElementById('size-slider');
  209. const timerDisplay = document.getElementById('chain-timer');
  210.  
  211. sizeSlider.addEventListener('input', (e) => {
  212. const newSize = parseInt(e.target.value);
  213. config.fontSize = newSize;
  214. GM_setValue('fontSize', newSize);
  215.  
  216. if (!isFullscreen) {
  217. timerDisplay.style.fontSize = `${newSize}px`;
  218. }
  219. });
  220.  
  221. // Fullscreen toggle
  222. document.getElementById('timer-fullscreen').addEventListener('click', () => {
  223. isFullscreen = !isFullscreen;
  224. timerContainer.classList.toggle('fullscreen');
  225. document.getElementById('timer-fullscreen').textContent = isFullscreen ? '⮌' : '⛶';
  226.  
  227. if (!isFullscreen) {
  228. // Only update font size when exiting fullscreen
  229. timerDisplay.style.fontSize = `${config.fontSize}px`;
  230. }
  231. });
  232.  
  233. // Minimize button
  234. document.getElementById('timer-minimize').addEventListener('click', () => {
  235. const controls = timerContainer.querySelector('.timer-controls');
  236. isMinimized = !isMinimized;
  237.  
  238. if (isMinimized) {
  239. controls.style.display = 'none';
  240. document.getElementById('timer-minimize').textContent = '□';
  241. } else {
  242. controls.style.display = 'flex';
  243. document.getElementById('timer-minimize').textContent = '_';
  244. }
  245. });
  246.  
  247. // Sound toggle
  248. document.getElementById('toggle-sound').addEventListener('click', () => {
  249. config.soundEnabled = !config.soundEnabled;
  250. GM_setValue('soundEnabled', config.soundEnabled);
  251. document.getElementById('toggle-sound').textContent = config.soundEnabled ? '🔊' : '🔇';
  252. if (config.soundEnabled) {
  253. beepAudio.play().catch(err => console.error("Beep failed to play:", err));
  254. }
  255. });
  256.  
  257. // Make the timer draggable
  258. timerContainer.onmousedown = function(event) {
  259. if (event.target.classList.contains('timer-btn') ||
  260. event.target.id === 'size-slider' ||
  261. isFullscreen) return;
  262.  
  263. let shiftX = event.clientX - timerContainer.getBoundingClientRect().left;
  264. let shiftY = event.clientY - timerContainer.getBoundingClientRect().top;
  265.  
  266. function moveAt(pageX, pageY) {
  267. config.boxPosition = {
  268. left: Math.max(0, Math.min(pageX - shiftX, window.innerWidth - timerContainer.offsetWidth)),
  269. top: Math.max(0, Math.min(pageY - shiftY, window.innerHeight - timerContainer.offsetHeight))
  270. };
  271. timerContainer.style.left = `${config.boxPosition.left}px`;
  272. timerContainer.style.top = `${config.boxPosition.top}px`;
  273. GM_setValue('boxPosition', config.boxPosition);
  274. }
  275.  
  276. function onMouseMove(event) {
  277. moveAt(event.pageX, event.pageY);
  278. }
  279.  
  280. document.addEventListener('mousemove', onMouseMove);
  281. document.onmouseup = function() {
  282. document.removeEventListener('mousemove', onMouseMove);
  283. document.onmouseup = null;
  284. };
  285. };
  286.  
  287. timerContainer.ondragstart = () => false;
  288.  
  289. // Initialize
  290. initObserver();
  291.  
  292. // Handle page navigation
  293. setInterval(() => {
  294. if (!document.querySelector('.chain-box-timeleft, .bar-timeleft___B9RGV')) {
  295. initObserver();
  296. }
  297. }, 3000);
  298.  
  299. document.addEventListener('visibilitychange', () => {
  300. if (!document.hidden) initObserver();
  301. });
  302.  
  303. // Handle SPA navigation
  304. const originalPushState = history.pushState;
  305. history.pushState = function() {
  306. originalPushState.apply(this, arguments);
  307. setTimeout(initObserver, 500);
  308. };
  309.  
  310. const originalReplaceState = history.replaceState;
  311. history.replaceState = function() {
  312. originalReplaceState.apply(this, arguments);
  313. setTimeout(initObserver, 500);
  314. };
  315. })();