Video Control with Reload

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

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

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