Video Control with Reload

为指定视频添加可移动的进度条、音量控制器和重新加载按钮

目前为 2024-06-29 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Video Control with Reload
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.9
  5. // @description 为指定视频添加可移动的进度条、音量控制器和重新加载按钮
  6. // @match https://app.kosmi.io/*
  7. // @grant none
  8. // @license MIT
  9. // ==/UserScript==
  10.  
  11. (function() {
  12. 'use strict';
  13.  
  14. let videoElement = null;
  15. let controller = null;
  16. let isDragging = false; // 是否正在拖动控制器
  17. let initialX = 0, initialY = 0; // 初始点击位置
  18. let lastX = 0, lastY = 0; // 上一次位置
  19.  
  20. // 创建控制器
  21. function createController() {
  22. controller = document.createElement('div');
  23. controller.id = 'video-controller';
  24. controller.style.cssText = `
  25. position: fixed;
  26. bottom: 20px;
  27. left: 50%;
  28. transform: translateX(-50%);
  29. background-color: rgba(0, 0, 0, 0.7);
  30. color: white;
  31. padding: 10px;
  32. border-radius: 5px;
  33. z-index: 9999;
  34. cursor: move;
  35. user-select: none;
  36. width: 300px;
  37. transition: width 0.3s, height 0.3s;
  38. display: none; /* 初始隐藏 */
  39. `;
  40. controller.innerHTML = `
  41. <div id="progress-container" style="width: 100%; height: 10px; background-color: #444; position: relative; cursor: pointer;">
  42. <div id="progress-indicator" style="width: 0%; height: 100%; background-color: #fff; position: absolute;"></div>
  43. </div>
  44. <div id="time-display" style="text-align: center; margin-top: 5px;">0:00 / 0:00</div>
  45. <div id="volume-container" style="display: flex; align-items: center; margin-top: 10px;">
  46. <span id="volume-icon" style="margin-right: 10px;">🔊</span>
  47. <input type="range" id="volume-slider" min="0" max="1" step="0.1" value="1" style="flex-grow: 1;">
  48. </div>
  49. <button id="reload-button" style="width: 100%; margin-top: 10px; padding: 5px; background-color: #4CAF50; border: none; color: white; cursor: pointer;">重新加载视频控制</button>
  50. `;
  51. document.body.appendChild(controller);
  52. return controller;
  53. }
  54.  
  55. // 创建显示/隐藏按钮
  56. function createToggleButton() {
  57. const button = document.createElement('button');
  58. button.textContent = '🎥'; // 修改按钮图标
  59. button.style.cssText = `
  60. position: fixed;
  61. left: 10px;
  62. top: 50%;
  63. transform: translateY(-50%);
  64. padding: 5px;
  65. font-size: 20px;
  66. background-color: rgba(0, 0, 0, 0.5);
  67. color: white;
  68. border: none;
  69. border-radius: 50%;
  70. cursor: move;
  71. z-index: 9999;
  72. `;
  73. document.body.appendChild(button);
  74.  
  75. // 使按钮可拖动
  76. let pos1 = 0, pos2 = 0;
  77.  
  78. button.addEventListener('touchstart', touchStart, false);
  79.  
  80. function touchStart(e) {
  81. isDragging = true;
  82. initialX = e.touches[0].clientX;
  83. initialY = e.touches[0].clientY;
  84. lastX = initialX;
  85. lastY = initialY;
  86. document.addEventListener('touchmove', touchMove, false);
  87. document.addEventListener('touchend', touchEnd, false);
  88. }
  89.  
  90. function touchMove(e) {
  91. if (!isDragging) return;
  92. e.preventDefault();
  93. let currentX = e.touches[0].clientX;
  94. let currentY = e.touches[0].clientY;
  95.  
  96. pos1 = lastX - currentX;
  97. pos2 = lastY - currentY;
  98. lastX = currentX;
  99. lastY = currentY;
  100.  
  101. button.style.top = (button.offsetTop - pos2) + "px";
  102. button.style.left = (button.offsetLeft - pos1) + "px";
  103. }
  104.  
  105. function touchEnd() {
  106. isDragging = false;
  107. document.removeEventListener('touchmove', touchMove, false);
  108. document.removeEventListener('touchend', touchEnd, false);
  109. }
  110.  
  111. button.addEventListener('click', function() {
  112. if (controller.style.display === 'none') {
  113. controller.style.display = 'block';
  114. } else {
  115. controller.style.display = 'none';
  116. }
  117. });
  118. }
  119.  
  120. // 使控制器可拖动
  121. function makeDraggable(element) {
  122. let pos1 = 0, pos2 = 0;
  123.  
  124. element.addEventListener('mousedown', dragMouseDown, false);
  125.  
  126. function dragMouseDown(e) {
  127. if (e.target.id === 'progress-container' || e.target.id === 'progress-indicator' || e.target.id === 'volume-slider' || e.target.id === 'reload-button') return;
  128. e.preventDefault();
  129. isDragging = true;
  130.  
  131. initialX = e.clientX;
  132. initialY = e.clientY;
  133. lastX = initialX;
  134. lastY = initialY;
  135. document.addEventListener('mouseup', closeDragElement, false);
  136. document.addEventListener('mousemove', throttle(elementDrag, 16), false); // 节流控制拖动频率
  137. }
  138.  
  139. function elementDrag(e) {
  140. if (!isDragging) return;
  141. e.preventDefault();
  142. pos1 = initialX - e.clientX;
  143. pos2 = initialY - e.clientY;
  144. initialX = e.clientX;
  145. initialY = e.clientY;
  146.  
  147. requestAnimationFrame(() => {
  148. element.style.top = (element.offsetTop - pos2) + "px";
  149. element.style.left = (element.offsetLeft - pos1) + "px";
  150. element.style.bottom = 'auto';
  151. });
  152. }
  153.  
  154. function closeDragElement() {
  155. document.removeEventListener('mouseup', closeDragElement, false);
  156. document.removeEventListener('mousemove', throttle(elementDrag, 16), false);
  157. isDragging = false;
  158. }
  159. }
  160.  
  161. // 节流函数,用于限制函数调用频率
  162. function throttle(func, limit) {
  163. let lastFunc;
  164. let lastRan;
  165. return function() {
  166. const context = this;
  167. const args = arguments;
  168. if (!lastRan) {
  169. func.apply(context, args);
  170. lastRan = Date.now();
  171. } else {
  172. clearTimeout(lastFunc);
  173. lastFunc = setTimeout(function() {
  174. if ((Date.now() - lastRan) >= limit) {
  175. func.apply(context, args);
  176. lastRan = Date.now();
  177. }
  178. }, limit - (Date.now() - lastRan));
  179. }
  180. };
  181. }
  182.  
  183. // 格式化时间
  184. function formatTime(seconds) {
  185. const minutes = Math.floor(seconds / 60);
  186. seconds = Math.floor(seconds % 60);
  187. return `${minutes}:${seconds.toString().padStart(2, '0')}`;
  188. }
  189.  
  190. // 主函数
  191. function main() {
  192. // 先移除已存在的控制器元素和按钮
  193. const existingController = document.getElementById('video-controller');
  194. const existingButton = document.getElementById('toggle-button');
  195. if (existingController) {
  196. existingController.remove();
  197. }
  198. if (existingButton) {
  199. existingButton.remove();
  200. }
  201.  
  202. videoElement = document.querySelector('video[src^="blob:https://app.kosmi.io/"]');
  203. if (!videoElement) {
  204. console.log('未找到指定视频');
  205. return;
  206. }
  207.  
  208. controller = createController();
  209. makeDraggable(controller);
  210. createToggleButton();
  211.  
  212. const progressContainer = document.getElementById('progress-container');
  213. const progressIndicator = document.getElementById('progress-indicator');
  214. const timeDisplay = document.getElementById('time-display');
  215. const volumeSlider = document.getElementById('volume-slider');
  216. const volumeIcon = document.getElementById('volume-icon');
  217. const reloadButton = document.getElementById('reload-button');
  218.  
  219. // 更新进度
  220. function updateProgress() {
  221. const progress = (videoElement.currentTime / videoElement.duration) * 100;
  222. progressIndicator.style.width = `${progress}%`;
  223. const current = formatTime(videoElement.currentTime);
  224. const total = formatTime(videoElement.duration);
  225. timeDisplay.textContent = `${current} / ${total}`;
  226. }
  227.  
  228. // 点击进度条更新视频位置
  229. progressContainer.addEventListener('click', function(e) {
  230. if (isDragging) return;
  231. const rect = progressContainer.getBoundingClientRect();
  232. const pos = (e.clientX - rect.left) / rect.width;
  233. videoElement.currentTime = pos * videoElement.duration;
  234. });
  235.  
  236. // 更新音量
  237. volumeSlider.addEventListener('input', function() {
  238. videoElement.volume = this.value;
  239. updateVolumeIcon(this.value);
  240. });
  241.  
  242. // 更新音量图标
  243. function updateVolumeIcon(volume) {
  244. if (volume > 0.5) {
  245. volumeIcon.textContent = '🔊';
  246. } else if (volume > 0) {
  247. volumeIcon.textContent = '🔉';
  248. } else {
  249. volumeIcon.textContent = '🔇';
  250. }
  251. }
  252.  
  253. // 初始化音量
  254. volumeSlider.value = videoElement.volume;
  255. updateVolumeIcon(videoElement.volume);
  256.  
  257. // 重新加载按钮事件
  258. reloadButton.addEventListener('click', function() {
  259. // 移除旧的事件监听器
  260. videoElement.removeEventListener('timeupdate', updateProgress);
  261. videoElement.removeEventListener('loadedmetadata', updateProgress);
  262.  
  263. // 重新执行主函数
  264. main();
  265.  
  266. // 重新绑定事件监听器
  267. videoElement.addEventListener('timeupdate', updateProgress);
  268. videoElement.addEventListener('loadedmetadata', updateProgress);
  269. });
  270.  
  271. videoElement.addEventListener('timeupdate', updateProgress);
  272. videoElement.addEventListener('loadedmetadata', updateProgress);
  273. }
  274.  
  275. // 等待视频加载完成
  276. function waitForVideo() {
  277. const video = document.querySelector('video[src^="blob:https://app.kosmi.io/"]');
  278. if (video) {
  279. main();
  280. } else {
  281. setTimeout(waitForVideo, 1000);
  282. }
  283. }
  284.  
  285. waitForVideo();
  286. })();
  287.