Twitch Enhancements

Automatically claim channel points, enable theater mode, claim prime rewards, claim drops, and add redeem buttons for GOG and Legacy Games on Twitch and Amazon Gaming websites.

  1. // ==UserScript==
  2. // @name Twitch Enhancements
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.5.4
  5. // @description Automatically claim channel points, enable theater mode, claim prime rewards, claim drops, and add redeem buttons for GOG and Legacy Games on Twitch and Amazon Gaming websites.
  6. // @author JJJ
  7. // @match https://www.twitch.tv/*
  8. // @match https://gaming.amazon.com/*
  9. // @match https://www.twitch.tv/drops/inventory*
  10. // @match https://www.gog.com/en/redeem
  11. // @match https://promo.legacygames.com/*
  12. // @icon https://th.bing.com/th/id/R.d71be224f193da01e7e499165a8981c5?rik=uBYlAxJ4XyXmJg&riu=http%3a%2f%2fpngimg.com%2fuploads%2ftwitch%2ftwitch_PNG28.png&ehk=PMc5m5Fil%2bhyq1zilk3F3cuzxSluXFBE80XgxVIG0rM%3d&risl=&pid=ImgRaw&r=0
  13. // @grant GM_setValue
  14. // @grant GM_getValue
  15. // @grant GM_deleteValue
  16. // @grant GM_registerMenuCommand
  17. // @license MIT
  18. // ==/UserScript==
  19.  
  20. (function () {
  21. 'use strict';
  22.  
  23. // Configuration settings
  24. const CONFIG = {
  25. enableAutoClaimPoints: GM_getValue('enableAutoClaimPoints', true),
  26. enableTheaterMode: GM_getValue('enableTheaterMode', true),
  27. enableClaimPrimeRewards: GM_getValue('enableClaimPrimeRewards', true),
  28. enableClaimDrops: GM_getValue('enableClaimDrops', true),
  29. enableGogRedeemButton: GM_getValue('enableGogRedeemButton', true),
  30. enableLegacyGamesRedeemButton: GM_getValue('enableLegacyGamesRedeemButton', true),
  31. enableHideGlobalMenu: GM_getValue('enableHideGlobalMenu', true),
  32. enableAutoRefreshDrops: GM_getValue('enableAutoRefreshDrops', true),
  33. enableClaimAllButton: GM_getValue('enableClaimAllButton', true),
  34. enableRemoveAllButton: GM_getValue('enableRemoveAllButton', true),
  35. settingsKey: GM_getValue('settingsKey', 'F2') // Default to F2 if not set
  36. };
  37.  
  38. // Add logger configuration
  39. const Logger = {
  40. styles: {
  41. info: 'color: #2196F3; font-weight: bold',
  42. warning: 'color: #FFC107; font-weight: bold',
  43. success: 'color: #4CAF50; font-weight: bold',
  44. error: 'color: #F44336; font-weight: bold'
  45. },
  46. prefix: '[TwitchEnhancements]',
  47. getTimestamp() {
  48. return new Date().toISOString().split('T')[1].slice(0, -1);
  49. },
  50. info(msg) {
  51. console.log(`%c${this.prefix} ${this.getTimestamp()} - ${msg}`, this.styles.info);
  52. },
  53. warning(msg) {
  54. console.warn(`%c${this.prefix} ${this.getTimestamp()} - ${msg}`, this.styles.warning);
  55. },
  56. success(msg) {
  57. console.log(`%c${this.prefix} ${this.getTimestamp()} - ${msg}`, this.styles.success);
  58. },
  59. error(msg) {
  60. console.error(`%c${this.prefix} ${this.getTimestamp()} - ${msg}`, this.styles.error);
  61. }
  62. };
  63.  
  64. // Twitch Constants
  65. const PLAYER_SELECTOR = '.video-player';
  66. const THEATER_MODE_BUTTON_SELECTOR = 'button[aria-label="Modo cine (alt+t)"], button[aria-label="Theatre Mode (alt+t)"]';
  67. const CLOSE_MENU_BUTTON_SELECTOR = 'button[aria-label="Close Menu"]';
  68. const CLOSE_MODAL_BUTTON_SELECTOR = 'button[aria-label="Close modal"]';
  69. const THEATER_MODE_CLASS = 'theatre-mode';
  70. const CLAIMABLE_BONUS_SELECTOR = '.claimable-bonus__icon';
  71. const CLAIM_DROPS_SELECTOR = 'button.ScCoreButton-sc-ocjdkq-0.eWlfQB';
  72. const PRIME_REWARD_SELECTOR = 'button.tw-interactive.tw-button.tw-button--full-width[data-a-target="buy-box_call-to-action"] span.tw-button__text div.tw-inline-block p.tw-font-size-5.tw-md-font-size-4[title="Get game"]';
  73. const PRIME_REWARD_SELECTOR_2 = 'p.tw-font-size-5.tw-md-font-size-4[data-a-target="buy-box_call-to-action-text"][title="Get game"]';
  74.  
  75. // Redeem on GOG Constants
  76. const GOG_REDEEM_CODE_INPUT_SELECTOR = '#codeInput';
  77. const GOG_CONTINUE_BUTTON_SELECTOR = 'button[type="submit"][aria-label="Proceed to the next step"]';
  78. const GOG_FINAL_REDEEM_BUTTON_SELECTOR = 'button[type="submit"][aria-label="Redeem the code"]';
  79.  
  80. // Redeem on Legacy Games Constants
  81. const LEGACY_GAMES_REDEEM_URL = 'https://promo.legacygames.com/royal-romances-cursed-hearts-ce-prime-deal/';
  82. const LEGACY_GAMES_CODE_INPUT_SELECTOR = '#primedeal_game_code';
  83. const LEGACY_GAMES_EMAIL_INPUT_SELECTOR = '#primedeal_email';
  84. const LEGACY_GAMES_EMAIL_VALIDATE_INPUT_SELECTOR = '#primedeal_email_validate';
  85. const LEGACY_GAMES_SUBMIT_BUTTON_SELECTOR = '#submitbutton';
  86. const LEGACY_GAMES_NEWSLETTER_CHECKBOX_SELECTOR = '#primedeal_newsletter';
  87.  
  88. let claiming = false;
  89.  
  90. // Check if MutationObserver is supported
  91. const MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
  92.  
  93. // Settings Dialog Functions
  94. function createSettingsDialog() {
  95. const dialogHTML = `
  96. <div id="twitchEnhancementsDialog" class="te-dialog">
  97. <h3>Twitch Enhancements Settings</h3>
  98. ${createToggle('enableAutoClaimPoints', 'Auto Claim Channel Points', 'Automatically claim channel points')}
  99. ${createToggle('enableTheaterMode', 'Auto Theater Mode', 'Automatically enable theater mode')}
  100. ${createToggle('enableClaimPrimeRewards', 'Auto Claim Prime Rewards', 'Automatically claim prime rewards')}
  101. ${createToggle('enableClaimDrops', 'Auto Claim Drops', 'Automatically claim Twitch drops')}
  102. ${createToggle('enableGogRedeemButton', 'GOG Redeem Button', 'Add GOG redeem button on Amazon Gaming')}
  103. ${createToggle('enableLegacyGamesRedeemButton', 'Legacy Games Button', 'Add Legacy Games redeem button on Amazon Gaming')}
  104. ${createToggle('enableHideGlobalMenu', 'Hide Global Menu', 'Hide the global menu on Twitch')}
  105. ${createToggle('enableAutoRefreshDrops', 'Auto Refresh Drops', 'Automatically refresh drops inventory page every 15 minutes')}
  106. ${createToggle('enableClaimAllButton', 'Enable Claim All Button', 'Add Claim All button on Amazon Gaming')}
  107. ${createToggle('enableRemoveAllButton', 'Enable Remove All Button', 'Add Remove All button on Amazon Gaming')}
  108. <div class="te-key-setting">
  109. <label for="settingsKey" class="te-key-label">Settings Toggle Key:</label>
  110. <div class="te-key-input-container">
  111. <input type="text" id="settingsKey" class="te-key-input" value="${CONFIG.settingsKey}" readonly>
  112. <button id="changeKeyButton" class="te-key-button">Change Key</button>
  113. </div>
  114. <div id="keyInstructions" class="te-key-instructions" style="display:none;">Press any key...</div>
  115. </div>
  116. <div class="te-button-container">
  117. <button id="saveSettingsButton" class="te-button te-button-save">Save</button>
  118. <button id="cancelSettingsButton" class="te-button te-button-cancel">Cancel</button>
  119. </div>
  120. </div>
  121. `;
  122.  
  123. const styleSheet = `
  124. <style>
  125. .te-dialog {
  126. position: fixed;
  127. top: 50%;
  128. left: 50%;
  129. transform: translate(-50%, -50%);
  130. background: rgba(18, 16, 24, 0.9);
  131. border: 1px solid #772ce8;
  132. border-radius: 8px;
  133. padding: 20px;
  134. box-shadow: 0 0 20px rgba(0, 0, 0, 0.7);
  135. z-index: 9999999; /* Increased z-index to ensure it appears above all elements */
  136. color: white;
  137. width: 350px;
  138. font-family: 'Roobert', 'Inter', Helvetica, Arial, sans-serif;
  139. }
  140. .te-dialog h3 {
  141. margin-top: 0;
  142. font-size: 1.4em;
  143. text-align: center;
  144. margin-bottom: 20px;
  145. color: #bf94ff;
  146. }
  147. .te-toggle-container {
  148. display: flex;
  149. justify-content: space-between;
  150. align-items: center;
  151. margin-bottom: 15px;
  152. }
  153. .te-toggle-label {
  154. flex-grow: 1;
  155. font-size: 0.95em;
  156. }
  157. .te-toggle {
  158. position: relative;
  159. display: inline-block;
  160. width: 50px;
  161. height: 24px;
  162. }
  163. .te-toggle input {
  164. position: absolute;
  165. width: 100%;
  166. height: 100%;
  167. opacity: 0;
  168. cursor: pointer;
  169. margin: 0;
  170. }
  171. .te-toggle-slider {
  172. position: absolute;
  173. cursor: pointer;
  174. top: 0;
  175. left: 0;
  176. right: 0;
  177. bottom: 0;
  178. background-color: #333;
  179. transition: .4s;
  180. border-radius: 24px;
  181. }
  182. .te-toggle-slider:before {
  183. position: absolute;
  184. content: "";
  185. height: 16px;
  186. width: 16px;
  187. left: 4px;
  188. bottom: 4px;
  189. background-color: white;
  190. transition: .4s;
  191. border-radius: 50%;
  192. }
  193. .te-toggle input:checked + .te-toggle-slider {
  194. background-color: #9147ff;
  195. }
  196. .te-toggle input:checked + .te-toggle-slider:before {
  197. transform: translateX(26px);
  198. }
  199. .te-button-container {
  200. display: flex;
  201. justify-content: space-between;
  202. margin-top: 20px;
  203. }
  204. .te-button {
  205. padding: 8px 16px;
  206. border: none;
  207. border-radius: 4px;
  208. cursor: pointer;
  209. font-size: 0.95em;
  210. transition: background-color 0.3s;
  211. }
  212. .te-button-save {
  213. background-color: #9147ff;
  214. color: white;
  215. }
  216. .te-button-save:hover {
  217. background-color: #772ce8;
  218. }
  219. .te-button-cancel {
  220. background-color: #464649;
  221. color: white;
  222. }
  223. .te-button-cancel:hover {
  224. background-color: #2d2d30;
  225. }
  226. .te-key-setting {
  227. margin-top: 20px;
  228. padding-top: 15px;
  229. border-top: 1px solid #464649;
  230. }
  231. .te-key-label {
  232. display: block;
  233. margin-bottom: 10px;
  234. font-size: 0.95em;
  235. }
  236. .te-key-input-container {
  237. display: flex;
  238. gap: 10px;
  239. }
  240. .te-key-input {
  241. flex: 1;
  242. background-color: #18181b;
  243. color: white;
  244. border: 1px solid #464649;
  245. border-radius: 4px;
  246. padding: 8px;
  247. text-align: center;
  248. font-size: 14px;
  249. }
  250. .te-key-button {
  251. background-color: #464649;
  252. color: white;
  253. border: none;
  254. border-radius: 4px;
  255. padding: 8px 12px;
  256. cursor: pointer;
  257. font-size: 0.85em;
  258. }
  259. .te-key-button:hover {
  260. background-color: #5c5c5f;
  261. }
  262. .te-key-instructions {
  263. margin-top: 10px;
  264. font-size: 0.85em;
  265. color: #bf94ff;
  266. text-align: center;
  267. }
  268. </style>
  269. `;
  270.  
  271. const dialogWrapper = document.createElement('div');
  272. dialogWrapper.innerHTML = styleSheet + dialogHTML;
  273. document.body.appendChild(dialogWrapper);
  274.  
  275. // Add event listeners to toggles with improved feedback - MODIFIED
  276. // Store toggle changes in memory instead of immediately updating CONFIG
  277. const pendingChanges = {}; // Object to track pending changes
  278.  
  279. document.querySelectorAll('.te-toggle input').forEach(toggle => {
  280. toggle.addEventListener('change', (event) => {
  281. const { id, checked } = event.target;
  282. Logger.info(`Toggle changed: ${id} = ${checked}`);
  283. // Instead of updating CONFIG directly, store the pending change
  284. pendingChanges[id] = checked;
  285. });
  286. });
  287.  
  288. // Add event listeners to buttons
  289. document.getElementById('saveSettingsButton').addEventListener('click', () => saveAndCloseDialog(pendingChanges));
  290. document.getElementById('cancelSettingsButton').addEventListener('click', closeDialog);
  291.  
  292. // Add event listener for change key button
  293. const changeKeyButton = document.getElementById('changeKeyButton');
  294. changeKeyButton.addEventListener('click', function () {
  295. const keyInput = document.getElementById('settingsKey');
  296. const keyInstructions = document.getElementById('keyInstructions');
  297.  
  298. // Show instructions and focus on input
  299. keyInstructions.style.display = 'block';
  300. keyInstructions.textContent = 'Press key combination (e.g. Ctrl+Shift+K)...';
  301. keyInput.value = 'Press keys...';
  302.  
  303. // Change button text to indicate canceling is possible
  304. changeKeyButton.textContent = 'Cancel';
  305.  
  306. // Flag to track if we're in key capture mode
  307. let capturingKey = true;
  308.  
  309. // Variables to store key combination
  310. let modifiers = {
  311. ctrl: false,
  312. alt: false,
  313. shift: false,
  314. meta: false
  315. };
  316. let mainKey = '';
  317.  
  318. // Function to format current key combination
  319. const formatKeyCombination = () => {
  320. const parts = [];
  321. if (modifiers.ctrl) parts.push('Ctrl');
  322. if (modifiers.alt) parts.push('Alt');
  323. if (modifiers.shift) parts.push('Shift');
  324. if (modifiers.meta) parts.push('Meta');
  325. if (mainKey && !['Control', 'Alt', 'Shift', 'Meta'].includes(mainKey)) {
  326. parts.push(mainKey);
  327. }
  328. return parts.join('+');
  329. };
  330.  
  331. // Function to update the input with current combination
  332. const updateKeyDisplay = () => {
  333. const combination = formatKeyCombination();
  334. if (combination) {
  335. keyInput.value = combination;
  336. } else {
  337. keyInput.value = 'Press keys...';
  338. }
  339. };
  340.  
  341. // Function to handle key down
  342. const handleKeyDown = function (e) {
  343. if (!capturingKey) return;
  344.  
  345. e.preventDefault();
  346. e.stopPropagation();
  347.  
  348. // Track modifier keys
  349. if (e.key === 'Control' || e.key === 'Alt' || e.key === 'Shift' || e.key === 'Meta') {
  350. switch (e.key) {
  351. case 'Control': modifiers.ctrl = true; break;
  352. case 'Alt': modifiers.alt = true; break;
  353. case 'Shift': modifiers.shift = true; break;
  354. case 'Meta': modifiers.meta = true; break;
  355. }
  356. } else {
  357. // Track main key
  358. mainKey = e.key;
  359. }
  360.  
  361. // Update the display
  362. updateKeyDisplay();
  363. };
  364.  
  365. // Function to handle key up
  366. const handleKeyUp = function (e) {
  367. if (!capturingKey) return;
  368.  
  369. e.preventDefault();
  370. e.stopPropagation();
  371.  
  372. // Handle modifier keys being released
  373. if (e.key === 'Control' || e.key === 'Alt' || e.key === 'Shift' || e.key === 'Meta') {
  374. switch (e.key) {
  375. case 'Control': modifiers.ctrl = false; break;
  376. case 'Alt': modifiers.alt = false; break;
  377. case 'Shift': modifiers.shift = false; break;
  378. case 'Meta': modifiers.meta = false; break;
  379. }
  380.  
  381. // Update the display
  382. updateKeyDisplay();
  383. } else {
  384. // If a non-modifier key was released, complete the capture
  385. const keyCombination = formatKeyCombination();
  386.  
  387. // Only save if we have a valid combination (at least one key)
  388. if (keyCombination && keyCombination !== 'Press keys...') {
  389. keyInput.value = keyCombination;
  390.  
  391. // Exit key capture mode
  392. document.removeEventListener('keydown', handleKeyDown, true);
  393. document.removeEventListener('keyup', handleKeyUp, true);
  394. keyInstructions.style.display = 'none';
  395. changeKeyButton.textContent = 'Change Key';
  396. capturingKey = false;
  397.  
  398. // Log the captured combination
  399. Logger.info(`Key combination captured: ${keyCombination}`);
  400. }
  401. }
  402. };
  403.  
  404. // Function to cancel key capture
  405. const cancelCapture = function () {
  406. if (!capturingKey) return;
  407.  
  408. document.removeEventListener('keydown', handleKeyDown, true);
  409. document.removeEventListener('keyup', handleKeyUp, true);
  410. keyInput.value = CONFIG.settingsKey;
  411. keyInstructions.style.display = 'none';
  412. changeKeyButton.textContent = 'Change Key';
  413. capturingKey = false;
  414. };
  415.  
  416. // Allow canceling key capture by clicking the button again
  417. changeKeyButton.addEventListener('click', cancelCapture, { once: true });
  418.  
  419. // Capture key events
  420. document.addEventListener('keydown', handleKeyDown, true);
  421. document.addEventListener('keyup', handleKeyUp, true);
  422. });
  423. }
  424.  
  425. function createToggle(id, label, title) {
  426. return `
  427. <div class="te-toggle-container" title="${title}">
  428. <label class="te-toggle">
  429. <input type="checkbox" id="${id}" ${CONFIG[id] ? 'checked' : ''}>
  430. <span class="te-toggle-slider"></span>
  431. </label>
  432. <label for="${id}" class="te-toggle-label">${label}</label>
  433. </div>
  434. `;
  435. }
  436.  
  437. // Modified saveAndCloseDialog function to apply changes dynamically
  438. function saveAndCloseDialog(pendingChanges = {}) {
  439. // Create a deep copy of the CONFIG object before any changes are made
  440. const oldConfig = JSON.parse(JSON.stringify(CONFIG));
  441. let changesMade = false;
  442.  
  443. // Improved debugging output
  444. Logger.info("Checking for settings changes...");
  445.  
  446. // Save toggle settings
  447. Object.keys(CONFIG).forEach(key => {
  448. if (key === 'settingsKey') return; // Handle separately
  449.  
  450. // Check if this setting has a pending change
  451. if (pendingChanges.hasOwnProperty(key)) {
  452. const oldValue = oldConfig[key];
  453. const newValue = pendingChanges[key];
  454.  
  455. // Log the comparison for debugging
  456. Logger.info(`Comparing ${key}: old=${oldValue} (${typeof oldValue}), new=${newValue} (${typeof newValue})`);
  457.  
  458. // Compare values - both should be booleans for toggle settings
  459. if (oldValue !== newValue) {
  460. changesMade = true;
  461. Logger.info(`Changed ${key} from ${oldValue} to ${newValue}`);
  462. CONFIG[key] = newValue;
  463. GM_setValue(key, newValue);
  464. }
  465. } else {
  466. // If no pending change, get value from form element
  467. const element = document.getElementById(key);
  468. if (element) {
  469. const oldValue = oldConfig[key];
  470. const newValue = element.checked;
  471.  
  472. // Log the comparison for debugging
  473. Logger.info(`Comparing ${key}: old=${oldValue} (${typeof oldValue}), new=${newValue} (${typeof newValue})`);
  474.  
  475. // Compare values
  476. if (oldValue !== newValue) {
  477. changesMade = true;
  478. Logger.info(`Changed ${key} from ${oldValue} to ${newValue}`);
  479. CONFIG[key] = newValue;
  480. GM_setValue(key, newValue);
  481. }
  482. }
  483. }
  484. });
  485.  
  486. // Save settings key
  487. const keyInput = document.getElementById('settingsKey');
  488. if (keyInput && keyInput.value !== oldConfig.settingsKey) {
  489. changesMade = true;
  490. Logger.info(`Changed settings key from ${oldConfig.settingsKey} to ${keyInput.value}`);
  491. CONFIG.settingsKey = keyInput.value;
  492. GM_setValue('settingsKey', keyInput.value);
  493. }
  494.  
  495. closeDialog();
  496.  
  497. if (changesMade) {
  498. Logger.success('Settings saved and applied immediately');
  499. applySettingsChanges(oldConfig);
  500. } else {
  501. // Show more helpful message when no changes are detected
  502. Logger.info('No changes detected. Settings remain the same.');
  503. }
  504. }
  505.  
  506. // Function to dynamically apply settings changes
  507. function applySettingsChanges(oldConfig) {
  508. // Restart observers or update UI elements based on config changes
  509.  
  510. // Handle auto refresh drops changes
  511. if (oldConfig.enableAutoRefreshDrops !== CONFIG.enableAutoRefreshDrops) {
  512. setupAutoRefreshDrops();
  513. }
  514.  
  515. // Handle claim points observer changes
  516. if (oldConfig.enableAutoClaimPoints !== CONFIG.enableAutoClaimPoints) {
  517. restartClaimPointsObserver();
  518. }
  519.  
  520. // Handle claim drops observer changes
  521. if (oldConfig.enableClaimDrops !== CONFIG.enableClaimDrops) {
  522. restartClaimDropsObserver();
  523. }
  524.  
  525. // Handle Amazon gaming buttons changes
  526. if (oldConfig.enableGogRedeemButton !== CONFIG.enableGogRedeemButton ||
  527. oldConfig.enableLegacyGamesRedeemButton !== CONFIG.enableLegacyGamesRedeemButton) {
  528. updateRedeeemButtons();
  529. }
  530.  
  531. // Handle PrimeOfferPopover changes for Claim All/Remove All buttons
  532. if (oldConfig.enableClaimAllButton !== CONFIG.enableClaimAllButton ||
  533. oldConfig.enableRemoveAllButton !== CONFIG.enableRemoveAllButton) {
  534. if (document.getElementById("PrimeOfferPopover-header")) {
  535. updatePrimeOfferButtons();
  536. }
  537. }
  538.  
  539. // Handle Theater Mode changes
  540. if (!oldConfig.enableTheaterMode && CONFIG.enableTheaterMode) {
  541. enableTheaterMode();
  542. }
  543.  
  544. // Handle Hide Global Menu changes
  545. if (CONFIG.enableHideGlobalMenu) {
  546. hideGlobalMenu();
  547. } else if (!CONFIG.enableHideGlobalMenu && oldConfig.enableHideGlobalMenu) {
  548. showGlobalMenu();
  549. }
  550. }
  551.  
  552. // Function to show global menu (when setting is turned off)
  553. function showGlobalMenu() {
  554. const GLOBAL_MENU_SELECTOR = 'div.ScBalloonWrapper-sc-14jr088-0.eEhNFm';
  555. const globalMenu = document.querySelector(GLOBAL_MENU_SELECTOR);
  556. if (globalMenu) {
  557. globalMenu.style.display = '';
  558. Logger.info('Global menu restored');
  559. }
  560. }
  561.  
  562. // Variables to track observers
  563. let claimPointsObserver = null;
  564. let claimDropsObserver = null;
  565. let autoRefreshInterval = null;
  566.  
  567. // Function to restart claim points observer
  568. function restartClaimPointsObserver() {
  569. if (claimPointsObserver) {
  570. claimPointsObserver.disconnect();
  571. claimPointsObserver = null;
  572. Logger.info('Auto claim points observer disconnected');
  573. }
  574.  
  575. if (CONFIG.enableAutoClaimPoints) {
  576. setupAutoClaimBonus();
  577. }
  578. }
  579.  
  580. // Function to restart claim drops observer
  581. function restartClaimDropsObserver() {
  582. if (claimDropsObserver) {
  583. claimDropsObserver.disconnect();
  584. claimDropsObserver = null;
  585. Logger.info('Claim drops observer disconnected');
  586. }
  587.  
  588. if (CONFIG.enableClaimDrops) {
  589. setupClaimDrops();
  590. }
  591. }
  592.  
  593. // Function to setup auto refresh drops timer
  594. function setupAutoRefreshDrops() {
  595. if (autoRefreshInterval) {
  596. clearInterval(autoRefreshInterval);
  597. autoRefreshInterval = null;
  598. Logger.info('Auto refresh drops timer cleared');
  599. }
  600.  
  601. if (CONFIG.enableAutoRefreshDrops) {
  602. autoRefreshInterval = setInterval(function () {
  603. if (window.location.href.startsWith('https://www.twitch.tv/drops/inventory')) {
  604. Logger.info('Auto-refreshing drops inventory page');
  605. window.location.reload();
  606. }
  607. }, 15 * 60000);
  608. Logger.info('Auto refresh drops timer started');
  609. }
  610. }
  611.  
  612. // Function to update redeem buttons
  613. function updateRedeeemButtons() {
  614. if (window.location.hostname === 'gaming.amazon.com') {
  615. if (CONFIG.enableGogRedeemButton) {
  616. addGogRedeemButton();
  617. } else {
  618. // Remove GOG buttons
  619. const gogButtons = document.querySelectorAll('.gog-redeem-button');
  620. gogButtons.forEach(button => button.remove());
  621. Logger.info('GOG redeem buttons removed');
  622. }
  623.  
  624. if (CONFIG.enableLegacyGamesRedeemButton) {
  625. addLegacyGamesRedeemButton();
  626. } else {
  627. // Remove Legacy Games buttons
  628. const legacyButtons = document.querySelectorAll('.legacy-games-redeem-button');
  629. legacyButtons.forEach(button => button.remove());
  630. Logger.info('Legacy Games redeem buttons removed');
  631. }
  632. }
  633. }
  634.  
  635. // Function to update the Prime Offer Popover buttons
  636. function updatePrimeOfferButtons() {
  637. const primeOfferHeader = document.getElementById("PrimeOfferPopover-header");
  638. if (!primeOfferHeader) return;
  639.  
  640. let o = new MutationObserver((m) => {
  641. if (!CONFIG.enableClaimAllButton && !CONFIG.enableRemoveAllButton) {
  642. // Remove all custom buttons
  643. const customButtonsContainer = document.querySelector('#PrimeOfferPopover-header > div');
  644. if (customButtonsContainer) {
  645. customButtonsContainer.remove();
  646. }
  647. return;
  648. }
  649.  
  650. // Trigger a refresh of the buttons
  651. const headerElement = document.getElementById("PrimeOfferPopover-header");
  652. if (headerElement) {
  653. // Force refresh by triggering our main observer
  654. const dummyDiv = document.createElement('div');
  655. document.body.appendChild(dummyDiv);
  656. document.body.removeChild(dummyDiv);
  657. }
  658. });
  659.  
  660. // Trigger the observer
  661. o.observe(document.body, { childList: true });
  662. setTimeout(() => o.disconnect(), 500); // Disconnect after a short time
  663. }
  664.  
  665. // Function to setup auto claim bonus
  666. function setupAutoClaimBonus() {
  667. if (!CONFIG.enableAutoClaimPoints || !MutationObserver) return;
  668.  
  669. Logger.info('Auto claimer is enabled.');
  670.  
  671. claimPointsObserver = new MutationObserver(mutationsList => {
  672. for (let mutation of mutationsList) {
  673. if (mutation.type === 'childList' && CONFIG.enableAutoClaimPoints) {
  674. let bonus = document.querySelector(CLAIMABLE_BONUS_SELECTOR);
  675. if (bonus && !claiming) {
  676. bonus.click();
  677. let date = new Date();
  678. claiming = true;
  679. setTimeout(() => {
  680. Logger.success('Claimed at ' + date.toLocaleString());
  681. claiming = false;
  682. }, Math.random() * 1000 + 2000);
  683. }
  684. }
  685. }
  686. });
  687.  
  688. claimPointsObserver.observe(document.body, { childList: true, subtree: true });
  689. }
  690.  
  691. // Function to setup claim drops
  692. function setupClaimDrops() {
  693. if (!CONFIG.enableClaimDrops || !MutationObserver) return;
  694.  
  695. var onMutate = function (mutationsList) {
  696. mutationsList.forEach(mutation => {
  697. if (CONFIG.enableClaimDrops && document.querySelector(CLAIM_DROPS_SELECTOR)) {
  698. document.querySelector(CLAIM_DROPS_SELECTOR).click();
  699. }
  700. });
  701. };
  702.  
  703. claimDropsObserver = new MutationObserver(onMutate);
  704. claimDropsObserver.observe(document.body, { childList: true, subtree: true });
  705. Logger.info('Claim drops observer started');
  706. }
  707.  
  708. function closeDialog() {
  709. const dialog = document.getElementById('twitchEnhancementsDialog');
  710. if (dialog) {
  711. dialog.remove();
  712. }
  713. }
  714.  
  715. function toggleSettingsDialog() {
  716. const dialog = document.getElementById('twitchEnhancementsDialog');
  717. if (dialog) {
  718. dialog.remove();
  719. } else {
  720. createSettingsDialog();
  721. }
  722. }
  723.  
  724. // Register menu command
  725. GM_registerMenuCommand('Twitch Enhancements Settings', toggleSettingsDialog);
  726.  
  727. // Function to click a button
  728. function clickButton(buttonSelector) {
  729. if (!MutationObserver) return;
  730.  
  731. const observer = new MutationObserver((mutationsList, observer) => {
  732. for (let mutation of mutationsList) {
  733. if (mutation.addedNodes.length) {
  734. const button = document.querySelector(buttonSelector);
  735. if (button) {
  736. button.click();
  737. observer.disconnect();
  738. return;
  739. }
  740. }
  741. }
  742. });
  743.  
  744. observer.observe(document, { childList: true, subtree: true });
  745. }
  746.  
  747. // Function to enable theater mode
  748. function enableTheaterMode() {
  749. if (!CONFIG.enableTheaterMode) return;
  750.  
  751. const player = document.querySelector(PLAYER_SELECTOR);
  752. if (player) {
  753. if (!player.classList.contains(THEATER_MODE_CLASS)) {
  754. clickButton(THEATER_MODE_BUTTON_SELECTOR);
  755. }
  756. } else {
  757. Logger.error('Player not found');
  758. }
  759. }
  760.  
  761. // Function to hide the global menu
  762. function hideGlobalMenu() {
  763. if (!CONFIG.enableHideGlobalMenu) return;
  764.  
  765. const GLOBAL_MENU_SELECTOR = 'div.ScBalloonWrapper-sc-14jr088-0.eEhNFm';
  766. const globalMenu = document.querySelector(GLOBAL_MENU_SELECTOR);
  767. if (globalMenu) {
  768. globalMenu.style.display = 'none';
  769. } else {
  770. Logger.error('Global menu not found');
  771. }
  772. }
  773.  
  774. // Function to automatically claim channel points
  775. function autoClaimBonus() {
  776. if (!CONFIG.enableAutoClaimPoints || !MutationObserver) return;
  777.  
  778. Logger.info('Auto claimer is enabled.');
  779.  
  780. let observer = new MutationObserver(mutationsList => {
  781. for (let mutation of mutationsList) {
  782. if (mutation.type === 'childList') {
  783. let bonus = document.querySelector(CLAIMABLE_BONUS_SELECTOR);
  784. if (bonus && !claiming) {
  785. bonus.click();
  786. let date = new Date();
  787. claiming = true;
  788. setTimeout(() => {
  789. Logger.success('Claimed at ' + date.toLocaleString());
  790. claiming = false;
  791. }, Math.random() * 1000 + 2000);
  792. }
  793. }
  794. }
  795. });
  796.  
  797. observer.observe(document.body, { childList: true, subtree: true });
  798. }
  799.  
  800. // Function to claim prime rewards with retry
  801. function claimPrimeReward() {
  802. if (!CONFIG.enableClaimPrimeRewards) return;
  803.  
  804. const maxAttempts = 5;
  805. let attempts = 0;
  806.  
  807. const tryClaim = () => {
  808. if (attempts >= maxAttempts) {
  809. Logger.warning('Max attempts reached for claiming prime reward');
  810. return;
  811. }
  812. attempts++;
  813.  
  814. const element = document.querySelector(PRIME_REWARD_SELECTOR) || document.querySelector(PRIME_REWARD_SELECTOR_2);
  815. if (element) {
  816. element.click();
  817. Logger.success('Prime reward claimed');
  818. } else {
  819. Logger.info(`Attempt ${attempts}/${maxAttempts}: Waiting for prime reward button...`);
  820. setTimeout(tryClaim, 1000);
  821. }
  822. };
  823.  
  824. setTimeout(tryClaim, 2000);
  825. }
  826.  
  827. // Function to claim drops
  828. function claimDrops() {
  829. if (!CONFIG.enableClaimDrops || !MutationObserver) return;
  830.  
  831. var onMutate = function (mutationsList) {
  832. mutationsList.forEach(mutation => {
  833. if (document.querySelector(CLAIM_DROPS_SELECTOR)) document.querySelector(CLAIM_DROPS_SELECTOR).click();
  834. })
  835. }
  836. var observer = new MutationObserver(onMutate);
  837. observer.observe(document.body, { childList: true, subtree: true });
  838. }
  839.  
  840. // Function to add the "Redeem on GOG" button
  841. function addGogRedeemButton() {
  842. if (!CONFIG.enableGogRedeemButton) return;
  843.  
  844. const claimCodeButton = document.querySelector('p[title="Claim Code"]');
  845. if (claimCodeButton && !document.querySelector('.gog-redeem-button')) {
  846. const claimCodeWrapper = claimCodeButton.closest('.claim-button-wrapper');
  847. if (claimCodeWrapper) {
  848. const gogRedeemButtonDiv = document.createElement('div');
  849. gogRedeemButtonDiv.className = 'claim-button tw-align-self-center gog-redeem-button';
  850.  
  851. const gogRedeemButton = document.createElement('a');
  852. gogRedeemButton.href = 'https://www.gog.com/en/redeem';
  853. gogRedeemButton.rel = 'noopener noreferrer';
  854. gogRedeemButton.className = 'tw-interactive tw-button tw-button--full-width';
  855. gogRedeemButton.dataset.aTarget = 'redeem-on-gog';
  856. gogRedeemButton.innerHTML = '<span class="tw-button__text" data-a-target="tw-button-text"><div class="tw-inline-flex"><p class="" title="Redeem on GOG">Redeem on GOG</p>&nbsp;&nbsp;<figure aria-label="ExternalLinkWithBox" class="tw-svg"><svg class="tw-svg__asset tw-svg__asset--externallinkwithbox tw-svg__asset--inherit" width="12px" height="12px" version="1.1" viewBox="0 0 11 11" x="0px" y="0px"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.3125 6.875V9.625C10.3125 10.3844 9.69689 11 8.9375 11H1.375C0.615608 11 0 10.3844 0 9.625V2.0625C0 1.30311 0.615608 0.6875 1.375 0.6875H4.125V2.0625H1.375V9.625H8.9375V6.875H10.3125ZM9.62301 2.34727L5.29664 6.67364L4.32437 5.70136L8.65073 1.375H6.18551V0H10.998V4.8125H9.62301V2.34727Z"></path></svg></figure></div></span>';
  857.  
  858. gogRedeemButtonDiv.appendChild(gogRedeemButton);
  859. claimCodeWrapper.appendChild(gogRedeemButtonDiv);
  860.  
  861. gogRedeemButton.addEventListener('click', function (e) {
  862. e.preventDefault();
  863. const codeInput = document.querySelector('input[aria-label]');
  864. if (codeInput) {
  865. const code = codeInput.value;
  866. if (code) {
  867. navigator.clipboard.writeText(code).then(function () {
  868. window.location.href = 'https://www.gog.com/en/redeem';
  869. });
  870. }
  871. }
  872. });
  873.  
  874. const style = document.createElement('style');
  875. style.innerHTML = `
  876. .claim-button-wrapper {
  877. display: flex;
  878. flex-direction: column;
  879. margin-top: 15px;
  880. }
  881. .claim-button,
  882. .gog-redeem-button {
  883. margin: 5px 0;
  884. }
  885. .tw-mg-l-1 {
  886. margin-top: 10px;
  887. }
  888. .claimable-item {
  889. flex-direction: column !important;
  890. gap: 15px;
  891. }
  892. .tw-flex-grow-1 {
  893. width: 100%;
  894. }
  895. `;
  896. document.head.appendChild(style);
  897. }
  898. }
  899. }
  900.  
  901. // Function to redeem code on GOG
  902. function redeemCodeOnGOG() {
  903. navigator.clipboard.readText().then(function (code) {
  904. const codeInput = document.querySelector(GOG_REDEEM_CODE_INPUT_SELECTOR);
  905. if (codeInput) {
  906. codeInput.value = code;
  907.  
  908. // Simulate input event to ensure any listeners are triggered
  909. const inputEvent = new Event('input', { bubbles: true });
  910. codeInput.dispatchEvent(inputEvent);
  911.  
  912. // Click the continue button after a short delay
  913. setTimeout(() => {
  914. const continueButton = document.querySelector(GOG_CONTINUE_BUTTON_SELECTOR);
  915. if (continueButton) {
  916. continueButton.click();
  917.  
  918. // Wait for the "Redeem" button to appear and click it
  919. const checkRedeemButton = setInterval(() => {
  920. const redeemButton = document.querySelector(GOG_FINAL_REDEEM_BUTTON_SELECTOR);
  921. if (redeemButton) {
  922. clearInterval(checkRedeemButton);
  923. redeemButton.click();
  924. }
  925. }, 500); // Check every 500ms for the Redeem button
  926. }
  927. }, 500); // Adjust the delay as needed
  928. }
  929. }).catch(function (err) {
  930. Logger.error('Failed to read clipboard contents: ' + err);
  931. });
  932. }
  933.  
  934. // Function to add the "Redeem on Legacy Games" button
  935. function addLegacyGamesRedeemButton() {
  936. if (!CONFIG.enableLegacyGamesRedeemButton) return;
  937.  
  938. const copyCodeButton = document.querySelector('button[aria-label="Copy code to your clipboard"]');
  939. if (copyCodeButton && !document.querySelector('.legacy-games-redeem-button')) {
  940. const copyCodeWrapper = copyCodeButton.closest('.copy-button-wrapper');
  941. if (copyCodeWrapper) {
  942. const legacyGamesRedeemButtonDiv = document.createElement('div');
  943. legacyGamesRedeemButtonDiv.className = 'copy-button tw-align-self-center legacy-games-redeem-button';
  944.  
  945. const legacyGamesRedeemButton = document.createElement('button');
  946. legacyGamesRedeemButton.ariaLabel = 'Redeem on Legacy Games';
  947. legacyGamesRedeemButton.className = 'tw-interactive tw-button tw-button--full-width';
  948. legacyGamesRedeemButton.dataset.aTarget = 'redeem-on-legacy-games';
  949. legacyGamesRedeemButton.innerHTML = '<span class="tw-button__text" data-a-target="tw-button-text">Redeem on Legacy Games</span>';
  950.  
  951. legacyGamesRedeemButtonDiv.appendChild(legacyGamesRedeemButton);
  952. copyCodeWrapper.appendChild(legacyGamesRedeemButtonDiv);
  953.  
  954. legacyGamesRedeemButton.addEventListener('click', function (e) {
  955. e.preventDefault();
  956. const codeInput = document.querySelector('input[aria-label]');
  957. if (codeInput) {
  958. const code = codeInput.value;
  959. if (code) {
  960. navigator.clipboard.writeText(code).then(function () {
  961. const email = GM_getValue('legacyGamesEmail', null);
  962. if (!email) {
  963. const userEmail = prompt('Please enter your email address:');
  964. if (userEmail) {
  965. GM_setValue('legacyGamesEmail', userEmail);
  966. window.location.href = LEGACY_GAMES_REDEEM_URL;
  967. }
  968. } else {
  969. window.location.href = LEGACY_GAMES_REDEEM_URL;
  970. }
  971. });
  972. }
  973. }
  974. });
  975.  
  976. const style = document.createElement('style');
  977. style.innerHTML = `
  978. .copy-button-wrapper {
  979. display: flex;
  980. flex-direction: column;
  981. margin-top: 15px;
  982. }
  983. .copy-button,
  984. .legacy-games-redeem-button {
  985. margin: 5px 0;
  986. }
  987. .tw-mg-l-1 {
  988. margin-top: 10px;
  989. }
  990. .claimable-item {
  991. flex-direction: column !important;
  992. gap: 15px;
  993. }
  994. .tw-flex-grow-1 {
  995. width: 100%;
  996. }
  997. `;
  998. document.head.appendChild(style);
  999. }
  1000. }
  1001. }
  1002.  
  1003. // Function to redeem code on Legacy Games
  1004. function redeemCodeOnLegacyGames() {
  1005. const maxAttempts = 5;
  1006. let attempts = 0;
  1007.  
  1008. const tryRedeem = () => {
  1009. if (attempts >= maxAttempts) return;
  1010. attempts++;
  1011.  
  1012. navigator.clipboard.readText().then(function (code) {
  1013. const codeInput = document.querySelector(LEGACY_GAMES_CODE_INPUT_SELECTOR);
  1014. const emailInput = document.querySelector(LEGACY_GAMES_EMAIL_INPUT_SELECTOR);
  1015. const emailValidateInput = document.querySelector(LEGACY_GAMES_EMAIL_VALIDATE_INPUT_SELECTOR);
  1016. const submitButton = document.querySelector(LEGACY_GAMES_SUBMIT_BUTTON_SELECTOR);
  1017. const newsletterCheckbox = document.querySelector(LEGACY_GAMES_NEWSLETTER_CHECKBOX_SELECTOR);
  1018. const email = GM_getValue('legacyGamesEmail', null);
  1019.  
  1020. if (!codeInput || !emailInput || !emailValidateInput || !submitButton) {
  1021. Logger.info('Waiting for elements to load...');
  1022. setTimeout(tryRedeem, 1000);
  1023. return;
  1024. }
  1025.  
  1026. if (email && code) {
  1027. // Fill in the form
  1028. codeInput.value = code;
  1029. emailInput.value = email;
  1030. emailValidateInput.value = email;
  1031.  
  1032. // Ensure newsletter checkbox is unchecked
  1033. if (newsletterCheckbox) {
  1034. newsletterCheckbox.checked = false;
  1035. }
  1036.  
  1037. // Trigger input events
  1038. [codeInput, emailInput, emailValidateInput].forEach(input => {
  1039. input.dispatchEvent(new Event('input', { bubbles: true }));
  1040. input.dispatchEvent(new Event('change', { bubbles: true }));
  1041. });
  1042.  
  1043. // Submit the form
  1044. setTimeout(() => {
  1045. submitButton.click();
  1046. Logger.success('Form submitted with code: ' + code + ' and email: ' + email);
  1047. }, 500);
  1048. }
  1049. }).catch(function (err) {
  1050. Logger.error('Failed to read clipboard contents: ' + err);
  1051. });
  1052. };
  1053.  
  1054. // Start the redemption process
  1055. setTimeout(tryRedeem, 2000);
  1056. }
  1057.  
  1058. // Function to open all "Claim Game" buttons in new tabs
  1059. function openClaimGameTabs() {
  1060. const claimGameButtons = document.querySelectorAll('div[data-a-target="tw-core-button-label-text"].Layout-sc-1xcs6mc-0.bFxzAY');
  1061. claimGameButtons.forEach(button => {
  1062. const parentButton = button.closest('a');
  1063. if (parentButton) {
  1064. window.open(parentButton.href, '_blank');
  1065. }
  1066. });
  1067. }
  1068.  
  1069. if (window.location.hostname === 'gaming.amazon.com') {
  1070. const observer = new MutationObserver((mutations, obs) => {
  1071. const claimCodeButton = document.querySelector('p[title="Claim Code"]');
  1072. if (claimCodeButton && CONFIG.enableGogRedeemButton) {
  1073. addGogRedeemButton();
  1074. }
  1075. const copyCodeButton = document.querySelector('button[aria-label="Copy code to your clipboard"]');
  1076. if (copyCodeButton && CONFIG.enableLegacyGamesRedeemButton) {
  1077. addLegacyGamesRedeemButton();
  1078. }
  1079. });
  1080.  
  1081. observer.observe(document, {
  1082. childList: true,
  1083. subtree: true
  1084. });
  1085.  
  1086. if (CONFIG.enableGogRedeemButton) addGogRedeemButton();
  1087. if (CONFIG.enableLegacyGamesRedeemButton) addLegacyGamesRedeemButton();
  1088. }
  1089.  
  1090. if (window.location.hostname === 'www.gog.com' && window.location.pathname === '/en/redeem') {
  1091. window.addEventListener('load', redeemCodeOnGOG);
  1092. }
  1093.  
  1094. if (window.location.hostname === 'promo.legacygames.com') {
  1095. window.addEventListener('load', redeemCodeOnLegacyGames);
  1096. }
  1097.  
  1098. setTimeout(enableTheaterMode, 1000);
  1099. setTimeout(setupAutoClaimBonus, 1000);
  1100. setTimeout(claimPrimeReward, 1000);
  1101. setTimeout(() => clickButton(CLOSE_MENU_BUTTON_SELECTOR), 1000);
  1102. setTimeout(() => clickButton(CLOSE_MODAL_BUTTON_SELECTOR), 1000);
  1103. setTimeout(hideGlobalMenu, 1000);
  1104. setTimeout(setupClaimDrops, 1000);
  1105.  
  1106. // Auto refresh drops inventory page
  1107. if (CONFIG.enableAutoRefreshDrops) {
  1108. setInterval(function () {
  1109. if (window.location.href.startsWith('https://www.twitch.tv/drops/inventory')) {
  1110. window.location.reload();
  1111. }
  1112. }, 15 * 60000);
  1113. }
  1114.  
  1115. // Add keyboard shortcut to toggle settings - now using the configured key
  1116. document.addEventListener('keyup', (event) => {
  1117. // Parse the configured key combination
  1118. const parts = CONFIG.settingsKey.split('+');
  1119. const requiredModifiers = {
  1120. Ctrl: parts.includes('Ctrl'),
  1121. Alt: parts.includes('Alt'),
  1122. Shift: parts.includes('Shift'),
  1123. Meta: parts.includes('Meta')
  1124. };
  1125.  
  1126. // The main key is the last part if it's not a modifier
  1127. const mainKey = parts.filter(part => !['Ctrl', 'Alt', 'Shift', 'Meta'].includes(part)).pop();
  1128.  
  1129. // Check if the event matches our configured combination
  1130. const matchesModifiers =
  1131. (!requiredModifiers.Ctrl || event.ctrlKey) &&
  1132. (!requiredModifiers.Alt || event.altKey) &&
  1133. (!requiredModifiers.Shift || event.shiftKey) &&
  1134. (!requiredModifiers.Meta || event.metaKey);
  1135.  
  1136. const matchesMainKey = mainKey ? event.key === mainKey : true;
  1137.  
  1138. if (matchesModifiers && matchesMainKey) {
  1139. // Only trigger on the exact key combination
  1140. if (
  1141. // If Ctrl is in the combination, ensure it's pressed
  1142. (!parts.includes('Ctrl') || event.ctrlKey) &&
  1143. // If Alt is in the combination, ensure it's pressed
  1144. (!parts.includes('Alt') || event.altKey) &&
  1145. // If Shift is in the combination, ensure it's pressed
  1146. (!parts.includes('Shift') || event.shiftKey) &&
  1147. // If Meta is in the combination, ensure it's pressed
  1148. (!parts.includes('Meta') || event.metaKey) &&
  1149. // If a main key is specified, ensure it matches
  1150. (mainKey ? event.key === mainKey : true)
  1151. ) {
  1152. // Prevent default behavior
  1153. event.preventDefault();
  1154. toggleSettingsDialog();
  1155.  
  1156. // Log for debugging
  1157. Logger.info(`${CONFIG.settingsKey} key combination pressed - toggling settings dialog`);
  1158. }
  1159. }
  1160. });
  1161.  
  1162. // Make sure event is captured at the document level with capture phase
  1163. document.addEventListener('keydown', (event) => {
  1164. // Parse the configured key combination
  1165. const parts = CONFIG.settingsKey.split('+');
  1166. const requiredModifiers = {
  1167. Ctrl: parts.includes('Ctrl'),
  1168. Alt: parts.includes('Alt'),
  1169. Shift: parts.includes('Shift'),
  1170. Meta: parts.includes('Meta')
  1171. };
  1172.  
  1173. // The main key is the last part if it's not a modifier
  1174. const mainKey = parts.filter(part => !['Ctrl', 'Alt', 'Shift', 'Meta'].includes(part)).pop();
  1175.  
  1176. // Check if the event matches our configured combination
  1177. const matchesModifiers =
  1178. (!requiredModifiers.Ctrl || event.ctrlKey) &&
  1179. (!requiredModifiers.Alt || event.altKey) &&
  1180. (!requiredModifiers.Shift || event.shiftKey) &&
  1181. (!requiredModifiers.Meta || event.metaKey);
  1182.  
  1183. const matchesMainKey = mainKey ? event.key === mainKey : true;
  1184.  
  1185. if (matchesModifiers && matchesMainKey) {
  1186. // Prevent default behavior for our combination
  1187. event.preventDefault();
  1188. }
  1189. }, true);
  1190.  
  1191. let o = new MutationObserver((m) => {
  1192. if (!CONFIG.enableClaimAllButton && !CONFIG.enableRemoveAllButton) return;
  1193.  
  1194. // Check if the PrimeOfferPopover-header element exists
  1195. const primeOfferHeader = document.getElementById("PrimeOfferPopover-header");
  1196. if (!primeOfferHeader) {
  1197. // If we're on a page where this element doesn't exist, we should stop
  1198. return;
  1199. }
  1200.  
  1201. let script = document.createElement("script");
  1202. script.innerHTML = `
  1203. // Add logger configuration for client-side script
  1204. const Logger = {
  1205. styles: {
  1206. info: 'color: #2196F3; font-weight: bold',
  1207. warning: 'color: #FFC107; font-weight: bold',
  1208. success: 'color: #4CAF50; font-weight: bold',
  1209. error: 'color: #F44336; font-weight: bold'
  1210. },
  1211. prefix: '[TwitchEnhancements]',
  1212. getTimestamp() {
  1213. return new Date().toISOString().split('T')[1].slice(0, -1);
  1214. },
  1215. info(msg) {
  1216. console.log(\`%c\${this.prefix} \${this.getTimestamp()} - \${msg}\`, this.styles.info);
  1217. },
  1218. warning(msg) {
  1219. console.warn(\`%c\${this.prefix} \${this.getTimestamp()} - \${msg}\`, this.styles.warning);
  1220. },
  1221. success(msg) {
  1222. console.log(\`%c\${this.prefix} \${this.getTimestamp()} - \${msg}\`, this.styles.success);
  1223. },
  1224. error(msg) {
  1225. console.error(\`%c\${this.prefix} \${this.getTimestamp()} - \${msg}\`, this.styles.error);
  1226. }
  1227. };
  1228.  
  1229. const openClaimGameTabs = () => {
  1230. // More specific selector targeting only prime offer buttons
  1231. const allButtonTexts = document.querySelectorAll('div[data-a-target="tw-core-button-label-text"]');
  1232.  
  1233. // Filter buttons to only include those with text "Claim Game" or just "Claim"
  1234. const claimGameButtons = Array.from(allButtonTexts).filter(button => {
  1235. const text = button.textContent.trim();
  1236. return (text === "Claim Game" || text === "Claim") &&
  1237. button.closest('a') && // Must be inside an anchor tag
  1238. button.closest('.prime-offer'); // Must be inside a prime offer
  1239. });
  1240.  
  1241. Logger.info(\`Found \${claimGameButtons.length} valid claim buttons\`);
  1242.  
  1243. // Open each valid claim button in a new tab
  1244. claimGameButtons.forEach(button => {
  1245. const parentButton = button.closest('a');
  1246. if (parentButton && parentButton.href &&
  1247. (parentButton.href.includes('gaming.amazon.com') ||
  1248. parentButton.href.includes('?ingress=twch'))) {
  1249. window.open(parentButton.href, '_blank');
  1250. }
  1251. });
  1252. };
  1253.  
  1254. const removeClaimedItems = () => {
  1255. // Find ALL items in the list, not just claimed ones
  1256. const allItems = document.querySelectorAll('.prime-offer');
  1257. let dismissedCount = 0;
  1258. let dismissButtons = [];
  1259.  
  1260. Logger.info(\`Found \${allItems.length} total items to dismiss\`);
  1261.  
  1262. // First collect all dismiss buttons - use multiple methods to ensure we catch all
  1263. // Method 1: Find buttons by attribute and data target
  1264. document.querySelectorAll('button[aria-label="Dismiss"][data-a-target="prime-offer-dismiss-button"]').forEach(btn => {
  1265. dismissButtons.push(btn);
  1266. });
  1267.  
  1268. // Method 2: Find buttons by test selector attribute as backup
  1269. document.querySelectorAll('button[data-test-selector="prime-offer-dismiss-button"]').forEach(btn => {
  1270. if (!dismissButtons.includes(btn)) {
  1271. dismissButtons.push(btn);
  1272. }
  1273. });
  1274.  
  1275. // Method 3: Find by class and structure if the above methods miss any
  1276. document.querySelectorAll('.prime-offer__dismiss button').forEach(btn => {
  1277. if (!dismissButtons.includes(btn)) {
  1278. dismissButtons.push(btn);
  1279. }
  1280. });
  1281.  
  1282. // Deduplicate just in case
  1283. dismissButtons = [...new Set(dismissButtons)];
  1284.  
  1285. Logger.info(\`Found \${dismissButtons.length} dismiss buttons to click\`);
  1286.  
  1287. // Process dismiss buttons with a delay to avoid UI lockups
  1288. if (dismissButtons.length > 0) {
  1289. const clickNextButton = (index) => {
  1290. if (index < dismissButtons.length) {
  1291. try {
  1292. dismissButtons[index].click();
  1293. dismissedCount++;
  1294.  
  1295. // Show progress in console
  1296. if (dismissedCount % 5 === 0 || dismissedCount === dismissButtons.length) {
  1297. Logger.info(\`Dismissed \${dismissedCount} of \${dismissButtons.length} items...\`);
  1298. }
  1299. } catch (e) {
  1300. Logger.error(\`Error clicking button \${index}: \` + e);
  1301. }
  1302.  
  1303. // Schedule next button click with a small delay
  1304. setTimeout(() => clickNextButton(index + 1), 75);
  1305. } else {
  1306. Logger.success(\`Completed! Dismissed \${dismissedCount} items total.\`);
  1307.  
  1308. // Look for any dismiss buttons that might have been missed
  1309. const remainingButtons = document.querySelectorAll('button[aria-label="Dismiss"]');
  1310. if (remainingButtons.length > 0) {
  1311. Logger.warning(\`Found \${remainingButtons.length} additional buttons to try\`);
  1312.  
  1313. // Try to click any remaining dismiss buttons as a final pass
  1314. remainingButtons.forEach(btn => {
  1315. try {
  1316. btn.click();
  1317. dismissedCount++;
  1318. } catch(e) {}
  1319. });
  1320.  
  1321. Logger.success(\`Final dismissal count: \${dismissedCount}\`);
  1322. }
  1323. }
  1324. };
  1325.  
  1326. // Start the dismissal process
  1327. clickNextButton(0);
  1328. } else {
  1329. Logger.warning('No dismiss buttons found to click');
  1330.  
  1331. // Last attempt fallback - try to find any button with "Dismiss" in aria-label
  1332. const fallbackButtons = document.querySelectorAll('button[aria-label="Dismiss"]');
  1333. if (fallbackButtons.length > 0) {
  1334. Logger.warning(\`Fallback: Found \${fallbackButtons.length} buttons with aria-label="Dismiss"\`);
  1335. fallbackButtons.forEach(btn => {
  1336. try {
  1337. btn.click();
  1338. dismissedCount++;
  1339. } catch(e) {}
  1340. });
  1341. Logger.success(\`Fallback dismissal completed: \${dismissedCount} items dismissed\`);
  1342. }
  1343. }
  1344. };
  1345. `;
  1346.  
  1347. // Safely clear and append to the header
  1348. primeOfferHeader.innerHTML = "";
  1349. primeOfferHeader.appendChild(script);
  1350.  
  1351. if (CONFIG.enableClaimAllButton || CONFIG.enableRemoveAllButton) {
  1352. primeOfferHeader.innerHTML += `
  1353. <div style="display: flex; gap: 10px; margin-bottom: 10px;">
  1354. ${CONFIG.enableClaimAllButton ? `
  1355. <input type='button' style='border: none; background-color: #9147ff; color: white; padding: 10px 20px; font-size: 14px; border-radius: 4px; cursor: pointer; flex: 1;'
  1356. class='tw-align-items-center tw-align-middle tw-border-bottom-left-radius-medium tw-border-bottom-right-radius-medium tw-border-top-left-radius-medium tw-border-top-right-radius-medium tw-core-button tw-core-button--primary tw-inline-flex tw-interactive tw-justify-content-center tw-overflow-hidden tw-relative'
  1357. value='Claim All'
  1358. onclick='openClaimGameTabs();'>
  1359. ` : ''}
  1360. ${CONFIG.enableRemoveAllButton ? `
  1361. <input type='button' style='border: none; background-color: #772ce8; color: white; padding: 10px 20px; font-size: 14px; border-radius: 4px; cursor: pointer; flex: 1;'
  1362. class='tw-align-items-center tw-align-middle tw-border-bottom-left-radius-medium tw-border-bottom-right-radius-medium tw-border-top-left-radius-medium tw-border-top-right-radius-medium tw-core-button tw-core-button--primary tw-inline-flex tw-interactive tw-justify-content-center tw-overflow-hidden tw-relative'
  1363. value='Remove All'
  1364. onclick='removeClaimedItems();'>
  1365. ` : ''}
  1366. </div>
  1367. `;
  1368. }
  1369. });
  1370.  
  1371. o.observe(document.body, { childList: true });
  1372. })();

QingJ © 2025

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