IndieGala Bulk Key Redeemer

Adds a button to reveal and copy all game keys at once

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})();