Video Control with Reload and Floating Window

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

  1. // ==UserScript==
  2. // @name Video Control with Reload and Floating Window
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.5
  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. height: 233px;
  41. transition: width 0.3s, height 0.3s;
  42. display: none;
  43. `;
  44. controller.innerHTML = `
  45. <progress id="progress-bar" value="0" max="100" style="width: 100%; height: 10px; background-color: #444; cursor: pointer;"></progress>
  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; cursor: pointer;">
  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; cursor: pointer;">
  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. <button id="float-video-button" style="width: 100%; margin-top: 10px; padding: 5px; background-color: #2196F3; border: none; color: white; cursor: pointer;">创建悬浮视频窗口</button>
  53. <button id="video-remove-black" style="width: 100%; margin-top: 10px; padding: 5px; background-color: #FFA500; 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" style="cursor: pointer;">宽度+</button>
  56. <button class="size-button" data-action="decrease-width" style="cursor: pointer;">宽度-</button>
  57. <button class="size-button" data-action="increase-height" style="cursor: pointer;">高度+</button>
  58. <button class="size-button" data-action="decrease-height" style="cursor: pointer;">高度-</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. // 让元素能在手机拖动
  168. function makeDraggable(element) {
  169. console.log('progressContainer1');
  170. let pos1 = 0,
  171. pos2 = 0,
  172. pos3 = 0,
  173. pos4 = 0;
  174. let isDragging = false;
  175.  
  176. element.onmousedown = dragMouseDown;
  177. element.ontouchstart = dragTouchStart;
  178.  
  179. function dragMouseDown(e) {
  180. console.log('progressContainer2');
  181. if (e.target.tagName === 'BUTTON' || e.target.tagName === 'INPUT') return;
  182. e.preventDefault();
  183. pos3 = e.clientX;
  184. pos4 = e.clientY;
  185. document.onmouseup = closeDragElement;
  186. document.onmousemove = elementDrag;
  187. isDragging = true;
  188. }
  189.  
  190. function dragTouchStart(e) {
  191. console.log(e.target.tagName);
  192. console.log(e);
  193. if (e.target.tagName === 'BUTTON' || e.target.tagName === 'PROGRESS' || e.target.tagName === 'INPUT') return;
  194. e.preventDefault();
  195. const touch = e.touches[0];
  196. pos3 = touch.clientX;
  197. pos4 = touch.clientY;
  198. document.ontouchend = closeDragElement;
  199. document.ontouchmove = elementTouchDrag;
  200. isDragging = true;
  201. }
  202.  
  203. function elementDrag(e) {
  204. console.log('progressContainer');
  205. if (!isDragging) return;
  206. e.preventDefault();
  207. pos1 = pos3 - e.clientX;
  208. pos2 = pos4 - e.clientY;
  209. pos3 = e.clientX;
  210. pos4 = e.clientY;
  211. element.style.top = (element.offsetTop - pos2) + "px";
  212. element.style.left = (element.offsetLeft - pos1) + "px";
  213. }
  214.  
  215. function elementTouchDrag(e) {
  216. console.log('progressContainer4');
  217. if (!isDragging) return;
  218. e.preventDefault();
  219. const touch = e.touches[0];
  220. pos1 = pos3 - touch.clientX;
  221. pos2 = pos4 - touch.clientY;
  222. pos3 = touch.clientX;
  223. pos4 = touch.clientY;
  224. element.style.top = (element.offsetTop - pos2) + "px";
  225. element.style.left = (element.offsetLeft - pos1) + "px";
  226. }
  227.  
  228. function closeDragElement() {
  229. console.log('progressContainer5');
  230. isDragging = false;
  231. document.onmouseup = null;
  232. document.onmousemove = null;
  233. document.ontouchend = null;
  234. document.ontouchmove = null;
  235. }
  236. }
  237.  
  238. function throttle(func, limit) {
  239. let lastFunc;
  240. let lastRan;
  241. return function() {
  242. const context = this;
  243. const args = arguments;
  244. if (!lastRan) {
  245. func.apply(context, args);
  246. lastRan = Date.now();
  247. } else {
  248. clearTimeout(lastFunc);
  249. lastFunc = setTimeout(function() {
  250. if ((Date.now() - lastRan) >= limit) {
  251. func.apply(context, args);
  252. lastRan = Date.now();
  253. }
  254. }, limit - (Date.now() - lastRan));
  255. }
  256. };
  257. }
  258.  
  259. function formatTime(seconds) {
  260. const minutes = Math.floor(seconds / 60);
  261. seconds = Math.floor(seconds % 60);
  262. return `${minutes}:${seconds.toString().padStart(2, '0')}`;
  263. }
  264.  
  265. function createFloatingWindow() {
  266. videoElement = document.querySelector('video[src^="blob:https://app.kosmi.io/"]');
  267. if (videoElement) {
  268. if (floatingWindow) {
  269. floatingWindow.remove();
  270. }
  271.  
  272. floatingWindow = document.createElement('div');
  273. floatingWindow.style.cssText = `
  274. position: fixed;
  275. top: 50px;
  276. left: 01px;
  277. width: 320px;
  278. height: 240px;
  279. background-color: #000;
  280. border: 0px solid #000;
  281. z-index: 10000;
  282. resize: both;
  283. cursor: move;
  284. overflow: hidden;
  285. `;
  286.  
  287. const closeButton = document.createElement('button');
  288. closeButton.textContent = 'X';
  289. closeButton.style.cssText = `
  290. position: absolute;
  291. top: 5px;
  292. right: 5px;
  293. background-color: grey;
  294. color: white;
  295. border: none;
  296. cursor: pointer;
  297. z-index: 10001;
  298. opacity: 0.1;
  299. `;
  300. closeButton.onclick = () => {
  301. // 停止播放视频
  302. videoElement.pause();
  303. videoElement.load();
  304. // 移除浮窗
  305. floatingWindow.remove();
  306. };
  307.  
  308. floatingWindow.appendChild(closeButton);
  309. document.body.appendChild(floatingWindow);
  310.  
  311. // 保存视频的原始尺寸
  312. const originalWidth = videoElement.offsetWidth;
  313. const originalHeight = videoElement.offsetHeight;
  314. // 调整视频大小以适应悬浮窗
  315. videoElement.style.width = '100%';
  316. videoElement.style.height = '100%';
  317.  
  318. // 将视频添加到悬浮窗中
  319. floatingWindow.appendChild(videoElement);
  320.  
  321. // 添加悬浮窗到body
  322. document.body.appendChild(floatingWindow);
  323.  
  324. makeDraggable(floatingWindow);
  325. }
  326. }
  327.  
  328. function removeVideoBlack() {
  329. videoElement = document.querySelector('video[src^="blob:https://app.kosmi.io/"]');
  330. if (videoElement) {
  331. videoElement.style.width = '100%';
  332. videoElement.style.height = '100%';
  333. videoElement.style.objectFit = 'cover';
  334. }
  335. }
  336.  
  337. function main() {
  338. const existingController = document.getElementById('video-controller');
  339. const existingButton = document.getElementById('toggle-button');
  340. if (existingController) {
  341. existingController.remove();
  342. }
  343. if (existingButton) {
  344. existingButton.remove();
  345. }
  346.  
  347. videoElement = document.querySelector('video[src^="blob:https://app.kosmi.io/"]');
  348. if (!videoElement) {
  349. console.log('未找到指定视频');
  350. return;
  351. }
  352.  
  353. controller = createController();
  354. makeDraggable(controller);
  355. createToggleButton();
  356.  
  357. const progressBar = document.getElementById('progress-bar');
  358. const timeDisplay = document.getElementById('time-display');
  359. const volumeSlider = document.getElementById('volume-slider');
  360. const volumeIcon = document.getElementById('volume-icon');
  361. const reloadButton = document.getElementById('reload-button');
  362. const floatVideoButton = document.getElementById('float-video-button');
  363. const videoRemoveBlack = document.getElementById('video-remove-black');
  364. const sizeButtons = document.querySelectorAll('.size-button');
  365.  
  366. function updateProgress() {
  367. const progress = (videoElement.currentTime / videoElement.duration) * 100;
  368. progressBar.value = progress; // 设置进度条的value属性
  369. const current = formatTime(videoElement.currentTime);
  370. const total = formatTime(videoElement.duration);
  371. timeDisplay.textContent = `${current} / ${total}`;
  372. }
  373.  
  374. // 添加点击事件监听器以改变视频播放位置
  375. progressBar.addEventListener('click', function(e) {
  376. if (isDragging) return;
  377.  
  378. // 获取进度条的位置
  379. const rect = progressBar.getBoundingClientRect();
  380. const pos = (e.clientX - rect.left) / rect.width;
  381.  
  382. // 设置视频播放位置
  383. videoElement.currentTime = pos * videoElement.duration;
  384. });
  385.  
  386. volumeSlider.addEventListener('input', function() {
  387. videoElement.volume = this.value;
  388. updateVolumeIcon(this.value);
  389. });
  390.  
  391. function updateVolumeIcon(volume) {
  392. if (volume > 0.5) {
  393. volumeIcon.textContent = '🔊';
  394. } else if (volume > 0) {
  395. volumeIcon.textContent = '🔉';
  396. } else {
  397. volumeIcon.textContent = '🔇';
  398. }
  399. }
  400.  
  401. volumeSlider.value = videoElement.volume;
  402. updateVolumeIcon(videoElement.volume);
  403.  
  404. reloadButton.addEventListener('click', function() {
  405. videoElement.removeEventListener('timeupdate', updateProgress);
  406. videoElement.removeEventListener('loadedmetadata', updateProgress);
  407. main();
  408. videoElement.addEventListener('timeupdate', updateProgress);
  409. videoElement.addEventListener('loadedmetadata', updateProgress);
  410. });
  411.  
  412. floatVideoButton.addEventListener('click', createFloatingWindow);
  413.  
  414. videoRemoveBlack.addEventListener('click', removeVideoBlack);
  415.  
  416. sizeButtons.forEach(button => {
  417. button.addEventListener('click', function() {
  418. if (!floatingWindow) return;
  419.  
  420. const action = this.dataset.action;
  421. const step = 20;
  422.  
  423. switch (action) {
  424. case 'increase-width':
  425. floatingWindow.style.width = (floatingWindow.offsetWidth + step) + 'px';
  426. break;
  427. case 'decrease-width':
  428. floatingWindow.style.width = Math.max(160, floatingWindow.offsetWidth - step) + 'px';
  429. break;
  430. case 'increase-height':
  431. floatingWindow.style.height = (floatingWindow.offsetHeight + step) + 'px';
  432. break;
  433. case 'decrease-height':
  434. floatingWindow.style.height = Math.max(120, floatingWindow.offsetHeight - step) + 'px';
  435. break;
  436. }
  437. });
  438. });
  439.  
  440. videoElement.addEventListener('timeupdate', updateProgress);
  441. videoElement.addEventListener('loadedmetadata', updateProgress);
  442. }
  443.  
  444. function waitForVideo() {
  445. const video = document.querySelector('video[src^="blob:https://app.kosmi.io/"]');
  446. if (video) {
  447. main();
  448. } else {
  449. setTimeout(waitForVideo, 1000);
  450. }
  451. }
  452.  
  453. waitForVideo();
  454. })();