更好的 Youtube Shorts

為 Youtube Shorts提供更多的控制功能,包括自動/手動跳轉到對應影片頁面,音量控制,播放速度控制,進度條,自動滾動,快捷鍵等等。

  1. // ==UserScript==
  2. // @name Better Youtube Shorts
  3. // @name:zh-CN 更好的 Youtube Shorts
  4. // @name:zh-TW 更好的 Youtube Shorts
  5. // @namespace Violentmonkey Scripts
  6. // @version 2.4.4
  7. // @description Provide more control functions for YouTube Shorts, including automatic/manual redirection to corresponding video pages, volume control, playback speed control, progress bar, auto scrolling, shortcut keys, and more.
  8. // @description:zh-CN 为 Youtube Shorts提供更多的控制功能,包括自动/手动跳转到对应视频页面,音量控制,播放速度控制,进度条,自动滚动,快捷键等等。
  9. // @description:zh-TW 為 Youtube Shorts提供更多的控制功能,包括自動/手動跳轉到對應影片頁面,音量控制,播放速度控制,進度條,自動滾動,快捷鍵等等。
  10. // @author Meriel
  11. // @match *://*.youtube.com/*
  12. // @exclude *://music.youtube.com/*
  13. // @run-at document-start
  14. // @grant GM.addStyle
  15. // @grant GM.registerMenuCommand
  16. // @grant GM.getValue
  17. // @grant GM.setValue
  18. // @grant GM_info
  19. // @license MIT
  20. // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
  21. // @homepageURL https://github.com/MerielVaren/better-youtube-shorts
  22. // @supportURL https://github.com/MerielVaren/better-youtube-shorts/issues
  23. // ==/UserScript==
  24.  
  25. (async () => {
  26. const shouldNotifyUserAboutChanges = true;
  27. const userLanguage = navigator.language || navigator.userLanguage;
  28. const i18nText = {
  29. zhSimplified: {
  30. closeText: `<br>双击关闭此消息👆`,
  31. updateText: `BTYS 版本 ${GM_info.script.version}<br>
  32. Hi,这次更新修复了一个小问题🛠️<br>
  33. 当打开自动滚动与记忆视频进度时<br>
  34. 如果一个视频播放完并跳转到了下一个<br>
  35. 此时回到上一个视频应该是从头开始的而不是从最后开始🤔<br>
  36. 这个逻辑才是正确的📢<br>
  37. 现在已经修复了这个问题🎉<br>
  38. `,
  39. newInstallationText: `
  40. 欢迎使用 Better YouTube Shorts🎉<br>
  41. 请检查 Tampermonkey 菜单中的设置🛠️<br>
  42. 里面还有更多功能📢<br>
  43. 下面是快捷键的说明👇<br>
  44. <br>
  45. 箭头上/下: 向上/向下滚动<br>
  46. 箭头左/右: 后退/前进<br>
  47. Shift + 箭头上/左: 音量增加/减少<br>
  48. Shift + 箭头下/右: 音量减少/增加<br>
  49. Alt + 回车: 切换全屏<br>
  50. Alt + W: 在当前标签页中打开观看页面<br>
  51. 0~9: 跳转到对应的进度<br>
  52. C: 增加视频播放速度<br>
  53. X: 减少视频播放速度<br>
  54. Z: 恢复视频播放速度<br>
  55. V: 显示/隐藏视频介绍下方的shorts<br>
  56. `,
  57. on: "开启",
  58. off: "关闭",
  59. constantVolume: "恒定音量",
  60. constantSpeed: "恒定速度",
  61. operationMode: "快捷键",
  62. videoMode: "视频操作模式",
  63. shortsMode: "短视频操作模式",
  64. continueFromLastCheckpoint: "从上次检查点继续",
  65. off: "关闭",
  66. temporary: "临时保存",
  67. permanent: "永久保存",
  68. loopPlayback: "循环播放",
  69. openWatchInCurrentTab: "在当前标签页中打开对应视频",
  70. doubleClickToFullscreen: "双击全屏",
  71. progressBarStyle: "进度条样式",
  72. original: "原始",
  73. custom: "自定义",
  74. autoScroll: "自动滚动",
  75. shortsAutoSwitchToVideo: "短视频自动切换到对应视频",
  76. },
  77. zhTraditional: {
  78. closeText: `<br>雙擊關閉此消息👆`,
  79. updateText: `BTYS 版本 ${GM_info.script.version}<br>
  80. Hi,這次更新修復了一個小問題🛠️<br>
  81. 當打開自動滾動與記憶視頻進度時<br>
  82. 如果一個視頻播放完並跳轉到了下一個<br>
  83. 此時回到上一個視頻應該是從頭開始的而不是從最後開始🤔<br>
  84. 這個邏輯才是正確的📢<br>
  85. `,
  86. newInstallationText: `
  87. 歡迎使用 Better YouTube Shorts🎉<br>
  88. 請檢查 Tampermonkey 菜單中的設置🛠️<br>
  89. 裡面還有更多功能📢<br>
  90. 下面是快捷鍵的說明👇<br>
  91. <br>
  92. 箭頭上/下: 向上/向下滾動<br>
  93. 箭頭左/右: 後退/前進<br>
  94. Shift + 箭頭上/左: 音量增加/減少<br>
  95. Shift + 箭頭下/右: 音量減少/增加<br>
  96. Alt + 回車: 切換全屏<br>
  97. Alt + W: 在當前標籤頁中打開觀看頁面<br>
  98. 0~9: 跳轉到對應的進度<br>
  99. C: 增加視頻播放速度<br>
  100. X: 減少視頻播放速度<br>
  101. Z: 恢復視頻播放速度<br>
  102. V: 顯示/隱藏視頻介紹下方的shorts<br>
  103. `,
  104. on: "開啟",
  105. off: "關閉",
  106. constantVolume: "恆定音量",
  107. constantSpeed: "恆定速度",
  108. operationMode: "快捷鍵",
  109. videoMode: "視頻操作模式",
  110. shortsMode: "短視頻操作模式",
  111. continueFromLastCheckpoint: "從上次檢查點繼續",
  112. off: "關閉",
  113. temporary: "臨時保存",
  114. permanent: "永久保存",
  115. loopPlayback: "循環播放",
  116. openWatchInCurrentTab: "在當前標籤頁中打開對應視頻",
  117. doubleClickToFullscreen: "雙擊全屏",
  118. progressBarStyle: "進度條樣式",
  119. original: "原始",
  120. custom: "自定義",
  121. autoScroll: "自動滾動",
  122. shortsAutoSwitchToVideo: "短視頻自動切換到對應視頻",
  123. },
  124. en: {
  125. closeText: `<br>Double click to close this message👆`,
  126. updateText: `BTYS Version ${GM_info.script.version}<br>
  127. Hi, this update fixes a small issue🛠️<br>
  128. When auto-scrolling and remembering video progress are enabled<br>
  129. If a video finishes and jumps to the next one<br>
  130. Returning to the previous video should start from the beginning rather than the end🤔<br>
  131. This logic is correct📢<br>
  132. This issue has been fixed🎉<br>
  133. `,
  134. newInstallationText: `
  135. Welcome to Better YouTube Shorts🎉<br>
  136. Please check the settings in the Tampermonkey menu🛠️<br>
  137. There are more features in it📢<br>
  138. Below is the explanation of the shortcut keys👇<br>
  139. <br>
  140. Arrow Up/Down: Scroll up/down<br>
  141. Arrow Left/Right: Seek backward/forward<br>
  142. Shift + Arrow Up/Left: Volume up/backward<br>
  143. Shift + Arrow Down/Right: Volume down/forward<br>
  144. Alt + Enter: Toggle fullscreen<br>
  145. Alt + W: Open watch page in current tab<br>
  146. 0~9: Jump to the corresponding progress<br>
  147. C: Increase video playback speed<br>
  148. X: Decrease video playback speed<br>
  149. Z: Restore video playback speed<br>
  150. V: Show/hide video description below shorts<br>
  151. `,
  152. on: "on",
  153. off: "off",
  154. constantVolume: "Constant Volume",
  155. constantSpeed: "Constant Speed",
  156. operationMode: "Operation Mode",
  157. videoMode: "video operation mode",
  158. shortsMode: "shorts operation mode",
  159. continueFromLastCheckpoint: "Continue From Last Checkpoint",
  160. off: "off",
  161. temporary: "temporary",
  162. permanent: "permanent",
  163. loopPlayback: "Loop Playback",
  164. openWatchInCurrentTab: "Open Watch in Current Tab",
  165. doubleClickToFullscreen: "Double Click to Fullscreen",
  166. progressBarStyle: "Progress Bar Style",
  167. original: "original",
  168. custom: "custom",
  169. autoScroll: "Auto Scroll",
  170. shortsAutoSwitchToVideo: "Shorts Auto Switch To Video",
  171. },
  172. };
  173. const i18n = userLanguage.toUpperCase().includes("ZH")
  174. ? ["ZH", "ZH-CN", "ZH-SG", "ZH-MY", "ZH-HANS"].includes(
  175. userLanguage.toUpperCase()
  176. )
  177. ? i18nText.zhSimplified
  178. : i18nText.zhTraditional
  179. : i18nText.en;
  180.  
  181. const isDarkMode =
  182. window.matchMedia("(prefers-color-scheme: dark)").matches ||
  183. document.documentElement.hasAttribute("dark");
  184. let currentUrl = "";
  185.  
  186. const once = (fn) => {
  187. let done = false;
  188. let result;
  189. return async (...args) => {
  190. if (done) return result;
  191. done = true;
  192. result = await fn(...args);
  193. return result;
  194. };
  195. };
  196.  
  197. const closeText = i18n.closeText;
  198. let updateText = i18n.updateText;
  199. let newInstallationText = i18n.newInstallationText;
  200. updateText += closeText;
  201. newInstallationText += closeText;
  202.  
  203. const higherVersion = (v1, v2) => {
  204. const v1Arr = v1.split(".");
  205. const v2Arr = v2.split(".");
  206. for (let i = 0; i < v1Arr.length; i++) {
  207. if (v1Arr[i] > v2Arr[i]) {
  208. return true;
  209. } else if (v1Arr[i] < v2Arr[i]) {
  210. return false;
  211. }
  212. }
  213. return false;
  214. };
  215.  
  216. const version = await GM.getValue("version");
  217. let interval;
  218. const checkVideoPaused = (video, waitTime = 100) => {
  219. if (!video.paused) {
  220. video.pause();
  221. interval = setTimeout(() => checkVideoPaused(video, waitTime), waitTime);
  222. } else {
  223. clearTimeout(interval);
  224. }
  225. };
  226. const newInstallation = once(async (reel, video) => {
  227. if (!version) {
  228. if (!interval) {
  229. interval = setTimeout(() => checkVideoPaused(video, 100), 100);
  230. }
  231. GM.setValue("version", GM_info.script.version);
  232. const info = document.createElement("div");
  233. info.style.cssText = `position: absolute; top: 0; left: 0; right: 0; bottom: 0; display: flex; justify-content: center; align-items: center; background-color: rgba(0, 0, 0, 0.5); z-index: 999; margin: 5px 0; color: black; font-size: 2rem; font-weight: bold; text-align: center; border-radius: 10px; padding: 10px; box-shadow: 0 0 10px 5px rgba(0, 0, 0, 0.5); transition: 0.5s;`;
  234. const infoText = document.createElement("div");
  235. infoText.style.cssText = `background-color: white; padding: 10px; border-radius: 10px; font-size: 1.5rem;`;
  236. infoText.innerHTML = newInstallationText;
  237. info.appendChild(infoText);
  238. reel.appendChild(info);
  239. info.addEventListener("dblclick", () => {
  240. info.remove();
  241. video.play();
  242. });
  243. }
  244. });
  245. const update = once(async (reel, video) => {
  246. GM.setValue("version", GM_info.script.version);
  247. if (
  248. typeof version === "string" &&
  249. higherVersion(GM_info.script.version, version) &&
  250. shouldNotifyUserAboutChanges
  251. ) {
  252. if (!interval) {
  253. interval = setTimeout(() => checkVideoPaused(video, 100), 100);
  254. }
  255. GM.setValue("version", GM_info.script.version);
  256. const info = document.createElement("div");
  257. info.style.cssText = `position: absolute; top: 0; left: 0; right: 0; bottom: 0; display: flex; justify-content: center; align-items: center; background-color: rgba(0, 0, 0, 0.5); z-index: 999; margin: 5px 0; color: black; font-size: 2rem; font-weight: bold; text-align: center; border-radius: 10px; padding: 10px; box-shadow: 0 0 10px 5px rgba(0, 0, 0, 0.5); transition: 0.5s;`;
  258. const infoText = document.createElement("div");
  259. infoText.style.cssText = `background-color: white; padding: 10px; border-radius: 10px; font-size: 1.5rem;`;
  260. infoText.innerHTML = updateText;
  261. info.appendChild(infoText);
  262. reel.appendChild(info);
  263. info.addEventListener("dblclick", () => {
  264. info.remove();
  265. video.play();
  266. });
  267. }
  268. });
  269.  
  270. let shortsAutoSwitchToVideo = await GM.getValue("shortsAutoSwitchToVideo");
  271. if (shortsAutoSwitchToVideo === void 0) {
  272. shortsAutoSwitchToVideo = false;
  273. GM.setValue("shortsAutoSwitchToVideo", shortsAutoSwitchToVideo);
  274. }
  275. GM.registerMenuCommand(
  276. `${i18n.shortsAutoSwitchToVideo}: ${
  277. shortsAutoSwitchToVideo ? i18n.on : i18n.off
  278. }`,
  279. () => {
  280. shortsAutoSwitchToVideo = !shortsAutoSwitchToVideo;
  281. GM.setValue("shortsAutoSwitchToVideo", shortsAutoSwitchToVideo).then(
  282. () => (location.href = location.href.replace("watch?v=", "shorts/"))
  283. );
  284. }
  285. );
  286.  
  287. if (shortsAutoSwitchToVideo) {
  288. if (window.location.pathname.match("/shorts/.+")) {
  289. window.location.replace(
  290. "https://www.youtube.com/watch?v=" +
  291. window.location.pathname.split("/shorts/").pop()
  292. );
  293. }
  294. document.addEventListener("yt-navigate-start", (event) => {
  295. const url = event.detail.url.split("/shorts/");
  296. if (url.length > 1) {
  297. window.location.replace("https://www.youtube.com/watch?v=" + url.pop());
  298. }
  299. });
  300. return;
  301. }
  302.  
  303. const initialize = once(async () => {
  304. GM.addStyle(
  305. `input[type="range"].volslider {
  306. height: 12px;
  307. -webkit-appearance: none;
  308. -moz-appearance: none; /* Firefox */
  309. appearance: none;
  310. margin: 10px 0;
  311. }
  312. input[type="range"].volslider:focus {
  313. outline: none;
  314. }
  315. input[type="range"].volslider::-webkit-slider-runnable-track {
  316. height: 8px;
  317. cursor: pointer;
  318. box-shadow: 0px 0px 0px #000000;
  319. background: ${isDarkMode ? "rgb(50, 50, 50)" : "#ccc"};
  320. border-radius: 25px;
  321. }
  322. input[type="range"].volslider::-webkit-slider-thumb {
  323. -webkit-appearance: none;
  324. width: 12px;
  325. height: 12px;
  326. margin-top: -2px;
  327. border-radius: 50%;
  328. background: ${isDarkMode ? "white" : "black"};
  329. }
  330.  
  331. /* Firefox */
  332. input[type="range"].volslider::-moz-range-track {
  333. height: 8px;
  334. cursor: pointer;
  335. box-shadow: 0px 0px 0px #000000;
  336. background: ${isDarkMode ? "rgb(50, 50, 50)" : "#ccc"};
  337. border-radius: 25px;
  338. }
  339. input[type="range"].volslider::-moz-range-thumb {
  340. width: 12px;
  341. height: 12px;
  342. border: none;
  343. border-radius: 50%;
  344. background: ${isDarkMode ? "white" : "black"};
  345. }
  346.  
  347. .switch {
  348. position: relative;
  349. display: inline-block;
  350. width: 40px;
  351. height: 12px;
  352. }
  353. .switch input {
  354. opacity: 0;
  355. width: 0;
  356. height: 0;
  357. }
  358.  
  359. /* The slider */
  360. .slider {
  361. position: absolute;
  362. cursor: pointer;
  363. top: 0;
  364. left: 0;
  365. right: 0;
  366. bottom: 0;
  367. background-color: ${isDarkMode ? "rgb(50, 50, 50)" : "#ccc"};
  368. -webkit-transition: 0.4s;
  369. transition: 0.4s;
  370. }
  371. .slider:before {
  372. position: absolute;
  373. content: "";
  374. height: 12px;
  375. width: 12px;
  376. left: 0px;
  377. bottom: 0px;
  378. background-color: ${isDarkMode ? "white" : "black"};
  379. -webkit-transition: 0.4s;
  380. transition: 0.4s;
  381. }
  382. input:checked + .slider {
  383. background-color: #ff0000;
  384. }
  385. input:focus + .slider {
  386. box-shadow: 0 0 0px #ff0000;
  387. }
  388. input:checked + .slider:before {
  389. -webkit-transform: translateX(29px);
  390. -ms-transform: translateX(29px);
  391. transform: translateX(29px);
  392. }
  393.  
  394. /* Rounded sliders */
  395. .slider.round {
  396. border-radius: 12px;
  397. }
  398. .slider.round:before {
  399. border-radius: 50%;
  400. }
  401.  
  402. /* red progress bar */
  403. #byts-progbar:hover #byts-progress::after,
  404. #byts-progbar.show-dot #byts-progress::after {
  405. content: '';
  406. position: absolute;
  407. top: 50%;
  408. right: 0;
  409. transform: translate(50%, -50%);
  410. width: 15px;
  411. height: 15px;
  412. background-color: #FF0000;
  413. border-radius: 50%;
  414. display: block;
  415. }
  416.  
  417. /* speed slider */
  418. input[type="range"].speedslider {
  419. height: 12px;
  420. -webkit-appearance: none;
  421. -moz-appearance: none; /* Firefox */
  422. appearance: none;
  423. margin: 10px 0;
  424. }
  425. input[type="range"].speedslider:focus {
  426. outline: none;
  427. }
  428. input[type="range"].speedslider::-webkit-slider-runnable-track {
  429. height: 8px;
  430. cursor: pointer;
  431. box-shadow: 0px 0px 0px #000000;
  432. background: ${isDarkMode ? "rgb(50, 50, 50)" : "#ccc"};
  433. border-radius: 25px;
  434. }
  435. input[type="range"].speedslider::-webkit-slider-thumb {
  436. -webkit-appearance: none;
  437. width: 12px;
  438. height: 12px;
  439. margin-top: -2px;
  440. border-radius: 50%;
  441. background: ${isDarkMode ? "white" : "black"};
  442. }
  443.  
  444. /* Firefox */
  445. input[type="range"].speedslider::-moz-range-track {
  446. height: 8px;
  447. cursor: pointer;
  448. box-shadow: 0px 0px 0px #000000;
  449. background: ${isDarkMode ? "rgb(50, 50, 50)" : "#ccc"};
  450. border-radius: 25px;
  451. }
  452. input[type="range"].speedslider::-moz-range-thumb {
  453. width: 12px;
  454. height: 12px;
  455. border: none;
  456. border-radius: 50%;
  457. background: ${isDarkMode ? "white" : "black"};
  458. }
  459. `
  460. );
  461.  
  462. let seekMouseDown = false;
  463. let lastCurSeconds = 0;
  464. let video = null;
  465. let autoScroll = await GM.getValue("autoScroll");
  466. let loopPlayback = await GM.getValue("loopPlayback");
  467. let constantVolume = await GM.getValue("constantVolume");
  468. let constantSpeed = await GM.getValue("constantSpeed");
  469. let operationMode = await GM.getValue("operationMode");
  470. let openWatchInCurrentTab = await GM.getValue("openWatchInCurrentTab");
  471. let doubleClickToFullscreen = await GM.getValue("doubleClickToFullscreen");
  472. let progressBarStyle = await GM.getValue("progressBarStyle");
  473. let hideMetaDescription = false;
  474. const checkpointStatusEnum = Object.freeze({
  475. [i18n.off]: 0,
  476. [i18n.temporary]: 1,
  477. [i18n.permanent]: 2,
  478. });
  479. let continueFromLastCheckpoint = await GM.getValue(
  480. "continueFromLastCheckpoint"
  481. );
  482. let lastShortsId = "";
  483.  
  484. if (autoScroll === void 0) {
  485. autoScroll = true;
  486. GM.setValue("autoScroll", autoScroll);
  487. }
  488. if (constantVolume === void 0) {
  489. constantVolume = false;
  490. GM.setValue("constantVolume", constantVolume);
  491. }
  492. if (constantSpeed === void 0) {
  493. constantSpeed = false;
  494. GM.setValue("constantSpeed", constantSpeed);
  495. }
  496. if (operationMode === void 0) {
  497. operationMode = "Shorts";
  498. GM.setValue("operationMode", operationMode);
  499. }
  500. if (continueFromLastCheckpoint === void 0) {
  501. continueFromLastCheckpoint = checkpointStatusEnum[i18n.off];
  502. GM.setValue("continueFromLastCheckpoint", continueFromLastCheckpoint);
  503. }
  504. if (loopPlayback === void 0) {
  505. loopPlayback = true;
  506. GM.setValue("loopPlayback", loopPlayback);
  507. }
  508. if (openWatchInCurrentTab === void 0) {
  509. openWatchInCurrentTab = false;
  510. GM.setValue("openWatchInCurrentTab", openWatchInCurrentTab);
  511. }
  512. let shortsCheckpoints;
  513. if (continueFromLastCheckpoint !== checkpointStatusEnum[i18n.off]) {
  514. shortsCheckpoints = await GM.getValue("shortsCheckpoints");
  515. if (
  516. shortsCheckpoints === void 0 ||
  517. continueFromLastCheckpoint === checkpointStatusEnum[i18n.temporary]
  518. ) {
  519. shortsCheckpoints = {};
  520. GM.setValue("shortsCheckpoints", shortsCheckpoints);
  521. }
  522. }
  523. if (doubleClickToFullscreen === void 0) {
  524. doubleClickToFullscreen = true;
  525. GM.setValue("doubleClickToFullscreen", doubleClickToFullscreen);
  526. }
  527. if (progressBarStyle === void 0) {
  528. progressBarStyle = "custom";
  529. GM.setValue("progressBarStyle", progressBarStyle);
  530. }
  531.  
  532. GM.registerMenuCommand(
  533. `${i18n.constantVolume}: ${constantVolume ? i18n.on : i18n.off}`,
  534. () => {
  535. constantVolume = !constantVolume;
  536. GM.setValue("constantVolume", constantVolume).then(() =>
  537. location.reload()
  538. );
  539. }
  540. );
  541. GM.registerMenuCommand(
  542. `${i18n.constantSpeed}: ${constantSpeed ? i18n.on : i18n.off}`,
  543. () => {
  544. constantSpeed = !constantSpeed;
  545. GM.setValue("constantSpeed", constantSpeed).then(() =>
  546. location.reload()
  547. );
  548. }
  549. );
  550. GM.registerMenuCommand(
  551. `${i18n.operationMode}: ${
  552. operationMode === "Video" ? i18n.videoMode : i18n.shortsMode
  553. }`,
  554. () => {
  555. operationMode = operationMode === "Video" ? "Shorts" : "Video";
  556. GM.setValue("operationMode", operationMode).then(() =>
  557. location.reload()
  558. );
  559. }
  560. );
  561. GM.registerMenuCommand(
  562. `${i18n.continueFromLastCheckpoint}: ${Object.keys(checkpointStatusEnum)
  563. .find(
  564. (key) => checkpointStatusEnum[key] === continueFromLastCheckpoint % 3
  565. )
  566. .toLowerCase()}`,
  567. () => {
  568. continueFromLastCheckpoint = (continueFromLastCheckpoint + 1) % 3;
  569. GM.setValue(
  570. "continueFromLastCheckpoint",
  571. continueFromLastCheckpoint
  572. ).then(() => location.reload());
  573. }
  574. );
  575. GM.registerMenuCommand(
  576. `${i18n.loopPlayback}: ${loopPlayback ? i18n.on : i18n.off}`,
  577. () => {
  578. loopPlayback = !loopPlayback;
  579. GM.setValue("loopPlayback", loopPlayback).then(() => location.reload());
  580. }
  581. );
  582. GM.registerMenuCommand(
  583. `${i18n.openWatchInCurrentTab}: ${
  584. openWatchInCurrentTab ? i18n.on : i18n.off
  585. }`,
  586. () => {
  587. openWatchInCurrentTab = !openWatchInCurrentTab;
  588. GM.setValue("openWatchInCurrentTab", openWatchInCurrentTab).then(() =>
  589. location.reload()
  590. );
  591. }
  592. );
  593. GM.registerMenuCommand(
  594. `${i18n.doubleClickToFullscreen}: ${
  595. doubleClickToFullscreen ? i18n.on : i18n.off
  596. }`,
  597. () => {
  598. doubleClickToFullscreen = !doubleClickToFullscreen;
  599. GM.setValue("doubleClickToFullscreen", doubleClickToFullscreen).then(
  600. () => location.reload()
  601. );
  602. }
  603. );
  604. GM.registerMenuCommand(
  605. `${i18n.progressBarStyle}: ${
  606. progressBarStyle === "custom" ? i18n.custom : i18n.original
  607. }`,
  608. () => {
  609. progressBarStyle =
  610. progressBarStyle === "custom" ? "original" : "custom";
  611. GM.setValue("progressBarStyle", progressBarStyle).then(() =>
  612. location.reload()
  613. );
  614. }
  615. );
  616.  
  617. const observer = new MutationObserver(
  618. async (mutations, shortsReady = false, videoPlayerReady = false) => {
  619. outer: for (const mutation of mutations) {
  620. for (const node of mutation.addedNodes) {
  621. if (!shortsReady) {
  622. shortsReady = node.tagName === "YTD-SHORTS";
  623. }
  624. if (!videoPlayerReady) {
  625. videoPlayerReady =
  626. typeof node.className === "string" &&
  627. node.className.includes("html5-main-video");
  628. }
  629. if (shortsReady && videoPlayerReady) {
  630. observer.disconnect();
  631. video = node;
  632. if (constantVolume) {
  633. video.volume = await GM.getValue("volume", 0);
  634. }
  635. addShortcuts();
  636. updateVidElemWithRAF();
  637. break outer;
  638. }
  639. }
  640. }
  641. }
  642. );
  643. observer.observe(document.documentElement, {
  644. childList: true,
  645. subtree: true,
  646. });
  647.  
  648. function videoOperationMode(e) {
  649. const volumeSlider = document.getElementById("byts-vol");
  650. if (!e.shiftKey) {
  651. if (
  652. e.key.toUpperCase() === "ARROWUP" ||
  653. e.key.toUpperCase() === "ARROWDOWN"
  654. ) {
  655. e.stopPropagation();
  656. e.preventDefault();
  657. switch (e.key.toUpperCase()) {
  658. case "ARROWUP":
  659. video.volume = Math.min(1, video.volume + 0.01);
  660. volumeSlider.value = video.volume;
  661. break;
  662. case "ARROWDOWN":
  663. video.volume = Math.max(0, video.volume - 0.01);
  664. volumeSlider.value = video.volume;
  665. break;
  666. default:
  667. break;
  668. }
  669. } else if (
  670. e.key.toUpperCase() === "ARROWLEFT" ||
  671. e.key.toUpperCase() === "ARROWRIGHT"
  672. ) {
  673. switch (e.key.toUpperCase()) {
  674. case "ARROWLEFT":
  675. video.currentTime -= 1;
  676. break;
  677. case "ARROWRIGHT":
  678. video.currentTime += 1;
  679. break;
  680. default:
  681. break;
  682. }
  683. }
  684. } else {
  685. switch (e.key.toUpperCase()) {
  686. case "ARROWLEFT":
  687. case "ARROWUP":
  688. navigationButtonUp();
  689. break;
  690. case "ARROWRIGHT":
  691. case "ARROWDOWN":
  692. navigationButtonDown();
  693. break;
  694. default:
  695. break;
  696. }
  697. }
  698. }
  699.  
  700. function shortsOperationMode(e) {
  701. const volumeSlider = document.getElementById("byts-vol");
  702. if (
  703. e.key.toUpperCase() === "ARROWUP" ||
  704. e.key.toUpperCase() === "ARROWDOWN"
  705. ) {
  706. e.stopPropagation();
  707. e.preventDefault();
  708. if (e.shiftKey) {
  709. switch (e.key.toUpperCase()) {
  710. case "ARROWUP":
  711. video.volume = Math.min(1, video.volume + 0.02);
  712. volumeSlider.value = video.volume;
  713. break;
  714. case "ARROWDOWN":
  715. video.volume = Math.max(0, video.volume - 0.02);
  716. volumeSlider.value = video.volume;
  717. break;
  718. default:
  719. break;
  720. }
  721. } else {
  722. switch (e.key.toUpperCase()) {
  723. case "ARROWUP":
  724. navigationButtonUp();
  725. break;
  726. case "ARROWDOWN":
  727. navigationButtonDown();
  728. break;
  729. default:
  730. break;
  731. }
  732. }
  733. } else if (
  734. e.key.toUpperCase() === "ARROWLEFT" ||
  735. e.key.toUpperCase() === "ARROWRIGHT"
  736. ) {
  737. if (e.shiftKey) {
  738. switch (e.key.toUpperCase()) {
  739. case "ARROWLEFT":
  740. video.volume = Math.max(0, video.volume - 0.01);
  741. volumeSlider.value = video.volume;
  742. break;
  743. case "ARROWRIGHT":
  744. video.volume = Math.min(1, video.volume + 0.01);
  745. volumeSlider.value = video.volume;
  746. break;
  747. default:
  748. break;
  749. }
  750. } else {
  751. switch (e.key.toUpperCase()) {
  752. case "ARROWLEFT":
  753. video.currentTime -= 1;
  754. break;
  755. case "ARROWRIGHT":
  756. video.currentTime += 1;
  757. break;
  758. default:
  759. break;
  760. }
  761. }
  762. }
  763. }
  764.  
  765. function handleEvent(e) {
  766. videoOperationMode(e);
  767. if (constantVolume) {
  768. constantVolume = false;
  769. requestAnimationFrame(() => (constantVolume = true));
  770. }
  771. }
  772.  
  773. function addShortcuts() {
  774. if (operationMode === "Video") {
  775. const observer = new MutationObserver((mutations) => {
  776. for (const mutation of mutations) {
  777. for (const node of mutation.addedNodes) {
  778. if (node?.id === "byts-vol-div") {
  779. document.addEventListener("keydown", handleEvent, {
  780. capture: true,
  781. });
  782. observer.disconnect();
  783. }
  784. }
  785. }
  786. });
  787. observer.observe(document.documentElement, {
  788. childList: true,
  789. subtree: true,
  790. });
  791. } else {
  792. document.addEventListener(
  793. "keydown",
  794. function (e) {
  795. shortsOperationMode(e);
  796. if (constantVolume) {
  797. constantVolume = false;
  798. requestAnimationFrame(() => (constantVolume = true));
  799. }
  800. },
  801. {
  802. capture: true,
  803. }
  804. );
  805. }
  806. if (doubleClickToFullscreen) {
  807. video.addEventListener("dblclick", function () {
  808. if (document.fullscreenElement) {
  809. document.exitFullscreen();
  810. } else {
  811. const fullscreenButton = document.querySelector(
  812. "#fullscreen-button-shape > button"
  813. );
  814. if (fullscreenButton) {
  815. fullscreenButton.click();
  816. } else {
  817. document.getElementsByTagName("ytd-app")[0].requestFullscreen();
  818. }
  819. }
  820. });
  821. }
  822. document.addEventListener("keydown", function (e) {
  823. if (e.altKey && e.key.toUpperCase() === "ENTER") {
  824. if (document.fullscreenElement) {
  825. document.exitFullscreen();
  826. } else {
  827. const fullscreenButton = document.querySelector(
  828. "#fullscreen-button-shape > button"
  829. );
  830. if (fullscreenButton) {
  831. fullscreenButton.click();
  832. } else {
  833. document.getElementsByTagName("ytd-app")[0].requestFullscreen();
  834. }
  835. }
  836. }
  837. });
  838. document.addEventListener("keydown", function (e) {
  839. if (e.altKey && e.key.toUpperCase() === "W") {
  840. const watchUrl = location.href.replace("shorts/", "watch?v=");
  841. if (openWatchInCurrentTab) {
  842. window.location.href = watchUrl;
  843. } else {
  844. window.open(watchUrl, "_blank");
  845. }
  846. }
  847. });
  848. document.addEventListener("keydown", function (e) {
  849. if (
  850. (e.key >= "0" && e.key <= "9") ||
  851. (e.code >= "Numpad0" && e.code <= "Numpad9")
  852. ) {
  853. video.currentTime = video.duration * (e.key / 10);
  854. }
  855. });
  856. document.addEventListener("keydown", function (e) {
  857. if (e.key.toUpperCase() === "C") {
  858. if (video.playbackRate < 3) {
  859. video.playbackRate += 0.1;
  860. }
  861. } else if (e.key.toUpperCase() === "X") {
  862. if (video.playbackRate > 0.1) {
  863. video.playbackRate -= 0.1;
  864. }
  865. } else if (e.key.toUpperCase() === "Z") {
  866. video.playbackRate = 1;
  867. }
  868. GM.setValue("playbackRate", video.playbackRate);
  869. });
  870. document.addEventListener("keydown", function (e) {
  871. if (e.key.toUpperCase() === "V") {
  872. hideMetaDescription = !hideMetaDescription;
  873. }
  874. });
  875. }
  876.  
  877. function padTo2Digits(num) {
  878. return num.toString().padStart(2, "0");
  879. }
  880.  
  881. function updateVidElemWithRAF() {
  882. try {
  883. if (currentUrl?.includes("youtube.com/shorts")) {
  884. updateVidElem();
  885. }
  886. } catch (e) {
  887. console.error(e);
  888. }
  889. requestAnimationFrame(updateVidElemWithRAF);
  890. }
  891.  
  892. function navigationButtonDown() {
  893. document.querySelector("#navigation-button-down button").click();
  894. }
  895.  
  896. function navigationButtonUp() {
  897. document.querySelector("#navigation-button-up button").click();
  898. }
  899.  
  900. function setVideoPlaybackTime(event, player) {
  901. const rect = player.getBoundingClientRect();
  902. let offsetX = event.clientX - rect.left;
  903. if (offsetX < 0) {
  904. offsetX = 0;
  905. } else if (offsetX > player.offsetWidth) {
  906. offsetX = player.offsetWidth - 1;
  907. }
  908. let currentTime = (offsetX / player.offsetWidth) * video.duration;
  909. if (currentTime === 0) currentTime = 1e-6;
  910. video.currentTime = currentTime;
  911. }
  912.  
  913. async function updateVidElem() {
  914. const currentVideo = document.querySelector(
  915. "#shorts-player > div.html5-video-container > video"
  916. );
  917. if (video !== currentVideo) {
  918. video = currentVideo;
  919. }
  920.  
  921. if (constantVolume) {
  922. video.volume = await GM.getValue("volume", 0);
  923. }
  924.  
  925. if (constantSpeed) {
  926. video.playbackRate = await GM.getValue("playbackRate", 1);
  927. }
  928.  
  929. const reel = document.querySelector("ytd-reel-video-renderer[is-active]");
  930. if (reel === null) {
  931. return;
  932. }
  933.  
  934. if (progressBarStyle === "custom") {
  935. const shortsPlayerControls = document.querySelector(
  936. "#scrubber > ytd-scrubber > shorts-player-controls"
  937. );
  938. const scrubber = document.getElementById("scrubber");
  939. shortsPlayerControls?.remove();
  940. scrubber?.remove();
  941. }
  942.  
  943. update(reel, video);
  944. newInstallation(reel, video);
  945.  
  946. if (continueFromLastCheckpoint !== checkpointStatusEnum[i18n.off] && video.duration) {
  947. const currentSec = Math.floor(video.currentTime);
  948. const shortsUrlList = location.href.split("/");
  949. if (!shortsUrlList.includes("shorts")) return;
  950. const shortsId = shortsUrlList.pop();
  951.  
  952. if (shortsId !== lastShortsId) {
  953. lastShortsId = shortsId;
  954. const checkpoint = shortsCheckpoints[shortsId] || 1e-6;
  955. video.pause();
  956. if (checkpoint + 1 >= video.duration) {
  957. video.currentTime = 1e-6;
  958. } else {
  959. video.currentTime = checkpoint;
  960. }
  961. video.play();
  962. }
  963.  
  964. if (currentSec !== lastCurSeconds && video.currentTime !== 0) {
  965. lastCurSeconds = currentSec;
  966. shortsCheckpoints[shortsId] = currentSec;
  967. GM.setValue("shortsCheckpoints", shortsCheckpoints);
  968. }
  969. }
  970.  
  971. if (operationMode === "Shorts") {
  972. document.removeEventListener("keydown", videoOperationMode, {
  973. capture: true,
  974. });
  975. document.addEventListener("keydown", shortsOperationMode, {});
  976. } else {
  977. document.removeEventListener("keydown", shortsOperationMode, {});
  978. document.addEventListener("keydown", videoOperationMode, {
  979. capture: true,
  980. });
  981. }
  982.  
  983. const metaDescription = document.querySelector(
  984. "ytd-reel-video-renderer[is-active] .metadata-container"
  985. );
  986. if (metaDescription) {
  987. metaDescription.style.visibility = hideMetaDescription
  988. ? "hidden"
  989. : "visible";
  990. }
  991.  
  992. // Volume Slider
  993. let volumeSliderDiv = document.getElementById("byts-vol-div");
  994. let volumeSlider = document.getElementById("byts-vol");
  995. let volumeTextDiv = document.getElementById("byts-vol-textdiv");
  996. const reelVolumeSliderDiv = reel.querySelector("#byts-vol-div");
  997. if (reelVolumeSliderDiv === null) {
  998. if (volumeSliderDiv === null) {
  999. volumeSliderDiv = document.createElement("div");
  1000. volumeSliderDiv.id = "byts-vol-div";
  1001. volumeSliderDiv.style.cssText = `user-select: none; width: 100px; left: 0px; background-color: transparent; position: absolute; margin-left: 5px; margin-top: ${reel.offsetHeight}px;`;
  1002. volumeSlider = document.createElement("input");
  1003. volumeSlider.style.cssText = `user-select: none; width: 80px; left: 0px; background-color: transparent; position: absolute; margin-top: 0px;`;
  1004. volumeSlider.type = "range";
  1005. volumeSlider.id = "byts-vol";
  1006. volumeSlider.className = "volslider";
  1007. volumeSlider.name = "vol";
  1008. volumeSlider.min = 0.0;
  1009. volumeSlider.max = 1.0;
  1010. volumeSlider.step = 0.01;
  1011. volumeSlider.value = video.volume;
  1012. volumeSlider.addEventListener("input", function () {
  1013. video.volume = this.value;
  1014. GM.setValue("volume", this.value);
  1015. });
  1016. volumeSliderDiv.appendChild(volumeSlider);
  1017. volumeTextDiv = document.createElement("div");
  1018. volumeTextDiv.id = "byts-vol-textdiv";
  1019. volumeTextDiv.style.cssText = `user-select: none; background-color: transparent; position: absolute; color: ${
  1020. isDarkMode ? "white" : "black"
  1021. }; font-size: 1.2rem; margin-left: ${volumeSlider.offsetWidth + 1}px`;
  1022. volumeTextDiv.textContent = `${(
  1023. video.volume.toFixed(2) * 100
  1024. ).toFixed()}%`;
  1025. volumeSliderDiv.appendChild(volumeTextDiv);
  1026. }
  1027. reel.appendChild(volumeSliderDiv);
  1028. }
  1029. if (constantVolume) {
  1030. video.volume = volumeSlider.value;
  1031. }
  1032. volumeSlider.value = video.volume;
  1033. volumeTextDiv.textContent = `${(
  1034. video.volume.toFixed(2) * 100
  1035. ).toFixed()}%`;
  1036. volumeSliderDiv.style.marginTop = `${reel.offsetHeight + 2}px`;
  1037. volumeTextDiv.style.marginLeft = `${volumeSlider.offsetWidth + 1}px`;
  1038. if (video.muted) {
  1039. volumeTextDiv.textContent = "0%";
  1040. volumeSlider.value = 0;
  1041. } else {
  1042. volumeTextDiv.textContent = `${(video.volume * 100).toFixed()}%`;
  1043. volumeSlider.value = video.volume;
  1044. }
  1045.  
  1046. if (progressBarStyle === "custom") {
  1047. // Progress Bar
  1048. let progressBar = document.getElementById("byts-progbar");
  1049. const reelProgressBar = reel.querySelector("#byts-progbar");
  1050. if (reelProgressBar === null) {
  1051. const builtinProgressbar = reel.querySelector("#progress-bar");
  1052. if (builtinProgressbar !== null) {
  1053. builtinProgressbar.remove();
  1054. }
  1055. if (progressBar === null) {
  1056. progressBar = document.createElement("div");
  1057. progressBar.id = "byts-progbar";
  1058. progressBar.style.cssText = `user-select: none; cursor: pointer; width: 98%; height: 7px; background-color: #343434; position: absolute; border-radius: 10px; margin-top: ${
  1059. reel.offsetHeight - 7
  1060. }px;`;
  1061. }
  1062. reel.appendChild(progressBar);
  1063.  
  1064. let wasPausedBeforeDrag = false;
  1065. progressBar.addEventListener("mousedown", function (e) {
  1066. seekMouseDown = true;
  1067. wasPausedBeforeDrag = video.paused;
  1068. setVideoPlaybackTime(e, progressBar);
  1069. video.pause();
  1070. progressBar.classList.add("show-dot");
  1071. });
  1072. document.addEventListener("mousemove", function (e) {
  1073. if (!seekMouseDown) return;
  1074. e.preventDefault();
  1075. setVideoPlaybackTime(e, progressBar);
  1076. if (!video.paused) {
  1077. video.pause();
  1078. }
  1079. e.preventDefault();
  1080. });
  1081. document.addEventListener("mouseup", function () {
  1082. if (!seekMouseDown) return;
  1083. seekMouseDown = false;
  1084. if (!wasPausedBeforeDrag) {
  1085. video.play();
  1086. }
  1087. progressBar.classList.remove("show-dot");
  1088. });
  1089. }
  1090. progressBar.style.marginTop = `${reel.offsetHeight - 7}px`;
  1091.  
  1092. // Progress Bar (Inner Red Bar)
  1093. const progressTime = (video.currentTime / video.duration) * 100;
  1094. let InnerProgressBar = progressBar.querySelector("#byts-progress");
  1095. if (InnerProgressBar === null) {
  1096. InnerProgressBar = document.createElement("div");
  1097. InnerProgressBar.id = "byts-progress";
  1098. InnerProgressBar.style.cssText = `
  1099. user-select: none;
  1100. background-color: #FF0000;
  1101. height: 100%;
  1102. border-radius: 10px;
  1103. width: ${progressTime}%;
  1104. position: relative;
  1105. `;
  1106. progressBar.appendChild(InnerProgressBar);
  1107. }
  1108. InnerProgressBar.style.width = `${progressTime}%`;
  1109. }
  1110.  
  1111. // Time Info
  1112. const durSecs = Math.floor(video.duration);
  1113. const durMinutes = Math.floor(durSecs / 60);
  1114. const durSeconds = durSecs % 60;
  1115. const curSecs = Math.floor(video.currentTime);
  1116.  
  1117. let timeInfo = document.getElementById("byts-timeinfo");
  1118. let timeInfoText = document.getElementById("byts-timeinfo-textdiv");
  1119. const reelTimeInfo = reel.querySelector("#byts-timeinfo");
  1120.  
  1121. if (!Number.isNaN(durSecs) && reelTimeInfo !== null) {
  1122. timeInfoText.textContent = `${Math.floor(curSecs / 60)}:${padTo2Digits(
  1123. curSecs % 60
  1124. )} / ${durMinutes}:${padTo2Digits(durSeconds)}`;
  1125. }
  1126. if (curSecs !== lastCurSeconds || reelTimeInfo === null) {
  1127. lastCurSeconds = curSecs;
  1128. const curMinutes = Math.floor(curSecs / 60);
  1129. const curSeconds = curSecs % 60;
  1130.  
  1131. if (reelTimeInfo === null) {
  1132. if (timeInfo === null) {
  1133. timeInfo = document.createElement("div");
  1134. timeInfo.id = "byts-timeinfo";
  1135. timeInfo.style.cssText = `user-select: none; display: flex; right: auto; left: auto; position: absolute; margin-top: ${
  1136. reel.offsetHeight - 2
  1137. }px;`;
  1138. timeInfoText = document.createElement("div");
  1139. timeInfoText.id = "byts-timeinfo-textdiv";
  1140. timeInfoText.style.cssText = `display: flex; margin-right: 5px; margin-top: 4px; color: ${
  1141. isDarkMode ? "white" : "black"
  1142. }; font-size: 1.2rem;`;
  1143. timeInfoText.textContent = `${curMinutes}:${padTo2Digits(
  1144. curSeconds
  1145. )} / ${durMinutes}:${padTo2Digits(durSeconds)}`;
  1146. timeInfo.appendChild(timeInfoText);
  1147. }
  1148. reel.appendChild(timeInfo);
  1149. timeInfoText.textContent = `${curMinutes}:${padTo2Digits(
  1150. curSeconds
  1151. )} / ${durMinutes}:${padTo2Digits(durSeconds)}`;
  1152. }
  1153. }
  1154. timeInfo.style.marginTop = `${reel.offsetHeight - 2}px`;
  1155.  
  1156. // Speed Slider
  1157. let speedSliderDiv = document.getElementById("byts-speed-div");
  1158. let speedSlider = document.getElementById("byts-speed");
  1159. let speedTextDiv = document.getElementById("byts-speed-textdiv");
  1160. const reelSpeedSliderDiv = reel.querySelector("#byts-speed-div");
  1161. if (reelSpeedSliderDiv === null) {
  1162. if (speedSliderDiv === null) {
  1163. speedSliderDiv = document.createElement("div");
  1164. speedSliderDiv.id = "byts-speed-div";
  1165. speedSliderDiv.style.cssText = `user-select: none; display: flex; width: 100px; left: 0px; background-color: transparent; position: absolute; margin-left: ${
  1166. userLanguage.toUpperCase().includes("ZH")
  1167. ? reel.offsetWidth - 176
  1168. : reel.offsetWidth - 185
  1169. }px; margin-top: ${reel.offsetHeight}px;`;
  1170. speedSlider = document.createElement("input");
  1171. speedSlider.style.cssText = `user-select: none; display: flex; width: 50px; left: 0px; background-color: transparent; position: absolute; margin-top: 0px;`;
  1172. speedSlider.type = "range";
  1173. speedSlider.id = "byts-speed";
  1174. speedSlider.className = "speedslider";
  1175. speedSlider.name = "speed";
  1176. speedSlider.min = 0.1;
  1177. speedSlider.max = 3.0;
  1178. speedSlider.step = 0.1;
  1179. speedSlider.value = video.playbackRate;
  1180. speedSlider.addEventListener("input", function () {
  1181. video.playbackRate = this.value;
  1182. speedTextDiv.textContent = `${this.value}x`;
  1183. GM.setValue("playbackRate", this.value);
  1184. });
  1185. speedSliderDiv.appendChild(speedSlider);
  1186. speedTextDiv = document.createElement("div");
  1187. speedTextDiv.id = "byts-speed-textdiv";
  1188. speedTextDiv.style.cssText = `user-select: none; display: flex; background-color: transparent; color: ${
  1189. isDarkMode ? "white" : "black"
  1190. }; font-size: 1.2rem; margin-left: ${speedSlider.offsetWidth + 5}px`;
  1191. speedTextDiv.textContent = `${parseFloat(video.playbackRate).toFixed(
  1192. 1
  1193. )}x`;
  1194. speedSliderDiv.appendChild(speedTextDiv);
  1195. }
  1196. reel.appendChild(speedSliderDiv);
  1197. }
  1198. speedSlider.value = video.playbackRate;
  1199. speedTextDiv.textContent = `${parseFloat(video.playbackRate).toFixed(
  1200. 1
  1201. )}x`;
  1202. speedSliderDiv.style.marginTop = `${reel.offsetHeight + 2}px`;
  1203. speedSliderDiv.style.marginLeft = `${
  1204. userLanguage.toUpperCase().includes("ZH")
  1205. ? reel.offsetWidth - 176
  1206. : reel.offsetWidth - 185
  1207. }px`;
  1208. speedTextDiv.style.marginLeft = `${speedSlider.offsetWidth + 5}px`;
  1209. if (reel.offsetHeight < 735) {
  1210. reel.removeChild(speedSliderDiv);
  1211. }
  1212.  
  1213. // AutoScroll
  1214. let autoScrollDiv = document.getElementById("byts-autoscroll-div");
  1215. const reelAutoScrollDiv = reel.querySelector("#byts-autoscroll-div");
  1216. if (reelAutoScrollDiv === null) {
  1217. if (autoScrollDiv === null) {
  1218. autoScrollDiv = document.createElement("div");
  1219. autoScrollDiv.id = "byts-autoscroll-div";
  1220. autoScrollDiv.style.cssText = `user-select: none; display: flex; right: 0px; position: absolute; margin-top: ${
  1221. reel.offsetHeight - 3
  1222. }px;`;
  1223. const autoScrollTextDiv = document.createElement("div");
  1224. autoScrollTextDiv.style.cssText = `display: flex; margin-right: 5px; margin-top: ${
  1225. userLanguage.toUpperCase().includes("ZH") ? "3px" : "5px"
  1226. }; color: ${isDarkMode ? "white" : "black"}; font-size: 1.2rem;`;
  1227. autoScrollTextDiv.textContent = i18n.autoScroll;
  1228. autoScrollDiv.appendChild(autoScrollTextDiv);
  1229. const autoScrollSwitch = document.createElement("label");
  1230. autoScrollSwitch.className = "switch";
  1231. autoScrollSwitch.style.marginTop = "5px";
  1232. const autoscrollInput = document.createElement("input");
  1233. autoscrollInput.id = "byts-autoscroll-input";
  1234. autoscrollInput.type = "checkbox";
  1235. autoscrollInput.checked = autoScroll;
  1236. autoscrollInput.addEventListener("input", function () {
  1237. autoScroll = this.checked;
  1238. GM.setValue("autoScroll", this.checked);
  1239. });
  1240. const autoScrollSlider = document.createElement("span");
  1241. autoScrollSlider.className = "slider round";
  1242. autoScrollSwitch.appendChild(autoscrollInput);
  1243. autoScrollSwitch.appendChild(autoScrollSlider);
  1244. autoScrollDiv.appendChild(autoScrollSwitch);
  1245. }
  1246. reel.appendChild(autoScrollDiv);
  1247. }
  1248. if (autoScroll === true) {
  1249. video.removeAttribute("loop");
  1250. video.removeEventListener("ended", navigationButtonDown);
  1251. video.addEventListener("ended", navigationButtonDown);
  1252. } else {
  1253. if (loopPlayback) {
  1254. video.setAttribute("loop", true);
  1255. video.removeEventListener("ended", navigationButtonDown);
  1256. } else {
  1257. video.removeAttribute("loop");
  1258. video.removeEventListener("ended", navigationButtonDown);
  1259. }
  1260. }
  1261. autoScrollDiv.style.marginTop = `${reel.offsetHeight - 3}px`;
  1262. }
  1263. });
  1264.  
  1265. const urlChange = (event) => {
  1266. const destinationUrl = event?.destination?.url || "";
  1267. if (destinationUrl.startsWith("about:blank")) return;
  1268. const href = destinationUrl || location.href;
  1269. if (href.includes("youtube.com/shorts")) {
  1270. if (shortsAutoSwitchToVideo) {
  1271. currentUrl = location.href = href.replace("shorts/", "watch?v=");
  1272. return;
  1273. } else {
  1274. currentUrl = href;
  1275. initialize();
  1276. }
  1277. }
  1278. };
  1279. urlChange();
  1280.  
  1281. unsafeWindow?.navigation?.addEventListener("navigate", urlChange);
  1282. unsafeWindow.addEventListener("replaceState", urlChange);
  1283. unsafeWindow.addEventListener("pushState", urlChange);
  1284. unsafeWindow.addEventListener("popState", urlChange);
  1285. unsafeWindow.addEventListener("hashchange", urlChange);
  1286. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址