SheridanCoriana6

Youtube controls. Add icons Frame Forward, Frame Backward, Chapter Forward, Chapter Backward

Mar 15th, 2026 (edited)
100
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 22.94 KB | None | 0 0
  1. // ==UserScript==
  2. // @name YouTube Custom Controls
  3. // @namespace http://tampermonkey.net/
  4. // @version 5.5
  5. // @description Custom YouTube controls with movable panel
  6. // @author marek7400
  7. // @match *://*.youtube.com/*
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. (function() {
  12. 'use strict';
  13.  
  14. // Globalne zmienne
  15. let currentZoom = 1;
  16. let panX = 0;
  17. let panY = 0;
  18.  
  19. let isDragging = false;
  20. let hasDragged = false;
  21. let dragStartX = 0;
  22. let dragStartY = 0;
  23. let initialPanX = 0;
  24. let initialPanY = 0;
  25.  
  26. let reqFrame = null;
  27. let dragInitDone = false;
  28. let transitionTimeout = null;
  29. let isTransitioning = false;
  30.  
  31. // Wstrzykujemy style CSS do strony
  32. function addHoverStyles() {
  33. if (document.getElementById('custom-yt-controls-style')) return;
  34.  
  35. const style = document.createElement('style');
  36. style.id = 'custom-yt-controls-style';
  37.  
  38. style.textContent = `
  39. #custom-yt-controls {
  40. position: absolute;
  41. bottom: 63px; /* Domyślna pozycja */
  42. right: 25px; /* Domyślna pozycja */
  43. background-color: rgba(0, 0, 0, 0.7);
  44. backdrop-filter: blur(4px);
  45. border-radius: 10px;
  46. padding: 6px;
  47. display: flex;
  48. gap: 8px;
  49. z-index: 50;
  50. box-shadow: 0 2px 10px rgba(0,0,0,0.5);
  51. align-items: center;
  52. cursor: grab; /* Kursor chwytania dla całego panelu */
  53.  
  54. opacity: 0;
  55. visibility: hidden;
  56. pointer-events: none;
  57. /* Usunięto transition dla 'bottom', aby przesuwanie myszką było płynne */
  58. transition: opacity 0.25s cubic-bezier(0.0,0.0,0.2,1), visibility 0.25s;
  59. }
  60.  
  61. #custom-yt-controls:active {
  62. cursor: grabbing;
  63. }
  64.  
  65. .html5-video-player:not(.ytp-autohide) #custom-yt-controls,
  66. #custom-yt-controls:hover,
  67. #custom-yt-controls.is-dragging {
  68. opacity: 1 !important;
  69. visibility: visible !important;
  70. pointer-events: auto;
  71. }
  72.  
  73. /* Jeśli panel NIE ma nadpisanej pozycji, ułóż go wyżej w pełnym ekranie */
  74. .html5-video-player.ytp-fullscreen #custom-yt-controls:not([style*="top"]) {
  75. bottom: 140px;
  76. }
  77.  
  78. .html5-video-player:has(.ytp-settings-button[aria-expanded="true"]) #custom-yt-controls {
  79. opacity: 0 !important;
  80. visibility: hidden !important;
  81. pointer-events: none !important;
  82. }
  83.  
  84. .custom-yt-btn {
  85. background: transparent;
  86. border: none;
  87. color: #fff;
  88. cursor: pointer;
  89. padding: 6px;
  90. border-radius: 6px;
  91. display: flex;
  92. align-items: center;
  93. justify-content: center;
  94. transition: background-color 0.2s;
  95. width: 38px;
  96. height: 38px;
  97. outline: none;
  98. }
  99.  
  100. .custom-yt-btn:hover {
  101. background-color: rgba(255, 255, 255, 0.25);
  102. }
  103. `;
  104. document.head.appendChild(style);
  105. }
  106.  
  107. // --- System Powiadomień i Dźwięków ---
  108. function playSystemSound() {
  109. try {
  110. const AudioContext = window.AudioContext || window.webkitAudioContext;
  111. if (!AudioContext) return;
  112. const ctx = new AudioContext();
  113.  
  114. function playTone(freq, startTime, duration, volume) {
  115. const osc = ctx.createOscillator();
  116. const gain = ctx.createGain();
  117. osc.type = 'sine';
  118. osc.frequency.setValueAtTime(freq, ctx.currentTime + startTime);
  119.  
  120. gain.gain.setValueAtTime(0, ctx.currentTime + startTime);
  121. gain.gain.linearRampToValueAtTime(volume, ctx.currentTime + startTime + 0.02);
  122. gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + startTime + duration);
  123.  
  124. osc.connect(gain);
  125. gain.connect(ctx.destination);
  126.  
  127. osc.start(ctx.currentTime + startTime);
  128. osc.stop(ctx.currentTime + startTime + duration);
  129. }
  130.  
  131. playTone(1200, 0, 0.08, 0.25);
  132. playTone(800, 0.05, 0.1, 0.25);
  133.  
  134. } catch (e) { console.warn("AudioContext error", e); }
  135. }
  136.  
  137. function showNotification(text) {
  138. const player = document.querySelector('.html5-video-player');
  139. if (!player) return;
  140.  
  141. let notif = document.getElementById('custom-yt-notification');
  142. if (!notif) {
  143. notif = document.createElement('div');
  144. notif.id = 'custom-yt-notification';
  145. Object.assign(notif.style, {
  146. position: 'absolute',
  147. top: '50%',
  148. left: '50%',
  149. transform: 'translate(-50%, -50%)',
  150. backgroundColor: '#FFCC00',
  151. color: '#000000',
  152. padding: '16px 32px',
  153. borderRadius: '8px',
  154. fontSize: '22px',
  155. fontWeight: 'bold',
  156. zIndex: '999999',
  157. pointerEvents: 'none',
  158. opacity: '0',
  159. transition: 'opacity 0.3s ease-in-out',
  160. boxShadow: '0 6px 20px rgba(0,0,0,0.6)',
  161. fontFamily: '"Roboto", "Arial", sans-serif',
  162. textAlign: 'center',
  163. textTransform: 'uppercase'
  164. });
  165. player.appendChild(notif);
  166. }
  167.  
  168. notif.textContent = text;
  169. notif.style.opacity = '1';
  170.  
  171. if (notif.hideTimeout) clearTimeout(notif.hideTimeout);
  172.  
  173. notif.hideTimeout = setTimeout(() => {
  174. notif.style.opacity = '0';
  175. }, 2000);
  176. }
  177.  
  178. function onScreenshotSuccess() {
  179. playSystemSound();
  180. const player = document.querySelector('.html5-video-player');
  181. const isFullscreen = player && player.classList.contains('ytp-fullscreen');
  182. if (isFullscreen && currentZoom > 1) {
  183. showNotification('Zapisano zrzut w folderze pobrane');
  184. }
  185. }
  186.  
  187. // --- System Przesuwania Obrazu (Pan/Zoom) ---
  188. function applyTransform(smooth = false) {
  189. if (reqFrame) cancelAnimationFrame(reqFrame);
  190.  
  191. reqFrame = requestAnimationFrame(() => {
  192. reqFrame = null;
  193. const video = document.querySelector('video');
  194. const player = document.querySelector('.html5-video-player');
  195. if (!video || !player) return;
  196.  
  197. video.style.transformOrigin = 'center center';
  198.  
  199. if (smooth) {
  200. video.style.transition = 'transform 0.25s cubic-bezier(0.2, 0, 0.2, 1)';
  201. isTransitioning = true;
  202. if (transitionTimeout) clearTimeout(transitionTimeout);
  203. transitionTimeout = setTimeout(() => {
  204. isTransitioning = false;
  205. if (video) video.style.transition = 'none';
  206. }, 250);
  207. } else if (!isTransitioning) {
  208. video.style.transition = 'none';
  209. }
  210.  
  211. if (currentZoom <= 1) {
  212. panX = 0;
  213. panY = 0;
  214. video.style.transform = `translate3d(0px, 0px, 0) scale(${currentZoom})`;
  215. return;
  216. }
  217.  
  218. const rect = player.getBoundingClientRect();
  219. const maxTx = ((currentZoom - 1) * rect.width) / 2;
  220. const maxTy = ((currentZoom - 1) * rect.height) / 2;
  221.  
  222. panX = Math.max(-maxTx, Math.min(maxTx, panX));
  223. panY = Math.max(-maxTy, Math.min(maxTy, panY));
  224.  
  225. video.style.transform = `translate3d(${panX}px, ${panY}px, 0) scale(${currentZoom})`;
  226. });
  227. }
  228.  
  229. function resetZoom() {
  230. currentZoom = 1;
  231. panX = 0;
  232. panY = 0;
  233. applyTransform(true);
  234. }
  235.  
  236. function changeZoom(amount) {
  237. currentZoom += amount;
  238. currentZoom = Math.max(0.5, currentZoom);
  239.  
  240. if (currentZoom <= 1) {
  241. panX = 0;
  242. panY = 0;
  243. }
  244. applyTransform(true);
  245. }
  246.  
  247. function initDragPanning() {
  248. if (dragInitDone) return;
  249. dragInitDone = true;
  250.  
  251. window.addEventListener('mousedown', (e) => {
  252. if (currentZoom <= 1 || e.button !== 0) return;
  253.  
  254. const player = document.querySelector('.html5-video-player');
  255. if (!player || !player.contains(e.target)) return;
  256.  
  257. if (e.target.closest('.ytp-chrome-bottom, .ytp-chrome-top, #custom-yt-controls, .ytp-settings-menu')) return;
  258.  
  259. isDragging = true;
  260. hasDragged = false;
  261. dragStartX = e.clientX;
  262. dragStartY = e.clientY;
  263. initialPanX = panX;
  264. initialPanY = panY;
  265.  
  266. player.style.cursor = 'grabbing';
  267. }, { capture: true });
  268.  
  269. window.addEventListener('mousemove', (e) => {
  270. if (!isDragging) return;
  271.  
  272. const dx = e.clientX - dragStartX;
  273. const dy = e.clientY - dragStartY;
  274.  
  275. if (Math.abs(dx) > 3 || Math.abs(dy) > 3) {
  276. hasDragged = true;
  277. }
  278.  
  279. panX = initialPanX + dx;
  280. panY = initialPanY + dy;
  281.  
  282. applyTransform(false);
  283. }, { capture: true, passive: true });
  284.  
  285. window.addEventListener('mouseup', () => {
  286. if (isDragging) {
  287. isDragging = false;
  288. const player = document.querySelector('.html5-video-player');
  289. if (player) player.style.cursor = '';
  290. }
  291. }, { capture: true });
  292.  
  293. window.addEventListener('click', (e) => {
  294. if (hasDragged) {
  295. e.stopPropagation();
  296. e.preventDefault();
  297. hasDragged = false;
  298. }
  299. }, { capture: true });
  300.  
  301. window.addEventListener('resize', () => {
  302. if (currentZoom > 1) applyTransform(false);
  303. });
  304. }
  305.  
  306. // --- System Przesuwania Całego Panelu z Ikonami ---
  307. function initPanelDragging(panel) {
  308. let isDraggingPanel = false;
  309. let startX, startY, initialLeft, initialTop;
  310.  
  311. panel.addEventListener('mousedown', (e) => {
  312. // Ignoruj kliknięcie, jeśli to kliknięcie w ikonę, a nie w tło panelu
  313. if (e.target.closest('.custom-yt-btn')) return;
  314. if (e.button !== 0) return; // Tylko lewy przycisk myszy
  315.  
  316. e.preventDefault(); // Zapobiega zaznaczaniu tekstu
  317. e.stopPropagation(); // Zapobiega pauzowaniu wideo
  318.  
  319. isDraggingPanel = true;
  320. panel.classList.add('is-dragging'); // Utrzymuje widoczność panelu podczas dragowania
  321.  
  322. const player = document.querySelector('.html5-video-player');
  323. const playerRect = player.getBoundingClientRect();
  324. const panelRect = panel.getBoundingClientRect();
  325.  
  326. startX = e.clientX;
  327. startY = e.clientY;
  328.  
  329. // Pobieramy aktualną pozycję względem lewego górnego rogu playera
  330. initialLeft = panelRect.left - playerRect.left;
  331. initialTop = panelRect.top - playerRect.top;
  332. });
  333.  
  334. document.addEventListener('mousemove', (e) => {
  335. if (!isDraggingPanel) return;
  336.  
  337. const player = document.querySelector('.html5-video-player');
  338. if (!player) return;
  339. const playerRect = player.getBoundingClientRect();
  340.  
  341. const dx = e.clientX - startX;
  342. const dy = e.clientY - startY;
  343.  
  344. let newLeft = initialLeft + dx;
  345. let newTop = initialTop + dy;
  346.  
  347. // Zabezpieczenie przed wyrzuceniem panelu poza ekran playera
  348. newLeft = Math.max(0, Math.min(newLeft, playerRect.width - panel.offsetWidth));
  349. newTop = Math.max(0, Math.min(newTop, playerRect.height - panel.offsetHeight));
  350.  
  351. // Zapisujemy w PROCENTACH (aby działało przy zmianie na pełny ekran)
  352. const leftPercent = (newLeft / playerRect.width) * 100;
  353. const topPercent = (newTop / playerRect.height) * 100;
  354.  
  355. panel.style.left = `${leftPercent}%`;
  356. panel.style.top = `${topPercent}%`;
  357. panel.style.right = 'auto'; // Reset domyślnych styli CSS
  358. panel.style.bottom = 'auto';
  359. });
  360.  
  361. document.addEventListener('mouseup', () => {
  362. if (isDraggingPanel) {
  363. isDraggingPanel = false;
  364. panel.classList.remove('is-dragging');
  365.  
  366. // Zapisujemy do LocalStorage, żeby pamiętało po odświeżeniu
  367. localStorage.setItem('ytCustomControlsPosition', JSON.stringify({
  368. left: panel.style.left,
  369. top: panel.style.top
  370. }));
  371. }
  372. });
  373. }
  374.  
  375. // --- Narzędzia ---
  376. function simulateKey(key, keyCode, ctrlKey = false) {
  377. const target = document.querySelector('.html5-video-player') || document.body;
  378. const eventObj = {
  379. key: key,
  380. code: key === ',' ? 'Comma' : (key === '.' ? 'Period' : key),
  381. keyCode: keyCode,
  382. which: keyCode,
  383. ctrlKey: ctrlKey,
  384. bubbles: true,
  385. cancelable: true,
  386. composed: true
  387. };
  388. target.dispatchEvent(new KeyboardEvent('keydown', eventObj));
  389. target.dispatchEvent(new KeyboardEvent('keyup', eventObj));
  390. }
  391.  
  392. function createIcon(pathData) {
  393. const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  394. svg.setAttribute('viewBox', '0 0 24 24');
  395. svg.style.width = '24px';
  396. svg.style.height = '24px';
  397. svg.style.fill = '#ffffff';
  398. svg.style.display = 'block';
  399. const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  400. path.setAttribute('d', pathData);
  401. svg.appendChild(path);
  402. return svg;
  403. }
  404.  
  405. const buttonsConfig =[
  406. {
  407. id: 'btn-screenshot',
  408. title: 'Zrób zrzut ekranu (lub pobierz miniaturkę na starcie)',
  409. path: 'M4 4h3l2-2h6l2 2h3c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2zm8 3c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zm0 8c-1.65 0-3-1.35-3-3s1.35-3 3-3 3 1.35 3 3-1.35 3-3 3z',
  410. action: async () => {
  411. const video = document.querySelector('video');
  412. if (!video) return;
  413.  
  414. try {
  415. if (video.currentTime === 0) {
  416. const urlParams = new URLSearchParams(window.location.search);
  417. const videoId = urlParams.get('v');
  418. if (videoId) {
  419. const thumbUrls =[
  420. `https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg`,
  421. `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`
  422. ];
  423. let thumbDownloaded = false;
  424. for (const url of thumbUrls) {
  425. try {
  426. const response = await fetch(url);
  427. if (response.ok) {
  428. const blob = await response.blob();
  429. const blobUrl = URL.createObjectURL(blob);
  430. const a = document.createElement('a');
  431. a.href = blobUrl;
  432. a.download = `YouTube_Miniaturka_${videoId}.jpg`;
  433. document.body.appendChild(a);
  434. a.click();
  435. document.body.removeChild(a);
  436. URL.revokeObjectURL(blobUrl);
  437. thumbDownloaded = true;
  438. break;
  439. }
  440. } catch (e) {
  441. console.warn('Nie udało się pobrać miniaturki.', e);
  442. }
  443. }
  444. if (thumbDownloaded) {
  445. onScreenshotSuccess();
  446. return;
  447. }
  448. }
  449. }
  450.  
  451. if (video.videoWidth === 0 || video.videoHeight === 0) {
  452. alert('Obraz nie został załadowany. Odtwórz film na chwilę.');
  453. return;
  454. }
  455.  
  456. const canvas = document.createElement('canvas');
  457. canvas.width = video.videoWidth;
  458. canvas.height = video.videoHeight;
  459. const ctx = canvas.getContext('2d');
  460.  
  461. ctx.fillStyle = '#000000';
  462. ctx.fillRect(0, 0, canvas.width, canvas.height);
  463.  
  464. if (currentZoom <= 1) {
  465. ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
  466. } else {
  467. const W = video.videoWidth;
  468. const H = video.videoHeight;
  469. const cssW = video.offsetWidth || W;
  470. const cssH = video.offsetHeight || H;
  471. const sW = W / currentZoom;
  472. const sH = H / currentZoom;
  473.  
  474. const cx = (W / 2) - (panX / currentZoom) * (W / cssW);
  475. const cy = (H / 2) - (panY / currentZoom) * (H / cssH);
  476.  
  477. const sx = cx - sW / 2;
  478. const sy = cy - sH / 2;
  479.  
  480. ctx.drawImage(video, sx, sy, sW, sH, 0, 0, canvas.width, canvas.height);
  481. }
  482.  
  483. const dataUrl = canvas.toDataURL('image/png');
  484. const time = video.currentTime;
  485. const hours = Math.floor(time / 3600);
  486. const minutes = Math.floor((time % 3600) / 60);
  487. const seconds = Math.floor(time % 60);
  488. const timeString = `${hours > 0 ? hours + 'h' : ''}${minutes}m${seconds}s`;
  489.  
  490. const a = document.createElement('a');
  491. a.href = dataUrl;
  492. a.download = `YouTube_Zrzut_${timeString}.png`;
  493. document.body.appendChild(a);
  494. a.click();
  495. document.body.removeChild(a);
  496.  
  497. onScreenshotSuccess();
  498.  
  499. } catch (err) {
  500. alert('Błąd - materiał może być chroniony DRM.');
  501. }
  502. }
  503. },
  504. {
  505. id: 'btn-zoom-in',
  506. title: 'Powiększ obraz (+50%)\nPrawy przycisk myszy: Reset na 100%',
  507. path: 'M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z',
  508. action: () => changeZoom(0.5),
  509. actionContext: () => resetZoom()
  510. },
  511. {
  512. id: 'btn-zoom-out',
  513. title: 'Pomniejsz obraz (-50%)\nPrawy przycisk myszy: Reset na 100%',
  514. path: 'M19 13H5v-2h14v2z',
  515. action: () => changeZoom(-0.5),
  516. actionContext: () => resetZoom()
  517. },
  518. {
  519. id: 'btn-ctrl-left',
  520. title: 'Ctrl + Strzałka w lewo',
  521. path: 'M11 18V6l-8.5 6 8.5 6zm.5-6l8.5 6V6l-8.5 6z',
  522. action: () => { simulateKey('ArrowLeft', 37, true); }
  523. },
  524. {
  525. id: 'btn-comma',
  526. title: 'Klatka w tył (,)',
  527. path: 'M15.41 16.59L10.83 12l4.58-4.59L14 6l-6 6 6 6 1.41-1.41z',
  528. action: () => {
  529. simulateKey(',', 188);
  530. const vid = document.querySelector('video');
  531. if (vid && vid.paused) { vid.currentTime -= 1/30; }
  532. }
  533. },
  534. {
  535. id: 'btn-dot',
  536. title: 'Klatka w przód (.)',
  537. path: 'M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z',
  538. action: () => {
  539. simulateKey('.', 190);
  540. const vid = document.querySelector('video');
  541. if (vid && vid.paused) { vid.currentTime += 1/30; }
  542. }
  543. },
  544. {
  545. id: 'btn-ctrl-right',
  546. title: 'Ctrl + Strzałka w prawo',
  547. path: 'M4 18l8.5-6L4 6v12zm9-12v12l8.5-6L13 6z',
  548. action: () => { simulateKey('ArrowRight', 39, true); }
  549. }
  550. ];
  551.  
  552. function checkAndCreateUI() {
  553. if (!window.location.pathname.startsWith('/watch')) return;
  554.  
  555. addHoverStyles();
  556. initDragPanning();
  557.  
  558. if (document.getElementById('custom-yt-controls')) return;
  559.  
  560. const player = document.querySelector('.html5-video-player');
  561. if (!player) return;
  562.  
  563. const container = document.createElement('div');
  564. container.id = 'custom-yt-controls';
  565.  
  566. // Wczytywanie zapisanej pozycji z LocalStorage
  567. try {
  568. const savedPos = localStorage.getItem('ytCustomControlsPosition');
  569. if (savedPos) {
  570. const pos = JSON.parse(savedPos);
  571. if (pos.left && pos.top) {
  572. container.style.left = pos.left;
  573. container.style.top = pos.top;
  574. container.style.right = 'auto';
  575. container.style.bottom = 'auto';
  576. }
  577. }
  578. } catch (e) {
  579. console.warn('Nie udało się wczytać pozycji panelu', e);
  580. }
  581.  
  582. buttonsConfig.forEach(config => {
  583. const btn = document.createElement('button');
  584. btn.id = config.id;
  585. btn.title = config.title;
  586. btn.className = 'custom-yt-btn';
  587.  
  588. const iconSvg = createIcon(config.path);
  589. btn.appendChild(iconSvg);
  590.  
  591. btn.addEventListener('click', (e) => {
  592. e.preventDefault();
  593. e.stopPropagation();
  594. config.action();
  595. });
  596.  
  597. if (config.actionContext) {
  598. btn.addEventListener('contextmenu', (e) => {
  599. e.preventDefault();
  600. e.stopPropagation();
  601. config.actionContext();
  602. });
  603. }
  604.  
  605. container.appendChild(btn);
  606. });
  607.  
  608. player.appendChild(container);
  609.  
  610. // Aktywuj system przesuwania dla nowo utworzonego panelu
  611. initPanelDragging(container);
  612. }
  613.  
  614. setInterval(checkAndCreateUI, 1000);
  615.  
  616. window.addEventListener('yt-navigate-start', () => {
  617. resetZoom();
  618. });
  619.  
  620. })();
Add Comment
Please, Sign In to add comment