YouTube Enhancer (Loop & Screenshot Buttons)

Add Loop, Save and Copy Screenshot Buttons.

  1. // ==UserScript==
  2. // @name YouTube Enhancer (Loop & Screenshot Buttons)
  3. // @description Add Loop, Save and Copy Screenshot Buttons.
  4. // @icon https://raw.githubusercontent.com/exyezed/youtube-enhancer/refs/heads/main/extras/youtube-enhancer.png
  5. // @version 1.5
  6. // @author exyezed
  7. // @namespace https://github.com/exyezed/youtube-enhancer/
  8. // @supportURL https://github.com/exyezed/youtube-enhancer/issues
  9. // @license MIT
  10. // @match https://www.youtube.com/*
  11. // @grant none
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. const buttonConfig = {
  18. screenshotFormat: "png",
  19. extension: 'png',
  20. clickDuration: 500
  21. };
  22.  
  23. const buttonCSS = `
  24. a.buttonLoopAndScreenshot-loop-button,
  25. a.buttonLoopAndScreenshot-save-screenshot-button,
  26. a.buttonLoopAndScreenshot-copy-screenshot-button {
  27. text-align: center;
  28. position: relative;
  29. display: flex;
  30. align-items: center;
  31. justify-content: center;
  32. width: 48px;
  33. height: 48px;
  34. }
  35.  
  36. a.buttonLoopAndScreenshot-loop-button svg,
  37. a.buttonLoopAndScreenshot-save-screenshot-button svg,
  38. a.buttonLoopAndScreenshot-copy-screenshot-button svg {
  39. width: 24px;
  40. height: 24px;
  41. vertical-align: middle;
  42. transition: fill 0.2s ease;
  43. }
  44.  
  45. a.buttonLoopAndScreenshot-loop-button:hover svg,
  46. a.buttonLoopAndScreenshot-save-screenshot-button:hover svg,
  47. a.buttonLoopAndScreenshot-copy-screenshot-button:hover svg {
  48. fill: url(#buttonGradient);
  49. }
  50.  
  51. a.buttonLoopAndScreenshot-loop-button.active svg,
  52. a.buttonLoopAndScreenshot-save-screenshot-button.clicked svg,
  53. a.buttonLoopAndScreenshot-copy-screenshot-button.clicked svg {
  54. fill: url(#successGradient);
  55. }
  56.  
  57. .buttonLoopAndScreenshot-shorts-save-button,
  58. .buttonLoopAndScreenshot-shorts-copy-button {
  59. display: flex;
  60. align-items: center;
  61. justify-content: center;
  62. margin-top: 16px;
  63. width: 48px;
  64. height: 48px;
  65. border-radius: 50%;
  66. cursor: pointer;
  67. transition: background-color 0.3s;
  68. }
  69.  
  70. .buttonLoopAndScreenshot-shorts-save-button svg,
  71. .buttonLoopAndScreenshot-shorts-copy-button svg {
  72. width: 24px;
  73. height: 24px;
  74. transition: fill 0.1s ease;
  75. }
  76.  
  77. .buttonLoopAndScreenshot-shorts-save-button svg path,
  78. .buttonLoopAndScreenshot-shorts-copy-button svg path {
  79. transition: fill 0.1s ease;
  80. }
  81.  
  82. .buttonLoopAndScreenshot-shorts-save-button:hover svg path,
  83. .buttonLoopAndScreenshot-shorts-copy-button:hover svg path {
  84. fill: url(#shortsButtonGradient) !important;
  85. }
  86.  
  87. .buttonLoopAndScreenshot-shorts-save-button.clicked svg path,
  88. .buttonLoopAndScreenshot-shorts-copy-button.clicked svg path {
  89. fill: url(#shortsSuccessGradient) !important;
  90. }
  91.  
  92. html[dark] .buttonLoopAndScreenshot-shorts-save-button,
  93. html[dark] .buttonLoopAndScreenshot-shorts-copy-button {
  94. background-color: rgba(255, 255, 255, 0.1);
  95. }
  96.  
  97. html[dark] .buttonLoopAndScreenshot-shorts-save-button:hover,
  98. html[dark] .buttonLoopAndScreenshot-shorts-copy-button:hover {
  99. background-color: rgba(255, 255, 255, 0.2);
  100. }
  101.  
  102. html[dark] .buttonLoopAndScreenshot-shorts-save-button svg path,
  103. html[dark] .buttonLoopAndScreenshot-shorts-copy-button svg path {
  104. fill: white;
  105. }
  106.  
  107. html:not([dark]) .buttonLoopAndScreenshot-shorts-save-button,
  108. html:not([dark]) .buttonLoopAndScreenshot-shorts-copy-button {
  109. background-color: rgba(0, 0, 0, 0.05);
  110. }
  111.  
  112. html:not([dark]) .buttonLoopAndScreenshot-shorts-save-button:hover,
  113. html:not([dark]) .buttonLoopAndScreenshot-shorts-copy-button:hover {
  114. background-color: rgba(0, 0, 0, 0.1);
  115. }
  116.  
  117. html:not([dark]) .buttonLoopAndScreenshot-shorts-save-button svg path,
  118. html:not([dark]) .buttonLoopAndScreenshot-shorts-copy-button svg path {
  119. fill: #030303;
  120. }
  121. `;
  122. const iconUtils = {
  123. createGradientDefs(isShortsButton = false) {
  124. const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
  125. const hoverGradient = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient');
  126. hoverGradient.setAttribute('id', isShortsButton ? 'shortsButtonGradient' : 'buttonGradient');
  127. hoverGradient.setAttribute('x1', '0%');
  128. hoverGradient.setAttribute('y1', '0%');
  129. hoverGradient.setAttribute('x2', '100%');
  130. hoverGradient.setAttribute('y2', '100%');
  131. const hoverStop1 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
  132. hoverStop1.setAttribute('offset', '0%');
  133. hoverStop1.setAttribute('style', 'stop-color:#f03');
  134. const hoverStop2 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
  135. hoverStop2.setAttribute('offset', '100%');
  136. hoverStop2.setAttribute('style', 'stop-color:#ff2791');
  137. hoverGradient.appendChild(hoverStop1);
  138. hoverGradient.appendChild(hoverStop2);
  139. defs.appendChild(hoverGradient);
  140. const successGradient = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient');
  141. successGradient.setAttribute('id', isShortsButton ? 'shortsSuccessGradient' : 'successGradient');
  142. successGradient.setAttribute('x1', '0%');
  143. successGradient.setAttribute('y1', '0%');
  144. successGradient.setAttribute('x2', '100%');
  145. successGradient.setAttribute('y2', '100%');
  146. const successStop1 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
  147. successStop1.setAttribute('offset', '0%');
  148. successStop1.setAttribute('style', 'stop-color:#0f9d58');
  149. const successStop2 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
  150. successStop2.setAttribute('offset', '100%');
  151. successStop2.setAttribute('style', 'stop-color:#00c853');
  152. successGradient.appendChild(successStop1);
  153. successGradient.appendChild(successStop2);
  154. defs.appendChild(successGradient);
  155. return defs;
  156. },
  157. createBaseSVG(viewBox, fill = '#e8eaed', isShortsButton = false) {
  158. const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  159. svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
  160. svg.setAttribute('height', '24px');
  161. svg.setAttribute('viewBox', viewBox);
  162. svg.setAttribute('width', '24px');
  163. svg.setAttribute('fill', fill);
  164. svg.appendChild(this.createGradientDefs(isShortsButton));
  165. return svg;
  166. },
  167. paths: {
  168. loopPath: 'M220-260q-92 0-156-64T0-480q0-92 64-156t156-64q37 0 71 13t61 37l68 62-60 54-62-56q-16-14-36-22t-42-8q-58 0-99 41t-41 99q0 58 41 99t99 41q22 0 42-8t36-22l310-280q27-24 61-37t71-13q92 0 156 64t64 156q0 92-64 156t-156 64q-37 0-71-13t-61-37l-68-62 60-54 62 56q16 14 36 22t42 8q58 0 99-41t41-99q0-58-41-99t-99-41q-22 0-42 8t-36 22L352-310q-27 24-61 37t-71 13Z',
  169. screenshotPath: 'M20 5h-3.17l-1.24-1.35A2 2 0 0 0 14.12 3H9.88c-.56 0-1.1.24-1.47.65L7.17 5H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2m-3 12H7a.5.5 0 0 1-.4-.8l2-2.67c.2-.27.6-.27.8 0L11.25 16l2.6-3.47c.2-.27.6-.27.8 0l2.75 3.67a.5.5 0 0 1-.4.8',
  170. copyScreenshotPath: 'M9 14h10l-3.45-4.5l-2.3 3l-1.55-2zm-1 4q-.825 0-1.412-.587T6 16V4q0-.825.588-1.412T8 2h12q.825 0 1.413.588T22 4v12q0 .825-.587 1.413T20 18zm0-2h12V4H8zm-4 6q-.825 0-1.412-.587T2 20V6h2v14h14v2zM8 4h12v12H8z'
  171. },
  172. createLoopIcon() {
  173. const svg = this.createBaseSVG('0 -960 960 960');
  174. const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  175. path.setAttribute('d', this.paths.loopPath);
  176. svg.appendChild(path);
  177. return svg;
  178. },
  179. createSaveScreenshotIcon(isShortsButton = false) {
  180. const svg = this.createBaseSVG('0 0 24 24', '#e8eaed', isShortsButton);
  181. const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  182. path.setAttribute('d', this.paths.screenshotPath);
  183. svg.appendChild(path);
  184. return svg;
  185. },
  186. createCopyScreenshotIcon(isShortsButton = false) {
  187. const svg = this.createBaseSVG('0 0 24 24', '#e8eaed', isShortsButton);
  188. const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  189. path.setAttribute('d', this.paths.copyScreenshotPath);
  190. svg.appendChild(path);
  191. return svg;
  192. }
  193. };
  194. const buttonUtils = {
  195. addStyle(styleString) {
  196. const style = document.createElement('style');
  197. style.textContent = styleString;
  198. document.head.append(style);
  199. },
  200.  
  201. getVideoId() {
  202. const urlParams = new URLSearchParams(window.location.search);
  203. return urlParams.get('v') || window.location.pathname.split('/').pop();
  204. },
  205.  
  206. getApiKey() {
  207. const scripts = document.getElementsByTagName('script');
  208. for (const script of scripts) {
  209. const match = script.textContent.match(/"INNERTUBE_API_KEY":\s*"([^"]+)"/);
  210. if (match && match[1]) return match[1];
  211. }
  212. return null;
  213. },
  214.  
  215. getClientInfo() {
  216. const scripts = document.getElementsByTagName('script');
  217. let clientName = null;
  218. let clientVersion = null;
  219. for (const script of scripts) {
  220. const nameMatch = script.textContent.match(/"INNERTUBE_CLIENT_NAME":\s*"([^"]+)"/);
  221. const versionMatch = script.textContent.match(/"INNERTUBE_CLIENT_VERSION":\s*"([^"]+)"/);
  222. if (nameMatch && nameMatch[1]) clientName = nameMatch[1];
  223. if (versionMatch && versionMatch[1]) clientVersion = versionMatch[1];
  224. }
  225. return { clientName, clientVersion };
  226. },
  227.  
  228. async fetchVideoDetails(videoId) {
  229. try {
  230. const apiKey = this.getApiKey();
  231. if (!apiKey) return null;
  232. const { clientName, clientVersion } = this.getClientInfo();
  233. if (!clientName || !clientVersion) return null;
  234. const response = await fetch(`https://www.youtube.com/youtubei/v1/player?key=${apiKey}`, {
  235. method: 'POST',
  236. headers: {
  237. 'Content-Type': 'application/json',
  238. },
  239. body: JSON.stringify({
  240. videoId: videoId,
  241. context: {
  242. client: {
  243. clientName: clientName,
  244. clientVersion: clientVersion,
  245. }
  246. }
  247. })
  248. });
  249. if (!response.ok) return null;
  250. const data = await response.json();
  251. if (data && data.videoDetails && data.videoDetails.title) {
  252. return data.videoDetails.title;
  253. }
  254. return 'YouTube Video';
  255. } catch (error) {
  256. return 'YouTube Video';
  257. }
  258. },
  259.  
  260. async getVideoTitle(callback) {
  261. const videoId = this.getVideoId();
  262. const title = await this.fetchVideoDetails(videoId);
  263. callback(title || 'YouTube Video');
  264. },
  265.  
  266. formatTime(time) {
  267. const date = new Date();
  268. const dateString = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
  269. const timeString = [
  270. Math.floor(time / 3600),
  271. Math.floor((time % 3600) / 60),
  272. Math.floor(time % 60)
  273. ].map(v => v.toString().padStart(2, '0')).join('-');
  274. return `${dateString} ${timeString}`;
  275. },
  276.  
  277. async copyToClipboard(blob) {
  278. const clipboardItem = new ClipboardItem({ "image/png": blob });
  279. await navigator.clipboard.write([clipboardItem]);
  280. },
  281.  
  282. downloadScreenshot(blob, filename) {
  283. const url = URL.createObjectURL(blob);
  284. const a = document.createElement('a');
  285. a.style.display = 'none';
  286. a.href = url;
  287. a.download = filename;
  288. document.body.appendChild(a);
  289. a.click();
  290. document.body.removeChild(a);
  291. URL.revokeObjectURL(url);
  292. },
  293.  
  294. captureScreenshot(player, action = 'download') {
  295. if (!player) return;
  296. const canvas = document.createElement("canvas");
  297. canvas.width = player.videoWidth;
  298. canvas.height = player.videoHeight;
  299. canvas.getContext('2d').drawImage(player, 0, 0, canvas.width, canvas.height);
  300. this.getVideoTitle((title) => {
  301. const time = player.currentTime;
  302. const filename = `${title} ${this.formatTime(time)}.${buttonConfig.extension}`;
  303. canvas.toBlob(async (blob) => {
  304. if (action === 'copy') {
  305. await this.copyToClipboard(blob);
  306. } else {
  307. this.downloadScreenshot(blob, filename);
  308. }
  309. }, `image/${buttonConfig.screenshotFormat}`);
  310. });
  311. }
  312. };
  313.  
  314. const regularVideo = {
  315. init() {
  316. this.waitForControls().then(() => {
  317. this.insertLoopElement();
  318. this.insertSaveScreenshotElement();
  319. this.insertCopyScreenshotElement();
  320. this.addObserver();
  321. this.addContextMenuListener();
  322. });
  323. },
  324.  
  325. waitForControls() {
  326. return new Promise((resolve, reject) => {
  327. let attempts = 0;
  328. const maxAttempts = 50;
  329. const checkControls = () => {
  330. const controls = document.querySelector('div.ytp-left-controls');
  331. if (controls) {
  332. resolve(controls);
  333. } else if (attempts >= maxAttempts) {
  334. reject(new Error('Controls not found after maximum attempts'));
  335. } else {
  336. attempts++;
  337. setTimeout(checkControls, 100);
  338. }
  339. };
  340. checkControls();
  341. });
  342. },
  343.  
  344. insertLoopElement() {
  345. const controls = document.querySelector('div.ytp-left-controls');
  346. if (!controls) return;
  347.  
  348. if (document.querySelector('.buttonLoopAndScreenshot-loop-button')) return;
  349.  
  350. const newButton = document.createElement('a');
  351. newButton.classList.add('ytp-button', 'buttonLoopAndScreenshot-loop-button');
  352. newButton.title = 'Loop Video';
  353. newButton.appendChild(iconUtils.createLoopIcon());
  354. newButton.addEventListener('click', this.toggleLoopState);
  355.  
  356. controls.appendChild(newButton);
  357. },
  358.  
  359. insertSaveScreenshotElement() {
  360. const controls = document.querySelector('div.ytp-left-controls');
  361. if (!controls) return;
  362.  
  363. if (document.querySelector('.buttonLoopAndScreenshot-save-screenshot-button')) return;
  364.  
  365. const newButton = document.createElement('a');
  366. newButton.classList.add('ytp-button', 'buttonLoopAndScreenshot-save-screenshot-button');
  367. newButton.title = 'Save Screenshot';
  368. newButton.appendChild(iconUtils.createSaveScreenshotIcon());
  369. newButton.addEventListener('click', this.handleSaveScreenshotClick);
  370.  
  371. const loopButton = document.querySelector('.buttonLoopAndScreenshot-loop-button');
  372. if (loopButton) {
  373. loopButton.parentNode.insertBefore(newButton, loopButton.nextSibling);
  374. } else {
  375. controls.appendChild(newButton);
  376. }
  377. },
  378. insertCopyScreenshotElement() {
  379. const controls = document.querySelector('div.ytp-left-controls');
  380. if (!controls) return;
  381.  
  382. if (document.querySelector('.buttonLoopAndScreenshot-copy-screenshot-button')) return;
  383.  
  384. const newButton = document.createElement('a');
  385. newButton.classList.add('ytp-button', 'buttonLoopAndScreenshot-copy-screenshot-button');
  386. newButton.title = 'Copy Screenshot to Clipboard';
  387. newButton.appendChild(iconUtils.createCopyScreenshotIcon());
  388. newButton.addEventListener('click', this.handleCopyScreenshotClick);
  389.  
  390. const saveButton = document.querySelector('.buttonLoopAndScreenshot-save-screenshot-button');
  391. if (saveButton) {
  392. saveButton.parentNode.insertBefore(newButton, saveButton.nextSibling);
  393. } else {
  394. controls.appendChild(newButton);
  395. }
  396. },
  397.  
  398. toggleLoopState() {
  399. const video = document.querySelector('video');
  400. video.loop = !video.loop;
  401. if (video.loop) video.play();
  402.  
  403. regularVideo.updateToggleControls();
  404. },
  405.  
  406. updateToggleControls() {
  407. const youtubeVideoLoop = document.querySelector('.buttonLoopAndScreenshot-loop-button');
  408. youtubeVideoLoop.classList.toggle('active');
  409. youtubeVideoLoop.setAttribute('title', this.isActive() ? 'Stop Looping' : 'Loop Video');
  410. },
  411.  
  412. isActive() {
  413. const youtubeVideoLoop = document.querySelector('.buttonLoopAndScreenshot-loop-button');
  414. return youtubeVideoLoop.classList.contains('active');
  415. },
  416.  
  417. addObserver() {
  418. const video = document.querySelector('video');
  419. new MutationObserver((mutations) => {
  420. mutations.forEach(() => {
  421. if ((video.getAttribute('loop') === null && this.isActive()) ||
  422. (video.getAttribute('loop') !== null && !this.isActive())) this.updateToggleControls();
  423. });
  424. }).observe(video, { attributes: true, attributeFilter: ['loop'] });
  425. },
  426.  
  427. addContextMenuListener() {
  428. const video = document.querySelector('video');
  429. video.addEventListener('contextmenu', () => {
  430. setTimeout(() => {
  431. const checkbox = document.querySelector('[role=menuitemcheckbox]');
  432. checkbox.setAttribute('aria-checked', this.isActive());
  433. checkbox.addEventListener('click', this.toggleLoopState);
  434. }, 50);
  435. });
  436. },
  437.  
  438. handleSaveScreenshotClick(event) {
  439. const button = event.currentTarget;
  440. button.classList.add('clicked');
  441. setTimeout(() => {
  442. button.classList.remove('clicked');
  443. }, buttonConfig.clickDuration);
  444.  
  445. const player = document.querySelector('video');
  446. buttonUtils.captureScreenshot(player, 'download');
  447. },
  448. handleCopyScreenshotClick(event) {
  449. const button = event.currentTarget;
  450. button.classList.add('clicked');
  451. setTimeout(() => {
  452. button.classList.remove('clicked');
  453. }, buttonConfig.clickDuration);
  454.  
  455. const player = document.querySelector('video');
  456. buttonUtils.captureScreenshot(player, 'copy');
  457. }
  458. };
  459.  
  460. const shortsVideo = {
  461. init() {
  462. this.insertSaveScreenshotElement();
  463. this.insertCopyScreenshotElement();
  464. },
  465. insertSaveScreenshotElement() {
  466. const shortsContainer = document.querySelector('ytd-reel-video-renderer[is-active] #actions');
  467. if (shortsContainer && !shortsContainer.querySelector('.buttonLoopAndScreenshot-shorts-save-button')) {
  468. const iconDiv = document.createElement('div');
  469. iconDiv.className = 'buttonLoopAndScreenshot-shorts-save-button';
  470. iconDiv.title = 'Save Screenshot';
  471. iconDiv.appendChild(iconUtils.createSaveScreenshotIcon(true));
  472. const customShortsIcon = shortsContainer.querySelector('#custom-shorts-icon');
  473. if (customShortsIcon) {
  474. customShortsIcon.parentNode.insertBefore(iconDiv, customShortsIcon);
  475. } else {
  476. shortsContainer.insertBefore(iconDiv, shortsContainer.firstChild);
  477. }
  478. iconDiv.addEventListener('click', (event) => {
  479. const button = event.currentTarget;
  480. button.classList.add('clicked');
  481. setTimeout(() => {
  482. button.classList.remove('clicked');
  483. }, buttonConfig.clickDuration);
  484. this.captureScreenshot('download');
  485. });
  486. }
  487. },
  488. insertCopyScreenshotElement() {
  489. const shortsContainer = document.querySelector('ytd-reel-video-renderer[is-active] #actions');
  490. if (shortsContainer && !shortsContainer.querySelector('.buttonLoopAndScreenshot-shorts-copy-button')) {
  491. const iconDiv = document.createElement('div');
  492. iconDiv.className = 'buttonLoopAndScreenshot-shorts-copy-button';
  493. iconDiv.title = 'Copy Screenshot to Clipboard';
  494. iconDiv.appendChild(iconUtils.createCopyScreenshotIcon(true));
  495. const saveButton = shortsContainer.querySelector('.buttonLoopAndScreenshot-shorts-save-button');
  496. if (saveButton) {
  497. saveButton.parentNode.insertBefore(iconDiv, saveButton.nextSibling);
  498. } else {
  499. const customShortsIcon = shortsContainer.querySelector('#custom-shorts-icon');
  500. if (customShortsIcon) {
  501. customShortsIcon.parentNode.insertBefore(iconDiv, customShortsIcon);
  502. } else {
  503. shortsContainer.insertBefore(iconDiv, shortsContainer.firstChild);
  504. }
  505. }
  506. iconDiv.addEventListener('click', (event) => {
  507. const button = event.currentTarget;
  508. button.classList.add('clicked');
  509. setTimeout(() => {
  510. button.classList.remove('clicked');
  511. }, buttonConfig.clickDuration);
  512. this.captureScreenshot('copy');
  513. });
  514. }
  515. },
  516.  
  517. captureScreenshot(action) {
  518. const player = document.querySelector('ytd-reel-video-renderer[is-active] video');
  519. buttonUtils.captureScreenshot(player, action);
  520. }
  521. };
  522.  
  523. const themeHandler = {
  524. init() {
  525. this.updateStyles();
  526. this.addObserver();
  527. },
  528.  
  529. updateStyles() {
  530. const isDarkTheme = document.documentElement.hasAttribute('dark');
  531. document.documentElement.classList.toggle('dark-theme', isDarkTheme);
  532. },
  533.  
  534. addObserver() {
  535. const observer = new MutationObserver(() => this.updateStyles());
  536. observer.observe(document.documentElement, {
  537. attributes: true,
  538. attributeFilter: ['dark']
  539. });
  540. }
  541. };
  542.  
  543. function initialize() {
  544. buttonUtils.addStyle(buttonCSS);
  545. waitForVideo().then(initializeWhenReady);
  546. }
  547.  
  548. function waitForVideo() {
  549. return new Promise((resolve) => {
  550. const checkVideo = () => {
  551. if (document.querySelector('video')) {
  552. resolve();
  553. } else {
  554. setTimeout(checkVideo, 100);
  555. }
  556. };
  557. checkVideo();
  558. });
  559. }
  560.  
  561. function initializeWhenReady() {
  562. initializeFeatures();
  563. }
  564.  
  565. function initializeFeatures() {
  566. regularVideo.init();
  567. themeHandler.init();
  568. initializeShortsFeatures();
  569. }
  570.  
  571. function initializeShortsFeatures() {
  572. if (window.location.pathname.includes('/shorts/')) {
  573. setTimeout(shortsVideo.init.bind(shortsVideo), 500);
  574. }
  575. }
  576.  
  577. const shortsObserver = new MutationObserver((mutations) => {
  578. for (let mutation of mutations) {
  579. if (mutation.type === 'childList') {
  580. initializeShortsFeatures();
  581. }
  582. }
  583. });
  584.  
  585. shortsObserver.observe(document.body, { childList: true, subtree: true });
  586.  
  587. window.addEventListener('yt-navigate-finish', initializeShortsFeatures);
  588.  
  589. document.addEventListener('yt-action', function(event) {
  590. if (event.detail && event.detail.actionName === 'yt-reload-continuation-items-command') {
  591. initializeShortsFeatures();
  592. }
  593. });
  594.  
  595. initialize();
  596. })();

QingJ © 2025

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