Size
10.3 KB
Version
1.1.9
Created
Feb 6, 2026
Updated
about 1 month ago
1// ==UserScript==
2// @name IndieGala Bulk Key Redeemer
3// @description Adds a button to reveal and copy all game keys at once
4// @version 1.1.9
5// @match https://*.indiegala.com/*
6// ==/UserScript==
7(function() {
8 'use strict';
9
10 console.log('IndieGala Bulk Key Redeemer loaded');
11
12 // Debounce function to prevent excessive calls
13 function debounce(func, wait) {
14 let timeout;
15 return function executedFunction(...args) {
16 const later = () => {
17 clearTimeout(timeout);
18 func(...args);
19 };
20 clearTimeout(timeout);
21 timeout = setTimeout(later, wait);
22 };
23 }
24
25 // Function to create the redeem all button
26 function createRedeemAllButton() {
27 // Check if button already exists
28 if (document.querySelector('#redeem-all-keys-btn')) {
29 console.log('Redeem all button already exists');
30 return;
31 }
32
33 // Find a suitable container for the button
34 const container = document.querySelector('.profile-private-page-library') ||
35 document.querySelector('.profile-private-page-cont') ||
36 document.querySelector('.library-showcase-content') ||
37 document.querySelector('body');
38
39 if (!container) {
40 console.log('Container not found, will retry');
41 return;
42 }
43
44 // Create button
45 const button = document.createElement('button');
46 button.id = 'redeem-all-keys-btn';
47 button.textContent = 'Reveal All Keys';
48 button.style.cssText = `
49 position: fixed;
50 top: 20px;
51 right: 20px;
52 z-index: 10000;
53 padding: 12px 24px;
54 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
55 color: white;
56 border: none;
57 border-radius: 8px;
58 font-size: 16px;
59 font-weight: bold;
60 cursor: pointer;
61 box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
62 transition: all 0.3s ease;
63 `;
64
65 // Add hover effect
66 button.addEventListener('mouseenter', () => {
67 button.style.transform = 'translateY(-2px)';
68 button.style.boxShadow = '0 6px 20px rgba(0, 0, 0, 0.3)';
69 });
70
71 button.addEventListener('mouseleave', () => {
72 button.style.transform = 'translateY(0)';
73 button.style.boxShadow = '0 4px 15px rgba(0, 0, 0, 0.2)';
74 });
75
76 // Add click handler
77 button.addEventListener('click', revealAllKeys);
78
79 document.body.appendChild(button);
80 console.log('Redeem all button created');
81 }
82
83 // Function to reveal all keys
84 async function revealAllKeys() {
85 const button = document.querySelector('#redeem-all-keys-btn');
86 const originalText = button.textContent;
87
88 button.disabled = true;
89 button.textContent = 'Processing...';
90 button.style.opacity = '0.7';
91
92 try {
93 // Find all "Click to redeem key" buttons - only the ones that are links (not buttons in dialogs)
94 // Filter to only get the unrevealed ones (those in profile-private-page-library-key-redeem containers)
95 const revealButtons = Array.from(document.querySelectorAll('a.profile-private-page-library-get-serial-btn')).filter(btn => {
96 const parent = btn.closest('.profile-private-page-library-key');
97 return parent && parent.classList.contains('profile-private-page-library-key-redeem');
98 });
99
100 console.log(`Found ${revealButtons.length} reveal buttons`);
101
102 if (revealButtons.length === 0) {
103 alert('No keys found to reveal. Make sure you are on the library page with unrevealed keys.');
104 button.textContent = originalText;
105 button.disabled = false;
106 button.style.opacity = '1';
107 return;
108 }
109
110 let revealedCount = 0;
111 const allKeys = [];
112
113 // Click each reveal button with a delay
114 for (let i = 0; i < revealButtons.length; i++) {
115 const btn = revealButtons[i];
116
117 // Update button text with progress
118 button.textContent = `Revealing ${i + 1}/${revealButtons.length}...`;
119
120 console.log(`Clicking button ${i + 1}`);
121
122 // Click the button to open the dialog
123 btn.click();
124
125 // Wait for dialog to appear
126 await new Promise(resolve => setTimeout(resolve, 1000));
127
128 // Find and click the Proceed button in the dialog
129 const proceedButton = document.querySelector('button.profile-private-page-library-get-serial-btn.bg-gradient-blue');
130
131 if (proceedButton) {
132 console.log('Clicking Proceed button');
133 proceedButton.click();
134 } else {
135 console.log('Proceed button not found');
136 }
137
138 revealedCount++;
139
140 // Wait much longer for the key to be revealed (server request + DOM update)
141 await new Promise(resolve => setTimeout(resolve, 4000));
142
143 // Find the parent container
144 const parentContainer = btn.closest('.profile-private-page-library-subitem');
145
146 if (parentContainer) {
147 // Look for the input field with the key
148 const keyElement = parentContainer.querySelector('input.profile-private-page-library-key-serial');
149
150 if (keyElement) {
151 const key = keyElement.value?.trim();
152
153 console.log(`Key element found for button ${i + 1}, value: "${key}"`);
154
155 // Validate that it looks like a game key
156 if (key && key.length > 5) {
157 allKeys.push(key);
158 console.log(`Found key ${i + 1}: ${key}`);
159 } else {
160 console.log(`Key found but invalid or empty: "${key}"`);
161 }
162 } else {
163 console.log(`No key input found for button ${i + 1}`);
164 }
165 } else {
166 console.log(`No parent container found for button ${i + 1}`);
167 }
168 }
169
170 console.log(`Revealed ${revealedCount} keys, collected ${allKeys.length} keys`);
171
172 // Copy all keys to clipboard if any were found
173 if (allKeys.length > 0) {
174 const keysText = allKeys.join('\n');
175 await GM.setClipboard(keysText);
176
177 button.textContent = `✓ ${allKeys.length} Keys Copied!`;
178 button.style.background = 'linear-gradient(135deg, #11998e 0%, #38ef7d 100%)';
179
180 // Show success message
181 showNotification(`Successfully revealed and copied ${allKeys.length} keys to clipboard!`, 'success');
182 } else {
183 button.textContent = `✓ ${revealedCount} Keys Revealed`;
184 showNotification(`Revealed ${revealedCount} keys. Keys may already be visible on the page.`, 'info');
185 }
186
187 // Reset button after 3 seconds
188 setTimeout(() => {
189 button.textContent = originalText;
190 button.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';
191 button.disabled = false;
192 button.style.opacity = '1';
193 }, 3000);
194
195 } catch (error) {
196 console.error('Error revealing keys:', error);
197 showNotification('Error revealing keys. Please try again.', 'error');
198
199 button.textContent = originalText;
200 button.disabled = false;
201 button.style.opacity = '1';
202 }
203 }
204
205 // Function to show notification
206 function showNotification(message, type = 'info') {
207 const notification = document.createElement('div');
208 notification.style.cssText = `
209 position: fixed;
210 top: 80px;
211 right: 20px;
212 z-index: 10001;
213 padding: 16px 24px;
214 background: ${type === 'success' ? '#38ef7d' : type === 'error' ? '#ff6b6b' : '#667eea'};
215 color: white;
216 border-radius: 8px;
217 font-size: 14px;
218 font-weight: 500;
219 box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
220 animation: slideIn 0.3s ease;
221 max-width: 300px;
222 `;
223 notification.textContent = message;
224
225 // Add animation
226 const style = document.createElement('style');
227 style.textContent = `
228 @keyframes slideIn {
229 from {
230 transform: translateX(400px);
231 opacity: 0;
232 }
233 to {
234 transform: translateX(0);
235 opacity: 1;
236 }
237 }
238 `;
239 document.head.appendChild(style);
240
241 document.body.appendChild(notification);
242
243 // Remove after 5 seconds
244 setTimeout(() => {
245 notification.style.animation = 'slideIn 0.3s ease reverse';
246 setTimeout(() => notification.remove(), 300);
247 }, 5000);
248 }
249
250 // Initialize the extension
251 function init() {
252 console.log('Initializing IndieGala Bulk Key Redeemer');
253
254 // Wait for page to be ready
255 if (document.readyState === 'loading') {
256 document.addEventListener('DOMContentLoaded', createRedeemAllButton);
257 } else {
258 createRedeemAllButton();
259 }
260
261 // Also try after a delay in case content loads dynamically
262 setTimeout(createRedeemAllButton, 2000);
263 setTimeout(createRedeemAllButton, 5000);
264
265 // Watch for dynamic content changes
266 const observer = new MutationObserver(debounce(() => {
267 createRedeemAllButton();
268 }, 1000));
269
270 observer.observe(document.body, {
271 childList: true,
272 subtree: true
273 });
274 }
275
276 // Start the extension
277 init();
278})();