Enhanced Audio Speed Controller with Time Info, Speed Highlight, Font Size Control, and Toggle

Adds time information (duration, currentTime, etc.), adjusts for playback speed, highlights the active speed button, toggle for hiding/showing the control panel, and font size control.

当前为 2024-10-18 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Enhanced Audio Speed Controller with Time Info, Speed Highlight, Font Size Control, and Toggle
  3. // @namespace http://tampermonkey.net/
  4. // @version 3.2
  5. // @description Adds time information (duration, currentTime, etc.), adjusts for playback speed, highlights the active speed button, toggle for hiding/showing the control panel, and font size control.
  6. // @author You
  7. // @match *://*/*
  8. // @grant none
  9. // @license MIT
  10. // ==/UserScript==
  11.  
  12. (function () {
  13. 'use strict';
  14.  
  15. let startTime = Date.now(); // Track the real-time start
  16. let isPanelVisible = true; // Track panel visibility
  17. let currentFontSize = 7; // Initial font size in pt
  18.  
  19. const odIcon = '🕰️';
  20. const adIcon = '⏰';
  21. const ctIcon = '⌚';
  22. const pcIcon = '➗';
  23. const trIcon = '⏳';
  24. const wcIcon = '🕛';
  25.  
  26. // Helper function to convert fractional minutes into hh:mm:ss format
  27. function convertToTimeFormat(minutes) {
  28. const totalSeconds = Math.floor(minutes * 60); // Convert minutes to seconds
  29. const hours = Math.floor(totalSeconds / 3600); // Calculate full hours
  30. const remainingSeconds = totalSeconds % 3600; // Remaining seconds after hours
  31. const mins = Math.floor(remainingSeconds / 60); // Full minutes
  32. const secs = remainingSeconds % 60; // Remaining seconds
  33.  
  34. const formattedTime =
  35. (hours > 9 ? hours : '0' + hours) + ':' +
  36. (mins > 9 ? mins : '0' + mins) + ':' +
  37. (secs > 9 ? secs : '0' + secs);
  38.  
  39. return formattedTime;
  40. }
  41.  
  42. // Function to calculate and update time stats
  43. function updateTimeStats() {
  44. const audioElement = document.querySelector('audio');
  45. if (audioElement && audioElement.duration && !isNaN(audioElement.duration)) {
  46. const duration = audioElement.duration;
  47. const adjustedDuration = audioElement.duration / audioElement.playbackRate / 60;
  48. const currentTime = audioElement.currentTime;
  49. const playbackRate = audioElement.playbackRate;
  50. const currentTimeDisp = currentTime / 60 / playbackRate;
  51. const percentComplete = (currentTime / duration) * 100;
  52.  
  53. const timeRemaining = (adjustedDuration - currentTimeDisp);
  54. const elapsedWallClockTime = (Date.now() - startTime) / 1000 / 60;
  55.  
  56. document.getElementById('original-duration').innerHTML = `${odIcon}<br>${convertToTimeFormat(duration / 60)}`;
  57. document.getElementById('adjusted-duration').innerHTML = `${adIcon}<br>${convertToTimeFormat(adjustedDuration)}`;
  58. document.getElementById('current-time').innerHTML = `${ctIcon}<br>${convertToTimeFormat(currentTimeDisp)}`;
  59. document.getElementById('percent-complete').innerHTML = `<span class="rotate">${pcIcon}</span><br>${percentComplete.toFixed(2)}%`;
  60. document.getElementById('time-remaining').innerHTML = `${trIcon}<br>${convertToTimeFormat(timeRemaining)}`;
  61. document.getElementById('elapsed-wall-clock').innerHTML = `${wcIcon}<br>${convertToTimeFormat(elapsedWallClockTime)}`;
  62. }
  63. }
  64.  
  65. // Function to create the control panel with buttons and time information
  66. function createControlPanel() {
  67. if (document.getElementById('audio-speed-control')) return;
  68.  
  69. const controlDiv = document.createElement('div');
  70. controlDiv.id = 'audio-speed-control';
  71. controlDiv.style.position = 'fixed';
  72. controlDiv.style.top = '20%';
  73. controlDiv.style.right = '0';
  74. controlDiv.style.background = 'rgba(0, 0, 0, 0.05)';
  75. controlDiv.style.padding = '5px';
  76. controlDiv.style.borderRadius = '5px';
  77. controlDiv.style.zIndex = '999999';
  78. controlDiv.style.display = 'flex';
  79. controlDiv.style.flexDirection = 'column';
  80. controlDiv.style.fontSize = `${currentFontSize}pt`;
  81. controlDiv.style.transition = 'transform 0.5s ease';
  82.  
  83. const timeStats = document.createElement('div');
  84. timeStats.style.marginBottom = '4px';
  85. timeStats.style.fontSize = '6pt';
  86. timeStats.style.color = 'black';
  87. timeStats.style.fontWeight = "bold";
  88. timeStats.style.textAlign = 'center';
  89.  
  90. const timeStatsData = [
  91. { id: 'original-duration', label: odIcon },
  92. { id: 'adjusted-duration', label: adIcon },
  93. { id: 'current-time', label: ctIcon },
  94. { id: 'percent-complete', label: pcIcon },
  95. { id: 'time-remaining', label: trIcon },
  96. { id: 'elapsed-wall-clock', label: wcIcon }
  97. ];
  98.  
  99. timeStatsData.forEach(stat => {
  100. const statDiv = document.createElement('div');
  101. statDiv.id = stat.id;
  102. statDiv.innerHTML = stat.label + "<br>--:--:--";
  103. timeStats.appendChild(statDiv);
  104. });
  105.  
  106. controlDiv.appendChild(timeStats);
  107.  
  108. // Create PAUSE button
  109. const pauseButton = document.createElement('button');
  110. pauseButton.innerText = '||'; // Label with pause symbol
  111. pauseButton.style.padding = '3px 5px';
  112. pauseButton.style.marginBottom = '2px';
  113. pauseButton.style.backgroundColor = '#bada55';
  114. pauseButton.style.border = 'none';
  115. pauseButton.style.borderRadius = '3px';
  116. pauseButton.style.cursor = 'pointer';
  117. pauseButton.style.fontSize = '8px';
  118. pauseButton.style.fontWeight = 'bold';
  119. pauseButton.style.color = '#222';
  120. pauseButton.style.width = '36px';
  121.  
  122. pauseButton.addEventListener('click', function () {
  123. const audioElement = document.querySelector('audio');
  124. if (audioElement) {
  125. const targetState = audioElement.paused; // Determine the desired state
  126.  
  127. function tryTogglePlayPause() {
  128. if (targetState) {
  129. audioElement.play().catch(error => {
  130. console.error("Error playing audio:", error);
  131. setTimeout(tryTogglePlayPause, 100);
  132. });
  133. pauseButton.innerText = '||';
  134. } else {
  135. audioElement.pause();
  136. pauseButton.innerText = '▶';
  137. }
  138. }
  139.  
  140. tryTogglePlayPause(); // Initial attempt
  141. if (targetState) {
  142. // If the target state is playing, schedule retries
  143. setTimeout(tryTogglePlayPause, 100);
  144. }
  145. }
  146. });
  147.  
  148.  
  149. // pauseButton.addEventListener('click', function () {
  150. // const audioElement = document.querySelector('audio');
  151. // if (audioElement) {
  152. // if (audioElement.paused) {
  153. // audioElement.play();
  154. // pauseButton.innerText = '||';
  155. // } else {
  156. // audioElement.pause();
  157. // pauseButton.innerText = '▶';
  158. // }
  159. // }
  160. // });
  161.  
  162. controlDiv.appendChild(pauseButton);
  163.  
  164. const speeds = [1.0, 1.25, 1.5, 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 3.25, 3.5, 3.75, 4.0, 4.5, 5.0];
  165. let activeButton = null;
  166.  
  167. speeds.forEach(speed => {
  168. const button = document.createElement('button');
  169. button.innerText = speed.toFixed(2);
  170. button.style.padding = '3px 5px';
  171. button.style.marginBottom = '2px';
  172. button.style.backgroundColor = '#bada55';
  173. button.style.border = 'none';
  174. button.style.borderRadius = '3px';
  175. button.style.cursor = 'pointer';
  176. button.style.fontSize = '8px';
  177. button.style.fontWeight = 'bold';
  178. button.style.color = '#222';
  179. button.style.width = '36px';
  180.  
  181. function setActiveButton() {
  182. if (activeButton) {
  183. activeButton.style.backgroundColor = '#bada55';
  184. activeButton.style.color = '#222';
  185. activeButton.style.fontWeight = 'normal';
  186. }
  187. button.style.backgroundColor = '#006400';
  188. button.style.color = '#fff';
  189. button.style.fontWeight = 'bold';
  190. activeButton = button;
  191. }
  192.  
  193. button.addEventListener('click', function () {
  194. const audioElement = document.querySelector('audio');
  195. if (audioElement) {
  196. audioElement.playbackRate = speed;
  197. setActiveButton();
  198. }
  199. });
  200.  
  201. controlDiv.appendChild(button);
  202. });
  203.  
  204. const toggleButton = document.createElement('button');
  205. toggleButton.innerText = '◀';
  206. toggleButton.style.position = 'absolute';
  207. toggleButton.style.top = '5px';
  208. toggleButton.style.left = '-15px';
  209. toggleButton.style.backgroundColor = '#bada55';
  210. toggleButton.style.border = 'none';
  211. toggleButton.style.borderRadius = '20%';
  212. toggleButton.style.cursor = 'pointer';
  213. toggleButton.style.padding = '2px';
  214. toggleButton.style.zIndex = '1000';
  215.  
  216. toggleButton.addEventListener('click', () => {
  217. if (isPanelVisible) {
  218. controlDiv.style.transform = 'translateX(100%)';
  219. toggleButton.innerText = '▶';
  220. } else {
  221. controlDiv.style.transform = 'translateX(0)';
  222. toggleButton.innerText = '◀';
  223. }
  224. isPanelVisible = !isPanelVisible;
  225. });
  226.  
  227. controlDiv.appendChild(toggleButton);
  228.  
  229. // Font size control
  230. const fontSizeControl = document.createElement('div');
  231. fontSizeControl.style.marginTop = '4px';
  232.  
  233. const fontIncreaseButton = document.createElement('button');
  234. fontIncreaseButton.innerText = '⬆';
  235. fontIncreaseButton.style.margin = '2px';
  236. fontIncreaseButton.style.padding = '3px';
  237. fontIncreaseButton.style.cursor = 'pointer';
  238.  
  239. fontIncreaseButton.addEventListener('click', () => {
  240. currentFontSize += 2;
  241. console.log("Size: " + currentFontSize);
  242. controlDiv.style.fontSize = `${currentFontSize}pt`;
  243. });
  244.  
  245. const fontDecreaseButton = document.createElement('button');
  246. fontDecreaseButton.innerText = '⬇';
  247. fontDecreaseButton.style.margin = '2px';
  248. fontDecreaseButton.style.padding = '3px';
  249. fontDecreaseButton.style.cursor = 'pointer';
  250.  
  251. fontDecreaseButton.addEventListener('click', () => {
  252. currentFontSize = Math.max(4, currentFontSize - 2); // Prevent font size from going too small
  253. controlDiv.style.fontSize = `${currentFontSize}pt`;
  254. });
  255.  
  256. fontSizeControl.appendChild(fontIncreaseButton);
  257. fontSizeControl.appendChild(fontDecreaseButton);
  258.  
  259. controlDiv.appendChild(fontSizeControl);
  260.  
  261. document.body.appendChild(controlDiv);
  262. }
  263.  
  264. const style = document.createElement('style');
  265. style.innerHTML = `
  266. .rotate {
  267. display: inline-block;
  268. transform: rotate(45deg); /* Rotates emoji */
  269. }
  270. `;
  271. document.head.appendChild(style);
  272.  
  273. // Update the time stats periodically
  274. setInterval(updateTimeStats, 1000); // Update every second
  275.  
  276. // Wait for the document to fully load and ensure audio element exists
  277. const observer = new MutationObserver((mutations, observer) => {
  278. const audioElement = document.querySelector('audio');
  279. if (audioElement) {
  280. console.log("Chapter duration:");
  281. console.log(audioElement.duration / 60);
  282. audioElement.addEventListener('loadedmetadata', () => {
  283. audioElement.playbackRate = 2.0; // Set default playback speed to 2.0
  284. createControlPanel();
  285. updateTimeStats();
  286. });
  287. observer.disconnect(); // Stop observing once the audio is found
  288. }
  289. });
  290.  
  291. // Start observing the document for changes
  292. observer.observe(document, {
  293. childList: true,
  294. subtree: true
  295. });
  296.  
  297. })();