Greasy Fork镜像 还支持 简体中文。

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.5
  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/gallery-of-things-reveries-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或关注我们的公众号极客氢云获取最新地址