Humble Bundle Key Revealer

Reveals all game keys on Humble Bundle download pages with one click

Size

14.6 KB

Version

1.1.10

Created

Dec 8, 2025

Updated

about 1 month ago

1// ==UserScript==
2// @name		Humble Bundle Key Revealer
3// @description		Reveals all game keys on Humble Bundle download pages with one click
4// @version		1.1.10
5// @match		https://*.humblebundle.com/*
6// @icon		https://cdn.humblebundle.com/static/hashed/47e474eed38083df699b7dfd8d29d575e3398f1e.ico
7// @grant		GM.setClipboard
8// ==/UserScript==
9(function() {
10    'use strict';
11
12    console.log('Humble Bundle Key Revealer extension loaded');
13
14    function createRevealAllButton() {
15        // Check if we're on a downloads page with keys
16        const keyFields = document.querySelectorAll('.js-keyfield.keyfield.enabled');
17        
18        if (keyFields.length === 0) {
19            console.log('No keys found on this page');
20            return;
21        }
22
23        console.log(`Found ${keyFields.length} keys to reveal`);
24
25        // Find the key container to add our button
26        const keyContainer = document.querySelector('.key-container.wrapper');
27        
28        if (!keyContainer) {
29            console.log('Key container not found');
30            return;
31        }
32
33        // Check if button already exists
34        if (document.getElementById('reveal-all-keys-btn')) {
35            console.log('Button already exists');
36            return;
37        }
38
39        // Create the reveal all button
40        const revealButton = document.createElement('button');
41        revealButton.id = 'reveal-all-keys-btn';
42        revealButton.textContent = '🔑 Reveal All Keys';
43        revealButton.style.cssText = `
44            background: linear-gradient(135deg, #cc2929 0%, #a02020 100%);
45            color: white;
46            border: none;
47            padding: 12px 24px;
48            font-size: 16px;
49            font-weight: bold;
50            border-radius: 4px;
51            cursor: pointer;
52            margin-bottom: 20px;
53            box-shadow: 0 2px 4px rgba(0,0,0,0.2);
54            transition: all 0.3s ease;
55            display: inline-block;
56        `;
57
58        // Add hover effect
59        revealButton.addEventListener('mouseenter', () => {
60            revealButton.style.transform = 'translateY(-2px)';
61            revealButton.style.boxShadow = '0 4px 8px rgba(0,0,0,0.3)';
62        });
63
64        revealButton.addEventListener('mouseleave', () => {
65            revealButton.style.transform = 'translateY(0)';
66            revealButton.style.boxShadow = '0 2px 4px rgba(0,0,0,0.2)';
67        });
68
69        // Add click handler to reveal all keys
70        revealButton.addEventListener('click', () => {
71            console.log('Revealing all keys...');
72            
73            const keyFieldsToReveal = document.querySelectorAll('.js-keyfield.keyfield.enabled');
74            let revealedCount = 0;
75            const gameKeysData = [];
76
77            keyFieldsToReveal.forEach((keyField, index) => {
78                // Check if key is already revealed (has class 'redeemed')
79                const isAlreadyRevealed = keyField.classList.contains('redeemed');
80                
81                if (isAlreadyRevealed) {
82                    // Key is already revealed, extract it immediately
83                    const keyRedeemer = keyField.closest('.key-redeemer') || keyField.closest('.content-choice');
84                    let gameName = 'Unknown Game';
85                    
86                    if (keyRedeemer) {
87                        // Try different selectors for game name
88                        const nameElement = keyRedeemer.querySelector('.heading-text h4') || 
89                                          keyRedeemer.querySelector('.content-choice-title');
90                        gameName = nameElement?.textContent.trim() || 'Unknown Game';
91                    }
92                    
93                    const keyValue = keyField.querySelector('.keyfield-value')?.textContent.trim() || 'Key not found';
94                    const cleanGameName = gameName.replace(/Look up on Steam/g, '').trim();
95                    
96                    gameKeysData.push({ name: cleanGameName, key: keyValue });
97                    console.log(`Collected (already revealed): ${cleanGameName} - ${keyValue}`);
98                    revealedCount++;
99                    
100                    // Check if all keys are collected
101                    if (revealedCount === keyFieldsToReveal.length) {
102                        copyToClipboard(gameKeysData, revealButton);
103                    }
104                } else {
105                    // Key needs to be revealed first
106                    setTimeout(() => {
107                        keyField.click();
108                        revealedCount++;
109                        console.log(`Revealed key ${revealedCount}/${keyFieldsToReveal.length}`);
110                        
111                        // Wait a bit for the key to be revealed, then extract it
112                        setTimeout(() => {
113                            // Find the game name from the heading
114                            const keyRedeemer = keyField.closest('.key-redeemer');
115                            const gameName = keyRedeemer ? keyRedeemer.querySelector('.heading-text h4')?.textContent.trim() : 'Unknown Game';
116                            
117                            // Find the revealed key value - wait for it to change from "Reveal your Steam key"
118                            const keyValueElement = keyField.querySelector('.keyfield-value');
119                            let keyValue = keyValueElement?.textContent.trim() || 'Key not found';
120                            
121                            // Clean up the game name (remove Steam icon text if present)
122                            const cleanGameName = gameName.replace(/Look up on Steam/g, '').trim();
123                            
124                            // Store the game name and key
125                            gameKeysData.push({ name: cleanGameName, key: keyValue });
126                            console.log(`Collected: ${cleanGameName} - ${keyValue}`);
127                            
128                            // If all keys are collected, copy to clipboard
129                            if (gameKeysData.length === keyFieldsToReveal.length) {
130                                copyToClipboard(gameKeysData, revealButton);
131                            }
132                        }, 1000); // Wait 1000ms for key to be revealed
133                        
134                        // Update button text with progress
135                        if (revealedCount < keyFieldsToReveal.length) {
136                            revealButton.textContent = `Revealing... (${revealedCount}/${keyFieldsToReveal.length})`;
137                        }
138                    }, index * 200); // 200ms delay between each click
139                }
140            });
141        });
142
143        // Insert the button at the top of the key container
144        keyContainer.insertBefore(revealButton, keyContainer.firstChild);
145        console.log('Reveal All Keys button added successfully');
146    }
147    
148    function createRevealAllButtonForChoicePage() {
149        // Check if we're on the Humble Choice page with game tiles
150        const gameTiles = document.querySelectorAll('.content-choice.claimed');
151        
152        if (gameTiles.length === 0) {
153            console.log('No game tiles found on this page');
154            return;
155        }
156
157        console.log(`Found ${gameTiles.length} game tiles on Choice page`);
158
159        // Find the content choices wrapper to add our button
160        const choicesWrapper = document.querySelector('.content-choices-wrapper');
161        
162        if (!choicesWrapper) {
163            console.log('Content choices wrapper not found');
164            return;
165        }
166
167        // Check if button already exists
168        if (document.getElementById('reveal-all-keys-btn')) {
169            console.log('Button already exists');
170            return;
171        }
172
173        // Create the reveal all button
174        const revealButton = document.createElement('button');
175        revealButton.id = 'reveal-all-keys-btn';
176        revealButton.textContent = '🔑 Copy All Keys';
177        revealButton.style.cssText = `
178            background: linear-gradient(135deg, #cc2929 0%, #a02020 100%);
179            color: white;
180            border: none;
181            padding: 12px 24px;
182            font-size: 16px;
183            font-weight: bold;
184            border-radius: 4px;
185            cursor: pointer;
186            margin: 20px;
187            box-shadow: 0 2px 4px rgba(0,0,0,0.2);
188            transition: all 0.3s ease;
189            display: inline-block;
190            position: relative;
191            z-index: 1000;
192        `;
193
194        // Add hover effect
195        revealButton.addEventListener('mouseenter', () => {
196            revealButton.style.transform = 'translateY(-2px)';
197            revealButton.style.boxShadow = '0 4px 8px rgba(0,0,0,0.3)';
198        });
199
200        revealButton.addEventListener('mouseleave', () => {
201            revealButton.style.transform = 'translateY(0)';
202            revealButton.style.boxShadow = '0 2px 4px rgba(0,0,0,0.2)';
203        });
204
205        // Add click handler to collect all keys by clicking each game
206        revealButton.addEventListener('click', async () => {
207            console.log('Collecting all keys from Choice page by clicking each game...');
208            
209            const gameTilesToClick = document.querySelectorAll('.content-choice.claimed .js-open-choice-modal');
210            const gameKeysData = [];
211            
212            console.log(`Found ${gameTilesToClick.length} games to click`);
213            
214            if (gameTilesToClick.length === 0) {
215                revealButton.textContent = '❌ No games found';
216                setTimeout(() => {
217                    revealButton.textContent = '🔑 Copy All Keys';
218                }, 2000);
219                return;
220            }
221            
222            // Click each game tile one by one
223            for (let i = 0; i < gameTilesToClick.length; i++) {
224                const tile = gameTilesToClick[i];
225                revealButton.textContent = `Processing... (${i + 1}/${gameTilesToClick.length})`;
226                
227                // Click the game tile to open its modal
228                tile.click();
229                console.log(`Clicked game ${i + 1}`);
230                
231                // Wait for modal to appear and key to load
232                await new Promise(resolve => setTimeout(resolve, 2500));
233                
234                // Find the modal and extract the key
235                const modal = document.querySelector('#site-modal');
236                if (modal) {
237                    const gameName = modal.querySelector('.game-name, h2, .modal-title')?.textContent.trim() || 
238                                   tile.querySelector('.content-choice-title')?.textContent.trim() || 
239                                   'Unknown Game';
240                    
241                    const keyField = modal.querySelector('.js-keyfield.keyfield.redeemed .keyfield-value');
242                    const keyValue = keyField?.textContent.trim() || 'Key not found';
243                    
244                    const cleanGameName = gameName.replace(/Look up on Steam/g, '').trim();
245                    gameKeysData.push({ name: cleanGameName, key: keyValue });
246                    console.log(`Collected: ${cleanGameName} - ${keyValue}`);
247                    
248                    // Close the modal by clicking outside or close button
249                    const closeButton = modal.querySelector('.js-close-modal, .close-button, [aria-label="Close"]');
250                    if (closeButton) {
251                        closeButton.click();
252                    } else {
253                        // Click on the modal backdrop to close
254                        const backdrop = modal.querySelector('.humblemodal-wrapper');
255                        if (backdrop) {
256                            backdrop.click();
257                        }
258                    }
259                    
260                    // Wait for modal to close
261                    await new Promise(resolve => setTimeout(resolve, 800));
262                }
263            }
264            
265            // All games processed, now copy to clipboard
266            if (gameKeysData.length > 0) {
267                console.log(`Finished collecting ${gameKeysData.length} keys, calling copyToClipboard...`);
268                copyToClipboard(gameKeysData, revealButton);
269            } else {
270                console.log('No keys found');
271                revealButton.textContent = '❌ No keys found';
272                setTimeout(() => {
273                    revealButton.textContent = '🔑 Copy All Keys';
274                }, 2000);
275            }
276        });
277
278        // Insert the button at the top of the choices wrapper
279        choicesWrapper.insertBefore(revealButton, choicesWrapper.firstChild);
280        console.log('Copy All Keys button added successfully to Choice page');
281    }
282    
283    function copyToClipboard(gameKeysData, revealButton) {
284        const formattedText = gameKeysData.map(item => `${item.name} - ${item.key}`).join('\n');
285        
286        // Copy to clipboard
287        GM.setClipboard(formattedText).then(() => {
288            console.log('All keys copied to clipboard!');
289            revealButton.textContent = '✅ Keys Copied to Clipboard!';
290            revealButton.style.background = 'linear-gradient(135deg, #28a745 0%, #1e7e34 100%)';
291            
292            // Reset button after 3 seconds
293            setTimeout(() => {
294                revealButton.textContent = '🔑 Copy All Keys';
295                revealButton.style.background = 'linear-gradient(135deg, #cc2929 0%, #a02020 100%)';
296            }, 3000);
297        }).catch(err => {
298            console.error('Failed to copy to clipboard:', err);
299            revealButton.textContent = '✅ All Keys Revealed!';
300            revealButton.style.background = 'linear-gradient(135deg, #28a745 0%, #1e7e34 100%)';
301            
302            // Reset button after 3 seconds
303            setTimeout(() => {
304                revealButton.textContent = '🔑 Copy All Keys';
305                revealButton.style.background = 'linear-gradient(135deg, #cc2929 0%, #a02020 100%)';
306            }, 3000);
307        });
308    }
309
310    // Wait for the page to load
311    function init() {
312        // Try to create the button immediately
313        createRevealAllButton();
314        createRevealAllButtonForChoicePage();
315
316        // Also observe for dynamic content loading
317        const observer = new MutationObserver(() => {
318            createRevealAllButton();
319            createRevealAllButtonForChoicePage();
320        });
321
322        // Observe the body for changes
323        if (document.body) {
324            observer.observe(document.body, {
325                childList: true,
326                subtree: true
327            });
328        }
329    }
330
331    // Start when DOM is ready
332    if (document.readyState === 'loading') {
333        document.addEventListener('DOMContentLoaded', init);
334    } else {
335        init();
336    }
337
338})();