Greasy Fork is available in English.

::GOG-Games Links::

Adds a YouTube button per game card to search for no-commentary gameplay videos.

  1. // ==UserScript==
  2. // @name ::GOG-Games Links::
  3. // @namespace masterofobzene-GOG-Games
  4. // @version 3.1
  5. // @description Adds a YouTube button per game card to search for no-commentary gameplay videos.
  6. // @author masterofobzene
  7. // @homepage https://github.com/masterofobzene/UserScriptRepo
  8. // @license GNU GPLv3
  9. // @match *://gog-games.to/*
  10. // @grant none
  11. // @icon https://files.mastodon.social/accounts/avatars/114/061/563/113/485/047/original/f9c6c7664af152f1.png
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. const YT_BUTTON_CLASS = 'yt-search-unique';
  18. const PROCESSED_ATTR = 'data-yt-processed-v2';
  19. const PURPLE_COLOR = '#6a1b9a';
  20. const HOVER_COLOR = '#4a148c';
  21. let processing = false;
  22.  
  23. function createYouTubeButton(gameName) {
  24. const button = document.createElement('button');
  25. button.className = YT_BUTTON_CLASS;
  26. button.textContent = 'YouTube Search';
  27. button.style.cssText = `
  28. padding: 6px 12px !important;
  29. background: ${PURPLE_COLOR} !important;
  30. color: white !important;
  31. border: none !important;
  32. border-radius: 4px !important;
  33. cursor: pointer !important;
  34. margin: 8px 0 !important;
  35. font-family: Arial !important;
  36. transition: background 0.2s !important;
  37. display: inline-block !important;
  38. position: relative !important;
  39. z-index: 1000 !important;
  40. `;
  41.  
  42. const handleClick = (event) => {
  43. event.stopImmediatePropagation();
  44. event.preventDefault();
  45. window.open(`https://youtube.com/results?search_query=${encodeURIComponent(gameName + ' no commentary')}`, '_blank');
  46. };
  47.  
  48. button.addEventListener('mouseover', () => button.style.background = HOVER_COLOR);
  49. button.addEventListener('mouseout', () => button.style.background = PURPLE_COLOR);
  50. button.addEventListener('click', handleClick, true); // Use capturing phase
  51. button.addEventListener('auxclick', handleClick, true);
  52.  
  53. return button;
  54. }
  55.  
  56. function processCard(card) {
  57. if (processing || card.hasAttribute(PROCESSED_ATTR)) return;
  58.  
  59. processing = true;
  60. try {
  61. const existingButton = card.querySelector(`.${YT_BUTTON_CLASS}`);
  62. if (existingButton) {
  63. existingButton.remove();
  64. }
  65.  
  66. const gameName = [
  67. () => card.querySelector('img[alt]')?.alt?.trim(),
  68. () => card.querySelector('[class*="title"]')?.textContent?.trim(),
  69. () => card.querySelector('h3, h4')?.textContent?.trim()
  70. ].reduce((acc, fn) => acc || fn(), '');
  71.  
  72. if (!gameName) return;
  73.  
  74. const container = card.querySelector('.actions, .card-footer') || card.querySelector('a')?.parentElement || card;
  75. if (container && !container.querySelector(`.${YT_BUTTON_CLASS}`)) {
  76. container.prepend(createYouTubeButton(gameName));
  77. card.setAttribute(PROCESSED_ATTR, 'true');
  78. }
  79. } finally {
  80. processing = false;
  81. }
  82. }
  83.  
  84. function processAllCards() {
  85. const cards = document.querySelectorAll('[class*="card"]:not([${PROCESSED_ATTR}])');
  86. cards.forEach(card => {
  87. if (!card.hasAttribute(PROCESSED_ATTR)) {
  88. processCard(card);
  89. }
  90. });
  91. }
  92.  
  93. // Initial processing after full load
  94. window.addEventListener('load', () => {
  95. setTimeout(processAllCards, 2000);
  96. }, {once: true});
  97.  
  98. // Targeted mutation observation
  99. const mainContent = document.getElementById('main') || document.querySelector('main') || document.body;
  100. const observer = new MutationObserver((mutations) => {
  101. mutations.forEach(mutation => {
  102. if (mutation.type === 'childList') {
  103. mutation.addedNodes.forEach(node => {
  104. if (node.nodeType === Node.ELEMENT_NODE && node.matches('[class*="card"]')) {
  105. processCard(node);
  106. }
  107. });
  108. }
  109. });
  110. });
  111.  
  112. observer.observe(mainContent, {
  113. childList: true,
  114. subtree: true
  115. });
  116. })();