Video Control with Reload and Floating Window

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

目前为 2024-07-26 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Video Control with Reload and Floating Window
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.3
  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,
  18. initialY = 0;
  19. let lastX = 0,
  20. lastY = 0;
  21. let buttonCreated = false;
  22. let floatingWindow = null;
  23.  
  24. function createController() {
  25. controller = document.createElement('div');
  26. controller.id = 'video-controller';
  27. controller.style.cssText = `
  28. position: fixed;
  29. bottom: 20px;
  30. left: 50%;
  31. transform: translateX(-50%);
  32. background-color: rgba(0, 0, 0, 0.7);
  33. color: white;
  34. padding: 10px;
  35. border-radius: 5px;
  36. z-index: 9999;
  37. cursor: move;
  38. user-select: none;
  39. width: 300px;
  40. transition: width 0.3s, height 0.3s;
  41. display: none;
  42. `;
  43. controller.innerHTML = `
  44. <div id="progress-container" style="width: 100%; height: 10px; background-color: #444; position: relative; cursor: pointer;">
  45. <div id="progress-indicator" style="width: 0%; height: 100%; background-color: #fff; position: absolute;"></div>
  46. </div>
  47. <div id="time-display" style="text-align: center; margin-top: 5px;">0:00 / 0:00</div>
  48. <div id="volume-container" style="display: flex; align-items: center; margin-top: 10px;">
  49. <span id="volume-icon" style="margin-right: 10px;">🔊</span>
  50. <input type="range" id="volume-slider" min="0" max="1" step="0.1" value="1" style="flex-grow: 1;">
  51. </div>
  52. <button id="reload-button" style="width: 100%; margin-top: 10px; padding: 5px; background-color: #4CAF50; border: none; color: white; cursor: pointer;">重新加载视频控制</button>
  53. <button id="float-video-button" style="width: 100%; margin-top: 10px; padding: 5px; background-color: #2196F3; border: none; color: white; cursor: pointer;">创建悬浮视频窗口</button>
  54. <div style="display: flex; justify-content: space-between; margin-top: 10px;">
  55. <button class="size-button" data-action="increase-width">宽度+</button>
  56. <button class="size-button" data-action="decrease-width">宽度-</button>
  57. <button class="size-button" data-action="increase-height">高度+</button>
  58. <button class="size-button" data-action="decrease-height">高度-</button>
  59. </div>
  60. `;
  61. document.body.appendChild(controller);
  62. return controller;
  63. }
  64.  
  65. function createToggleButton() {
  66. if (!buttonCreated) {
  67. const button = document.createElement('button');
  68. button.textContent = '🎥';
  69. button.style.cssText = `
  70. position: fixed;
  71. left: 10px;
  72. top: 50%;
  73. transform: translateY(-50%);
  74. padding: 5px;
  75. font-size: 20px;
  76. background-color: rgba(0, 0, 0, 0.5);
  77. color: white;
  78. border: none;
  79. border-radius: 50%;
  80. cursor: move;
  81. z-index: 9999;
  82. `;
  83. document.body.appendChild(button);
  84.  
  85. let pos1 = 0,
  86. pos2 = 0;
  87.  
  88. button.onmousedown = function(e) {
  89. e.preventDefault();
  90. isDragging = true;
  91. initialX = e.clientX;
  92. initialY = e.clientY;
  93. lastX = initialX;
  94. lastY = initialY;
  95. document.onmouseup = closeDragElement;
  96. document.onmousemove = throttle(buttonDrag, 16);
  97. };
  98.  
  99. function buttonDrag(e) {
  100. if (!isDragging) return;
  101. e.preventDefault();
  102. pos1 = lastX - e.clientX;
  103. pos2 = lastY - e.clientY;
  104. lastX = e.clientX;
  105. lastY = e.clientY;
  106.  
  107. button.style.top = (button.offsetTop - pos2) + "px";
  108. button.style.left = (button.offsetLeft - pos1) + "px";
  109. }
  110.  
  111. function closeDragElement() {
  112. document.onmouseup = null;
  113. document.onmousemove = null;
  114. isDragging = false;
  115. }
  116.  
  117. button.addEventListener('click', function() {
  118. if (controller.style.display === 'none') {
  119. controller.style.display = 'block';
  120. } else {
  121. controller.style.display = 'none';
  122. }
  123. });
  124.  
  125. let isClicking = false;
  126. button.addEventListener('touchstart', function(e) {
  127. e.preventDefault();
  128. if (e.touches.length === 1) {
  129. isClicking = true;
  130. setTimeout(() => {
  131. if (isClicking) {
  132. if (controller.style.display === 'none' || controller.style.display === '') {
  133. controller.style.display = 'block';
  134. } else {
  135. controller.style.display = 'none';
  136. }
  137. }
  138. }, 200);
  139. } else {
  140. isClicking = false;
  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. button.addEventListener('touchmove', function(e) {
  150. e.preventDefault();
  151. if (!isDragging) return;
  152. isClicking = false;
  153. const touch = e.touches[0];
  154. const newX = touch.clientX - initialX;
  155. const newY = touch.clientY - initialY;
  156. button.style.left = newX + 'px';
  157. button.style.top = newY + 'px';
  158. });
  159.  
  160. button.addEventListener('touchend', function() {
  161. isDragging = false;
  162. });
  163. }
  164. buttonCreated = true;
  165. }
  166.  
  167. function makeDraggable(element) {
  168. let pos1 = 0,
  169. pos2 = 0;
  170.  
  171. element.onmousedown = dragMouseDown;
  172.  
  173. function dragMouseDown(e) {
  174. if (e.target.id === 'progress-container' || e.target.id === 'progress-indicator' || e.target.id === 'volume-slider' || e.target.tagName === 'BUTTON') return;
  175. e.preventDefault();
  176. isDragging = true;
  177.  
  178. initialX = e.clientX;
  179. initialY = e.clientY;
  180. lastX = initialX;
  181. lastY = initialY;
  182. document.onmouseup = closeDragElement;
  183. document.onmousemove = throttle(elementDrag, 16);
  184. }
  185.  
  186. function elementDrag(e) {
  187. if (!isDragging) return;
  188. e.preventDefault();
  189. pos1 = initialX - e.clientX;
  190. pos2 = initialY - e.clientY;
  191. initialX = e.clientX;
  192. initialY = e.clientY;
  193.  
  194. requestAnimationFrame(() => {
  195. element.style.top = (element.offsetTop - pos2) + "px";
  196. element.style.left = (element.offsetLeft - pos1) + "px";
  197. element.style.bottom = 'auto';
  198. });
  199. }
  200.  
  201. function closeDragElement() {
  202. document.onmouseup = null;
  203. document.onmousemove = null;
  204. isDragging = false;
  205. }
  206. }
  207.  
  208. function throttle(func, limit) {
  209. let lastFunc;
  210. let lastRan;
  211. return function() {
  212. const context = this;
  213. const args = arguments;
  214. if (!lastRan) {
  215. func.apply(context, args);
  216. lastRan = Date.now();
  217. } else {
  218. clearTimeout(lastFunc);
  219. lastFunc = setTimeout(function() {
  220. if ((Date.now() - lastRan) >= limit) {
  221. func.apply(context, args);
  222. lastRan = Date.now();
  223. }
  224. }, limit - (Date.now() - lastRan));
  225. }
  226. };
  227. }
  228.  
  229. function formatTime(seconds) {
  230. const minutes = Math.floor(seconds / 60);
  231. seconds = Math.floor(seconds % 60);
  232. return `${minutes}:${seconds.toString().padStart(2, '0')}`;
  233. }
  234.  
  235. function createFloatingWindow() {
  236. if (floatingWindow) {
  237. floatingWindow.remove();
  238. }
  239.  
  240. floatingWindow = document.createElement('div');
  241. floatingWindow.style.cssText = `
  242. position: fixed;
  243. top: 50px;
  244. left: 50px;
  245. width: 320px;
  246. height: 240px;
  247. background-color: #000;
  248. border: 2px solid #fff;
  249. z-index: 10000;
  250. resize: both;
  251. overflow: hidden;
  252. `;
  253.  
  254. const closeButton = document.createElement('button');
  255. closeButton.textContent = 'X';
  256. closeButton.style.cssText = `
  257. position: absolute;
  258. top: 5px;
  259. right: 5px;
  260. background-color: red;
  261. color: white;
  262. border: none;
  263. cursor: pointer;
  264. z-index: 10001;
  265. `;
  266. closeButton.onclick = () => floatingWindow.remove();
  267.  
  268. floatingWindow.appendChild(closeButton);
  269. document.body.appendChild(floatingWindow);
  270. videoElement = document.querySelector('video[src^="blob:"]');
  271. if (videoElement) {
  272. // 保存视频的原始尺寸
  273. const originalWidth = videoElement.offsetWidth;
  274. const originalHeight = videoElement.offsetHeight;
  275. // 调整视频大小以适应悬浮窗
  276. videoElement.style.width = '100%';
  277. videoElement.style.height = '100%';
  278.  
  279. // 将视频添加到悬浮窗中
  280. floatingWindow.appendChild(videoElement);
  281.  
  282. // 添加悬浮窗到body
  283. document.body.appendChild(floatingWindow);
  284.  
  285. makeDraggable(floatingWindow);
  286. }
  287. }
  288.  
  289. function main() {
  290. const existingController = document.getElementById('video-controller');
  291. const existingButton = document.getElementById('toggle-button');
  292. if (existingController) {
  293. existingController.remove();
  294. }
  295. if (existingButton) {
  296. existingButton.remove();
  297. }
  298.  
  299. videoElement = document.querySelector('video[src^="blob:https://app.kosmi.io/"]');
  300. if (!videoElement) {
  301. console.log('未找到指定视频');
  302. return;
  303. }
  304.  
  305. controller = createController();
  306. makeDraggable(controller);
  307. createToggleButton();
  308.  
  309. const progressContainer = document.getElementById('progress-container');
  310. const progressIndicator = document.getElementById('progress-indicator');
  311. const timeDisplay = document.getElementById('time-display');
  312. const volumeSlider = document.getElementById('volume-slider');
  313. const volumeIcon = document.getElementById('volume-icon');
  314. const reloadButton = document.getElementById('reload-button');
  315. const floatVideoButton = document.getElementById('float-video-button');
  316. const sizeButtons = document.querySelectorAll('.size-button');
  317.  
  318. function updateProgress() {
  319. const progress = (videoElement.currentTime / videoElement.duration) * 100;
  320. progressIndicator.style.width = `${progress}%`;
  321. const current = formatTime(videoElement.currentTime);
  322. const total = formatTime(videoElement.duration);
  323. timeDisplay.textContent = `${current} / ${total}`;
  324. }
  325.  
  326. progressContainer.addEventListener('click', function(e) {
  327. if (isDragging) return;
  328. const rect = progressContainer.getBoundingClientRect();
  329. const pos = (e.clientX - rect.left) / rect.width;
  330. videoElement.currentTime = pos * videoElement.duration;
  331. });
  332.  
  333. volumeSlider.addEventListener('input', function() {
  334. videoElement.volume = this.value;
  335. updateVolumeIcon(this.value);
  336. });
  337.  
  338. function updateVolumeIcon(volume) {
  339. if (volume > 0.5) {
  340. volumeIcon.textContent = '🔊';
  341. } else if (volume > 0) {
  342. volumeIcon.textContent = '🔉';
  343. } else {
  344. volumeIcon.textContent = '🔇';
  345. }
  346. }
  347.  
  348. volumeSlider.value = videoElement.volume;
  349. updateVolumeIcon(videoElement.volume);
  350.  
  351. reloadButton.addEventListener('click', function() {
  352. videoElement.removeEventListener('timeupdate', updateProgress);
  353. videoElement.removeEventListener('loadedmetadata', updateProgress);
  354. main();
  355. videoElement.addEventListener('timeupdate', updateProgress);
  356. videoElement.addEventListener('loadedmetadata', updateProgress);
  357. });
  358.  
  359. floatVideoButton.addEventListener('click', createFloatingWindow);
  360.  
  361. sizeButtons.forEach(button => {
  362. button.addEventListener('click', function() {
  363. if (!floatingWindow) return;
  364.  
  365. const action = this.dataset.action;
  366. const step = 20;
  367.  
  368. switch (action) {
  369. case 'increase-width':
  370. floatingWindow.style.width = (floatingWindow.offsetWidth + step) + 'px';
  371. break;
  372. case 'decrease-width':
  373. floatingWindow.style.width = Math.max(160, floatingWindow.offsetWidth - step) + 'px';
  374. break;
  375. case 'increase-height':
  376. floatingWindow.style.height = (floatingWindow.offsetHeight + step) + 'px';
  377. break;
  378. case 'decrease-height':
  379. floatingWindow.style.height = Math.max(120, floatingWindow.offsetHeight - step) + 'px';
  380. break;
  381. }
  382. });
  383. });
  384.  
  385. videoElement.addEventListener('timeupdate', updateProgress);
  386. videoElement.addEventListener('loadedmetadata', updateProgress);
  387. }
  388.  
  389. function waitForVideo() {
  390. const video = document.querySelector('video[src^="blob:https://app.kosmi.io/"]');
  391. if (video) {
  392. main();
  393. } else {
  394. setTimeout(waitForVideo, 1000);
  395. }
  396. }
  397.  
  398. waitForVideo();
  399. })();