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