Lazy URL Loader

Opens 500+ URLs in batches of 50, automatically opening next URL when a tab closes

Size

17.3 KB

Version

1.1.9

Created

Feb 2, 2026

Updated

13 days ago

1// ==UserScript==
2// @name		Lazy URL Loader
3// @description		Opens 500+ URLs in batches of 50, automatically opening next URL when a tab closes
4// @version		1.1.9
5// @match		*://*/*
6// ==/UserScript==
7(function() {
8    'use strict';
9
10    console.log('Lazy URL Loader extension started on:', window.location.href);
11
12    // State management
13    let urlQueue = [];
14    let currentIndex = 0;
15    let BATCH_SIZE = 50;
16    const MAIN_PAGE_KEY = 'lazy-loader-main-page';
17    const OPENING_LOCK_KEY = 'lazy-loader-opening-lock';
18
19    // Create UI (on every page)
20    function createUI() {
21        // Check if UI already exists
22        if (document.getElementById('lazy-url-loader-container')) {
23            return;
24        }
25
26        const container = document.createElement('div');
27        container.id = 'lazy-url-loader-container';
28        container.style.cssText = `
29            position: fixed;
30            top: 20px;
31            right: 20px;
32            width: 350px;
33            background: white;
34            border: 2px solid #333;
35            border-radius: 8px;
36            padding: 15px;
37            box-shadow: 0 4px 12px rgba(0,0,0,0.3);
38            z-index: 999999;
39            font-family: Arial, sans-serif;
40        `;
41
42        container.innerHTML = `
43            <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
44                <h3 style="margin: 0; color: #333; font-size: 16px;">Lazy URL Loader</h3>
45                <button id="minimize-btn" style="
46                    padding: 5px 10px;
47                    background: #6c757d;
48                    color: white;
49                    border: none;
50                    border-radius: 4px;
51                    font-size: 12px;
52                    cursor: pointer;
53                "></button>
54            </div>
55            <div id="panel-content">
56                <textarea id="url-input" placeholder="Paste URLs here (one per line)..." style="
57                    width: 100%;
58                    height: 120px;
59                    padding: 8px;
60                    border: 1px solid #ccc;
61                    border-radius: 4px;
62                    font-size: 13px;
63                    resize: vertical;
64                    box-sizing: border-box;
65                    margin-bottom: 10px;
66                "></textarea>
67                <div style="margin-bottom: 10px;">
68                    <label style="font-size: 13px; color: #333; display: block; margin-bottom: 5px;">
69                        Batch Size (URLs to open):
70                    </label>
71                    <input id="batch-size-input" type="number" min="1" max="100" value="50" style="
72                        width: 100%;
73                        padding: 8px;
74                        border: 1px solid #ccc;
75                        border-radius: 4px;
76                        font-size: 13px;
77                        box-sizing: border-box;
78                    ">
79                </div>
80                <div style="display: flex; gap: 10px; align-items: center;">
81                    <button id="load-urls-btn" style="
82                        flex: 1;
83                        padding: 10px;
84                        background: #007bff;
85                        color: white;
86                        border: none;
87                        border-radius: 4px;
88                        font-size: 14px;
89                        font-weight: bold;
90                        cursor: pointer;
91                    ">Load URLs</button>
92                    <button id="close-panel-btn" style="
93                        padding: 10px 15px;
94                        background: #6c757d;
95                        color: white;
96                        border: none;
97                        border-radius: 4px;
98                        font-size: 13px;
99                        cursor: pointer;
100                    ">Close</button>
101                </div>
102                <div id="status-display" style="
103                    margin-top: 10px;
104                    padding: 8px;
105                    background: #f8f9fa;
106                    border-radius: 4px;
107                    font-size: 12px;
108                    color: #333;
109                    display: none;
110                "></div>
111            </div>
112        `;
113
114        document.body.appendChild(container);
115
116        // Event listeners
117        document.getElementById('load-urls-btn').addEventListener('click', handleLoadUrls);
118        document.getElementById('close-panel-btn').addEventListener('click', () => {
119            container.style.display = 'none';
120        });
121
122        document.getElementById('minimize-btn').addEventListener('click', () => {
123            const content = document.getElementById('panel-content');
124            const minimizeBtn = document.getElementById('minimize-btn');
125            if (content.style.display === 'none') {
126                content.style.display = 'block';
127                minimizeBtn.textContent = '−';
128            } else {
129                content.style.display = 'none';
130                minimizeBtn.textContent = '+';
131            }
132        });
133
134        // Allow Ctrl+Enter in textarea to submit
135        document.getElementById('url-input').addEventListener('keydown', (e) => {
136            if (e.ctrlKey && e.key === 'Enter') {
137                handleLoadUrls();
138            }
139        });
140
141        console.log('UI created successfully');
142    }
143
144    // Create "Next URL" button on opened tabs
145    async function createNextButton() {
146        // Check if button already exists
147        if (document.getElementById('lazy-loader-next-btn')) {
148            return;
149        }
150        
151        // FIXED: Show button on ALL tabs (no main page exclusion)
152        // This way it works even when you close the first tab
153
154        const button = document.createElement('button');
155        button.id = 'lazy-loader-next-btn';
156        button.textContent = '➡️ Next URL';
157        button.style.cssText = `
158            position: fixed;
159            bottom: 20px;
160            right: 20px;
161            padding: 15px 30px;
162            background: #28a745;
163            color: white;
164            border: none;
165            border-radius: 8px;
166            font-size: 18px;
167            font-weight: bold;
168            cursor: pointer;
169            z-index: 999999;
170            box-shadow: 0 4px 12px rgba(0,0,0,0.3);
171            transition: all 0.3s ease;
172        `;
173
174        button.addEventListener('mouseenter', () => {
175            button.style.background = '#218838';
176            button.style.transform = 'scale(1.05)';
177        });
178
179        button.addEventListener('mouseleave', () => {
180            button.style.background = '#28a745';
181            button.style.transform = 'scale(1)';
182        });
183
184        button.addEventListener('click', handleNextUrl);
185
186        document.body.appendChild(button);
187        console.log('Next URL button created');
188    }
189
190    // Parse URLs from textarea
191    function parseUrls(text) {
192        const lines = text.split('\n');
193        const urls = [];
194        
195        for (const line of lines) {
196            const trimmed = line.trim();
197            if (trimmed) {
198                // If URL doesn't start with http:// or https://, add https://
199                if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
200                    urls.push(trimmed);
201                } else {
202                    urls.push('https://' + trimmed);
203                }
204            }
205        }
206        
207        return urls;
208    }
209
210    // Handle load URLs button click
211    async function handleLoadUrls() {
212        const textarea = document.getElementById('url-input');
213        const statusDisplay = document.getElementById('status-display');
214        const batchSizeInput = document.getElementById('batch-size-input');
215        const inputText = textarea.value;
216
217        if (!inputText.trim()) {
218            alert('Please paste URLs first!');
219            return;
220        }
221
222        const urls = parseUrls(inputText);
223        
224        if (urls.length === 0) {
225            alert('No valid URLs found. Please paste at least one URL per line.');
226            return;
227        }
228
229        // Get batch size from input
230        const batchSizeValue = parseInt(batchSizeInput.value);
231        if (isNaN(batchSizeValue) || batchSizeValue < 1 || batchSizeValue > 100) {
232            alert('Please enter a valid batch size between 1 and 100.');
233            return;
234        }
235        BATCH_SIZE = batchSizeValue;
236
237        console.log(`Parsed ${urls.length} URLs with batch size ${BATCH_SIZE}`);
238
239        // RESET: Clear previous state completely
240        urlQueue = urls;
241        currentIndex = 0;
242
243        // Mark this page as the main control page
244        await GM.setValue(MAIN_PAGE_KEY, window.location.href);
245
246        // Save state (reset to 0)
247        await GM.setValue('urlQueue', JSON.stringify(urlQueue));
248        await GM.setValue('currentIndex', 0);
249        await GM.setValue(OPENING_LOCK_KEY, 0);
250
251        // Open first batch
252        const batchToOpen = Math.min(BATCH_SIZE, urlQueue.length);
253        console.log(`Opening first batch of ${batchToOpen} URLs`);
254
255        for (let i = 0; i < batchToOpen; i++) {
256            const url = urlQueue[i];
257            console.log(`Opening URL ${i + 1}: ${url}`);
258            GM.openInTab(url, true);
259            currentIndex++;
260        }
261
262        await GM.setValue('currentIndex', currentIndex);
263
264        // Update status
265        statusDisplay.style.display = 'block';
266        statusDisplay.innerHTML = `
267            <strong>Status:</strong> Opened ${batchToOpen} URLs<br>
268            <strong>Remaining:</strong> ${urlQueue.length - currentIndex} URLs<br>
269            <strong>Total:</strong> ${urlQueue.length} URLs<br>
270            <div style="margin-top: 8px; padding: 8px; background: #fff3cd; border-radius: 4px; border: 1px solid #ffc107; font-size: 11px;">
271                💡 <strong>Tip:</strong> Click "Next URL" button on each tab when done!
272            </div>
273        `;
274
275        console.log(`Opened ${batchToOpen} URLs. Remaining: ${urlQueue.length - currentIndex}`);
276
277        // Start monitoring for updates
278        startStatusMonitoring();
279    }
280
281    // Monitor status updates
282    async function startStatusMonitoring() {
283        console.log('Starting status monitoring...');
284        
285        setInterval(async () => {
286            const storedIndex = await GM.getValue('currentIndex', 0);
287            const storedQueue = JSON.parse(await GM.getValue('urlQueue', '[]'));
288            
289            if (storedQueue.length > 0) {
290                const statusDisplay = document.getElementById('status-display');
291                if (statusDisplay) {
292                    statusDisplay.style.display = 'block';
293                    const isComplete = storedIndex >= storedQueue.length;
294                    statusDisplay.innerHTML = `
295                        <strong>Status:</strong> ${isComplete ? '✅ Complete' : '🔄 Active'}<br>
296                        <strong>Opened:</strong> ${storedIndex} URLs<br>
297                        <strong>Remaining:</strong> ${Math.max(0, storedQueue.length - storedIndex)} URLs<br>
298                        <strong>Total:</strong> ${storedQueue.length} URLs
299                        ${!isComplete ? '<div style="margin-top: 8px; padding: 8px; background: #fff3cd; border-radius: 4px; border: 1px solid #ffc107; font-size: 11px;">💡 <strong>Tip:</strong> Click "Next URL" button on each tab when done!</div>' : ''}
300                    `;
301                }
302            }
303        }, 1000);
304    }
305
306    // Handle "Next URL" button click
307    async function handleNextUrl() {
308        console.log('Next URL button clicked');
309        
310        // Try to acquire lock
311        const lockTimestamp = await GM.getValue(OPENING_LOCK_KEY, 0);
312        const now = Date.now();
313        
314        // If lock is held and less than 2 seconds old, skip
315        if (lockTimestamp > 0 && (now - lockTimestamp) < 2000) {
316            console.log('Another tab is already opening a URL, please wait...');
317            alert('Please wait, another tab is opening...');
318            return;
319        }
320        
321        // Acquire lock
322        await GM.setValue(OPENING_LOCK_KEY, now);
323        
324        // Wait a bit to ensure no race condition
325        await new Promise(resolve => setTimeout(resolve, 100));
326        
327        // Check lock again
328        const currentLock = await GM.getValue(OPENING_LOCK_KEY, 0);
329        if (currentLock !== now) {
330            console.log('Lost lock race, another tab won');
331            alert('Another tab is opening a URL, please try again.');
332            // FIXED: Release lock before returning
333            await GM.setValue(OPENING_LOCK_KEY, 0);
334            return;
335        }
336        
337        const storedIndex = await GM.getValue('currentIndex', 0);
338        const storedQueue = JSON.parse(await GM.getValue('urlQueue', '[]'));
339        
340        if (storedIndex < storedQueue.length) {
341            const nextUrl = storedQueue[storedIndex];
342            console.log(`Opening next URL (${storedIndex + 1}/${storedQueue.length}): ${nextUrl}`);
343            
344            // Update index FIRST
345            await GM.setValue('currentIndex', storedIndex + 1);
346            
347            // FIXED: Release lock BEFORE opening tab/closing (async operations may fail)
348            await GM.setValue(OPENING_LOCK_KEY, 0);
349            
350            // Check if this was the last URL
351            if (storedIndex + 1 >= storedQueue.length) {
352                console.log('All URLs have been opened!');
353                alert('🎉 You finished all the URLs!');
354            }
355            
356            // FIXED: Open next URL in BACKGROUND at the end
357            // Use { active: false, insert: true } to ensure it opens at the end
358            GM.openInTab(nextUrl, { active: false, insert: false, setParent: false });
359            
360            // Close current tab after a small delay to ensure new tab opens first
361            // Browser will naturally move focus to the next tab in order
362            console.log('Closing current tab in 300ms...');
363            setTimeout(() => {
364                try {
365                    window.close();
366                } catch (e) {
367                    console.error('Failed to close tab:', e);
368                    // Fallback: try with GM_closeTab if available
369                    if (typeof GM_closeTab !== 'undefined') {
370                        GM_closeTab();
371                    } else {
372                        alert('⚠️ Could not auto-close tab. Please close manually and move to next tab.');
373                    }
374                }
375            }, 300);
376        } else {
377            console.log('All URLs have been opened');
378            // FIXED: Release lock before closing
379            await GM.setValue(OPENING_LOCK_KEY, 0);
380            alert('🎉 All URLs have been opened! You can close this tab.');
381            try {
382                window.close();
383            } catch (e) {
384                console.error('Failed to close tab:', e);
385            }
386        }
387    }
388
389    // Initialize with retry mechanism
390    async function init() {
391        console.log('Initializing Lazy URL Loader...');
392        
393        // Wait for body to be ready
394        if (!document.body) {
395            setTimeout(init, 100);
396            return;
397        }
398
399        const mainPageUrl = await GM.getValue(MAIN_PAGE_KEY, '');
400        
401        // Always create UI on every page
402        createUI();
403        
404        // Create "Next URL" button on opened tabs
405        await createNextButton();
406        
407        // Restore state if exists
408        const storedQueue = await GM.getValue('urlQueue', '[]');
409        const parsedQueue = JSON.parse(storedQueue);
410        
411        if (parsedQueue.length > 0) {
412            urlQueue = parsedQueue;
413            currentIndex = await GM.getValue('currentIndex', 0);
414            
415            const statusDisplay = document.getElementById('status-display');
416            if (statusDisplay && currentIndex < urlQueue.length) {
417                statusDisplay.style.display = 'block';
418                statusDisplay.innerHTML = `
419                    <strong>Status:</strong> Resumed<br>
420                    <strong>Opened:</strong> ${currentIndex} URLs<br>
421                    <strong>Remaining:</strong> ${urlQueue.length - currentIndex} URLs<br>
422                    <strong>Total:</strong> ${urlQueue.length} URLs
423                    <div style="margin-top: 8px; padding: 8px; background: #fff3cd; border-radius: 4px; border: 1px solid #ffc107; font-size: 11px;">
424                        💡 <strong>Tip:</strong> Click "Next URL" button on each tab when done!
425                    </div>
426                `;
427                
428                startStatusMonitoring();
429            }
430        }
431        
432        console.log('Lazy URL Loader initialized');
433    }
434
435    // Start initialization immediately and on DOM ready
436    if (document.readyState === 'loading') {
437        document.addEventListener('DOMContentLoaded', init);
438    } else {
439        init();
440    }
441    
442    // FIXED: Also retry after page fully loads (for slow-loading pages)
443    window.addEventListener('load', () => {
444        setTimeout(() => {
445            // Re-create UI if it doesn't exist
446            if (!document.getElementById('lazy-url-loader-container')) {
447                console.log('UI not found after load, retrying...');
448                createUI();
449            }
450            // Re-create button if it doesn't exist
451            if (!document.getElementById('lazy-loader-next-btn')) {
452                console.log('Button not found after load, retrying...');
453                createNextButton();
454            }
455        }, 500);
456    });
457})();
Lazy URL Loader | Robomonkey