Professional media downloader with side panel interface. Download images and videos from any website at maximum speed with original filenames.
Size
39.9 KB
Version
1.0.1
Created
Feb 4, 2026
Updated
14 days ago
1// ==UserScript==
2// @name MediaGrab Pro - Universal Media Downloader
3// @description Professional media downloader with side panel interface. Download images and videos from any website at maximum speed with original filenames.
4// @version 1.0.1
5// @match https://*/*
6// @match http://*/*
7// @icon https://cococut.net/favicon.ico
8// @grant GM.getValue
9// @grant GM.setValue
10// @grant GM.xmlhttpRequest
11// @grant GM.openInTab
12// @grant GM.listValues
13// @grant GM.deleteValue
14// @connect *
15// ==/UserScript==
16(function() {
17 'use strict';
18
19 // ============================================
20 // UTILITY FUNCTIONS
21 // ============================================
22
23 function debounce(func, wait) {
24 let timeout;
25 return function executedFunction(...args) {
26 const later = () => {
27 clearTimeout(timeout);
28 func(...args);
29 };
30 clearTimeout(timeout);
31 timeout = setTimeout(later, wait);
32 };
33 }
34
35 function sanitizeFilename(filename) {
36 return filename.replace(/[^a-z0-9_\-\.]/gi, '_').substring(0, 200);
37 }
38
39 function getFileExtension(url, mimeType) {
40 // Try to get extension from URL
41 const urlMatch = url.match(/\.([a-z0-9]+)(?:\?|#|$)/i);
42 if (urlMatch) return urlMatch[1];
43
44 // Fallback to mime type
45 const mimeMap = {
46 'image/jpeg': 'jpg',
47 'image/png': 'png',
48 'image/gif': 'gif',
49 'image/webp': 'webp',
50 'image/svg+xml': 'svg',
51 'video/mp4': 'mp4',
52 'video/webm': 'webm',
53 'video/ogg': 'ogv',
54 'video/quicktime': 'mov'
55 };
56 return mimeMap[mimeType] || 'bin';
57 }
58
59 function formatFileSize(bytes) {
60 if (bytes === 0) return '0 B';
61 const k = 1024;
62 const sizes = ['B', 'KB', 'MB', 'GB'];
63 const i = Math.floor(Math.log(bytes) / Math.log(k));
64 return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
65 }
66
67 // ============================================
68 // MEDIA DETECTION
69 // ============================================
70
71 class MediaDetector {
72 constructor() {
73 this.detectedMedia = {
74 images: new Map(),
75 videos: new Map()
76 };
77 this.observer = null;
78 }
79
80 init() {
81 console.log('MediaGrab Pro: Initializing media detector');
82 this.scanPage();
83 this.setupObserver();
84 this.interceptNetworkRequests();
85 }
86
87 scanPage() {
88 // Scan for images
89 const images = document.querySelectorAll('img[src], img[data-src], picture source, [style*="background-image"]');
90 images.forEach(img => this.processImage(img));
91
92 // Scan for videos
93 const videos = document.querySelectorAll('video[src], video source, [data-video-src]');
94 videos.forEach(video => this.processVideo(video));
95
96 console.log(`MediaGrab Pro: Found ${this.detectedMedia.images.size} images and ${this.detectedMedia.videos.size} videos`);
97 }
98
99 processImage(element) {
100 let src = element.src || element.dataset.src || element.getAttribute('data-lazy-src');
101
102 // Check for background images
103 if (!src && element.style.backgroundImage) {
104 const match = element.style.backgroundImage.match(/url\(['"]?([^'"]+)['"]?\)/);
105 if (match) src = match[1];
106 }
107
108 if (src && src.startsWith('http') && !src.includes('data:image')) {
109 const url = new URL(src, window.location.href).href;
110 if (!this.detectedMedia.images.has(url)) {
111 this.detectedMedia.images.set(url, {
112 url: url,
113 type: 'image',
114 element: element,
115 filename: this.extractFilename(url, 'image'),
116 timestamp: Date.now()
117 });
118 }
119 }
120 }
121
122 processVideo(element) {
123 let src = element.src || element.dataset.src || element.dataset.videoSrc;
124
125 // Check for source elements
126 if (!src && element.tagName === 'SOURCE') {
127 src = element.src;
128 }
129
130 // Check parent video element
131 if (!src && element.parentElement && element.parentElement.tagName === 'VIDEO') {
132 src = element.parentElement.src;
133 }
134
135 if (src && src.startsWith('http')) {
136 const url = new URL(src, window.location.href).href;
137 if (!this.detectedMedia.videos.has(url)) {
138 this.detectedMedia.videos.set(url, {
139 url: url,
140 type: 'video',
141 element: element,
142 filename: this.extractFilename(url, 'video'),
143 timestamp: Date.now()
144 });
145 }
146 }
147 }
148
149 extractFilename(url, type) {
150 try {
151 // Try to get filename from URL
152 const urlObj = new URL(url);
153 let filename = urlObj.pathname.split('/').pop();
154
155 // If no filename, try to get from page title
156 if (!filename || filename.length < 3) {
157 const pageTitle = document.title.substring(0, 50);
158 const timestamp = Date.now();
159 filename = `${sanitizeFilename(pageTitle)}_${timestamp}`;
160 }
161
162 // Ensure extension
163 if (!filename.match(/\.[a-z0-9]+$/i)) {
164 filename += type === 'video' ? '.mp4' : '.jpg';
165 }
166
167 return sanitizeFilename(filename);
168 } catch (e) {
169 return `media_${Date.now()}.${type === 'video' ? 'mp4' : 'jpg'}`;
170 }
171 }
172
173 setupObserver() {
174 const debouncedScan = debounce(() => this.scanPage(), 1000);
175
176 this.observer = new MutationObserver(debouncedScan);
177 this.observer.observe(document.body, {
178 childList: true,
179 subtree: true,
180 attributes: true,
181 attributeFilter: ['src', 'data-src', 'style']
182 });
183 }
184
185 interceptNetworkRequests() {
186 // Intercept fetch requests
187 const originalFetch = window.fetch;
188 window.fetch = async (...args) => {
189 const response = await originalFetch(...args);
190 const url = typeof args[0] === 'string' ? args[0] : args[0].url;
191
192 if (response.ok) {
193 const contentType = response.headers.get('content-type');
194 if (contentType) {
195 if (contentType.includes('image/')) {
196 this.detectedMedia.images.set(url, {
197 url: url,
198 type: 'image',
199 filename: this.extractFilename(url, 'image'),
200 timestamp: Date.now()
201 });
202 } else if (contentType.includes('video/')) {
203 this.detectedMedia.videos.set(url, {
204 url: url,
205 type: 'video',
206 filename: this.extractFilename(url, 'video'),
207 timestamp: Date.now()
208 });
209 }
210 }
211 }
212
213 return response;
214 };
215 }
216
217 getDetectedMedia() {
218 return {
219 images: Array.from(this.detectedMedia.images.values()),
220 videos: Array.from(this.detectedMedia.videos.values())
221 };
222 }
223 }
224
225 // ============================================
226 // DOWNLOAD MANAGER
227 // ============================================
228
229 class DownloadManager {
230 constructor() {
231 this.downloads = new Map();
232 this.activeDownloads = 0;
233 this.maxConcurrent = 10; // Maximum concurrent downloads for speed
234 }
235
236 async download(mediaItem) {
237 const downloadId = `download_${Date.now()}_${Math.random()}`;
238
239 const downloadInfo = {
240 id: downloadId,
241 url: mediaItem.url,
242 filename: mediaItem.filename,
243 status: 'pending',
244 progress: 0,
245 size: 0,
246 downloadedSize: 0,
247 startTime: Date.now(),
248 error: null
249 };
250
251 this.downloads.set(downloadId, downloadInfo);
252 this.notifyUpdate(downloadId);
253
254 try {
255 await this.performDownload(downloadId, mediaItem);
256 } catch (error) {
257 console.error('MediaGrab Pro: Download failed', error);
258 downloadInfo.status = 'failed';
259 downloadInfo.error = error.message;
260 this.notifyUpdate(downloadId);
261 }
262
263 return downloadId;
264 }
265
266 async performDownload(downloadId, mediaItem) {
267 const downloadInfo = this.downloads.get(downloadId);
268 downloadInfo.status = 'downloading';
269 this.activeDownloads++;
270 this.notifyUpdate(downloadId);
271
272 return new Promise((resolve, reject) => {
273 GM.xmlhttpRequest({
274 method: 'GET',
275 url: mediaItem.url,
276 responseType: 'blob',
277 onprogress: (progress) => {
278 if (progress.lengthComputable) {
279 downloadInfo.progress = (progress.loaded / progress.total) * 100;
280 downloadInfo.size = progress.total;
281 downloadInfo.downloadedSize = progress.loaded;
282 this.notifyUpdate(downloadId);
283 }
284 },
285 onload: (response) => {
286 try {
287 const blob = response.response;
288 const url = URL.createObjectURL(blob);
289
290 // Create download link
291 const a = document.createElement('a');
292 a.href = url;
293 a.download = mediaItem.filename;
294 document.body.appendChild(a);
295 a.click();
296 document.body.removeChild(a);
297
298 // Cleanup
299 setTimeout(() => URL.revokeObjectURL(url), 100);
300
301 downloadInfo.status = 'completed';
302 downloadInfo.progress = 100;
303 this.activeDownloads--;
304 this.notifyUpdate(downloadId);
305
306 console.log(`MediaGrab Pro: Downloaded ${mediaItem.filename}`);
307 resolve();
308 } catch (error) {
309 reject(error);
310 }
311 },
312 onerror: (error) => {
313 downloadInfo.status = 'failed';
314 downloadInfo.error = 'Network error';
315 this.activeDownloads--;
316 this.notifyUpdate(downloadId);
317 reject(new Error('Network error'));
318 },
319 ontimeout: () => {
320 downloadInfo.status = 'failed';
321 downloadInfo.error = 'Timeout';
322 this.activeDownloads--;
323 this.notifyUpdate(downloadId);
324 reject(new Error('Timeout'));
325 }
326 });
327 });
328 }
329
330 async downloadBatch(mediaItems) {
331 console.log(`MediaGrab Pro: Starting batch download of ${mediaItems.length} items`);
332 const downloadPromises = mediaItems.map(item => this.download(item));
333 return Promise.allSettled(downloadPromises);
334 }
335
336 notifyUpdate(downloadId) {
337 const event = new CustomEvent('mediagrab-download-update', {
338 detail: {
339 downloadId: downloadId,
340 download: this.downloads.get(downloadId)
341 }
342 });
343 window.dispatchEvent(event);
344 }
345
346 getDownloads() {
347 return Array.from(this.downloads.values());
348 }
349
350 clearCompleted() {
351 for (const [id, download] of this.downloads.entries()) {
352 if (download.status === 'completed') {
353 this.downloads.delete(id);
354 }
355 }
356 }
357 }
358
359 // ============================================
360 // SIDE PANEL UI
361 // ============================================
362
363 class SidePanelUI {
364 constructor(mediaDetector, downloadManager) {
365 this.mediaDetector = mediaDetector;
366 this.downloadManager = downloadManager;
367 this.isOpen = false;
368 this.currentTab = 'images';
369 this.panel = null;
370 }
371
372 init() {
373 this.createToggleButton();
374 this.createPanel();
375 this.setupEventListeners();
376 console.log('MediaGrab Pro: Side panel UI initialized');
377 }
378
379 createToggleButton() {
380 const button = document.createElement('button');
381 button.id = 'mediagrab-toggle-btn';
382 button.innerHTML = '📥';
383 button.title = 'MediaGrab Pro';
384
385 const style = document.createElement('style');
386 style.textContent = `
387 #mediagrab-toggle-btn {
388 position: fixed;
389 top: 50%;
390 right: 0;
391 transform: translateY(-50%);
392 width: 50px;
393 height: 50px;
394 background: linear-gradient(135deg, #9b59b6 0%, #e91e63 100%);
395 border: none;
396 border-radius: 10px 0 0 10px;
397 color: white;
398 font-size: 24px;
399 cursor: pointer;
400 z-index: 999999;
401 box-shadow: -2px 2px 10px rgba(0,0,0,0.3);
402 transition: all 0.3s ease;
403 }
404 #mediagrab-toggle-btn:hover {
405 width: 60px;
406 box-shadow: -4px 4px 15px rgba(0,0,0,0.4);
407 }
408 `;
409
410 document.head.appendChild(style);
411 document.body.appendChild(button);
412
413 button.addEventListener('click', () => this.togglePanel());
414 }
415
416 createPanel() {
417 const panel = document.createElement('div');
418 panel.id = 'mediagrab-panel';
419 panel.innerHTML = `
420 <div class="mediagrab-header">
421 <h2>MediaGrab Pro</h2>
422 <button class="mediagrab-close-btn">✕</button>
423 </div>
424
425 <div class="mediagrab-tabs">
426 <button class="mediagrab-tab active" data-tab="images">
427 <span class="tab-icon">🖼️</span>
428 <span class="tab-label">Images</span>
429 <span class="tab-count" id="images-count">0</span>
430 </button>
431 <button class="mediagrab-tab" data-tab="videos">
432 <span class="tab-icon">🎬</span>
433 <span class="tab-label">Videos</span>
434 <span class="tab-count" id="videos-count">0</span>
435 </button>
436 <button class="mediagrab-tab" data-tab="downloads">
437 <span class="tab-icon">📥</span>
438 <span class="tab-label">Downloads</span>
439 <span class="tab-count" id="downloads-count">0</span>
440 </button>
441 </div>
442
443 <div class="mediagrab-content">
444 <div class="mediagrab-tab-content active" id="images-content">
445 <div class="mediagrab-actions">
446 <button class="mediagrab-btn primary" id="download-all-images">
447 Download All Images
448 </button>
449 <button class="mediagrab-btn secondary" id="refresh-images">
450 Refresh
451 </button>
452 </div>
453 <div class="mediagrab-media-grid" id="images-grid"></div>
454 </div>
455
456 <div class="mediagrab-tab-content" id="videos-content">
457 <div class="mediagrab-actions">
458 <button class="mediagrab-btn primary" id="download-all-videos">
459 Download All Videos
460 </button>
461 <button class="mediagrab-btn secondary" id="refresh-videos">
462 Refresh
463 </button>
464 </div>
465 <div class="mediagrab-media-list" id="videos-list"></div>
466 </div>
467
468 <div class="mediagrab-tab-content" id="downloads-content">
469 <div class="mediagrab-actions">
470 <button class="mediagrab-btn secondary" id="clear-completed">
471 Clear Completed
472 </button>
473 </div>
474 <div class="mediagrab-downloads-list" id="downloads-list"></div>
475 </div>
476 </div>
477 `;
478
479 this.addStyles();
480 document.body.appendChild(panel);
481 this.panel = panel;
482 }
483
484 addStyles() {
485 const style = document.createElement('style');
486 style.textContent = `
487 #mediagrab-panel {
488 position: fixed;
489 top: 0;
490 right: -450px;
491 width: 450px;
492 height: 100vh;
493 background: linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%);
494 box-shadow: -5px 0 20px rgba(0,0,0,0.3);
495 z-index: 999998;
496 transition: right 0.3s ease;
497 display: flex;
498 flex-direction: column;
499 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
500 }
501
502 #mediagrab-panel.open {
503 right: 0;
504 }
505
506 .mediagrab-header {
507 background: linear-gradient(135deg, #9b59b6 0%, #e91e63 50%, #89b5f5 100%);
508 color: white;
509 padding: 20px;
510 display: flex;
511 justify-content: space-between;
512 align-items: center;
513 box-shadow: 0 2px 10px rgba(0,0,0,0.2);
514 }
515
516 .mediagrab-header h2 {
517 margin: 0;
518 font-size: 24px;
519 font-weight: 600;
520 }
521
522 .mediagrab-close-btn {
523 background: rgba(255,255,255,0.2);
524 border: none;
525 color: white;
526 width: 35px;
527 height: 35px;
528 border-radius: 50%;
529 font-size: 20px;
530 cursor: pointer;
531 transition: all 0.2s;
532 }
533
534 .mediagrab-close-btn:hover {
535 background: rgba(255,255,255,0.3);
536 transform: rotate(90deg);
537 }
538
539 .mediagrab-tabs {
540 display: flex;
541 background: white;
542 border-bottom: 2px solid #dee2e6;
543 }
544
545 .mediagrab-tab {
546 flex: 1;
547 padding: 15px 10px;
548 background: white;
549 border: none;
550 cursor: pointer;
551 display: flex;
552 flex-direction: column;
553 align-items: center;
554 gap: 5px;
555 transition: all 0.2s;
556 position: relative;
557 }
558
559 .mediagrab-tab:hover {
560 background: #f8f9fa;
561 }
562
563 .mediagrab-tab.active {
564 background: linear-gradient(135deg, #9b59b6 0%, #e91e63 100%);
565 color: white;
566 }
567
568 .mediagrab-tab .tab-icon {
569 font-size: 24px;
570 }
571
572 .mediagrab-tab .tab-label {
573 font-size: 12px;
574 font-weight: 500;
575 }
576
577 .mediagrab-tab .tab-count {
578 position: absolute;
579 top: 5px;
580 right: 5px;
581 background: #e91e63;
582 color: white;
583 border-radius: 10px;
584 padding: 2px 6px;
585 font-size: 10px;
586 font-weight: bold;
587 }
588
589 .mediagrab-tab.active .tab-count {
590 background: rgba(255,255,255,0.3);
591 }
592
593 .mediagrab-content {
594 flex: 1;
595 overflow-y: auto;
596 padding: 15px;
597 }
598
599 .mediagrab-tab-content {
600 display: none;
601 }
602
603 .mediagrab-tab-content.active {
604 display: block;
605 }
606
607 .mediagrab-actions {
608 display: flex;
609 gap: 10px;
610 margin-bottom: 15px;
611 }
612
613 .mediagrab-btn {
614 flex: 1;
615 padding: 12px 20px;
616 border: none;
617 border-radius: 8px;
618 font-size: 14px;
619 font-weight: 600;
620 cursor: pointer;
621 transition: all 0.2s;
622 }
623
624 .mediagrab-btn.primary {
625 background: linear-gradient(135deg, #9b59b6 0%, #e91e63 100%);
626 color: white;
627 }
628
629 .mediagrab-btn.primary:hover {
630 transform: translateY(-2px);
631 box-shadow: 0 4px 12px rgba(155, 89, 182, 0.4);
632 }
633
634 .mediagrab-btn.secondary {
635 background: #89b5f5;
636 color: white;
637 }
638
639 .mediagrab-btn.secondary:hover {
640 background: #6fa3f3;
641 transform: translateY(-2px);
642 }
643
644 .mediagrab-media-grid {
645 display: grid;
646 grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
647 gap: 10px;
648 }
649
650 .mediagrab-media-item {
651 position: relative;
652 aspect-ratio: 1;
653 border-radius: 8px;
654 overflow: hidden;
655 cursor: pointer;
656 transition: all 0.2s;
657 background: white;
658 box-shadow: 0 2px 8px rgba(0,0,0,0.1);
659 }
660
661 .mediagrab-media-item:hover {
662 transform: scale(1.05);
663 box-shadow: 0 4px 16px rgba(0,0,0,0.2);
664 }
665
666 .mediagrab-media-item img {
667 width: 100%;
668 height: 100%;
669 object-fit: cover;
670 }
671
672 .mediagrab-media-item .download-overlay {
673 position: absolute;
674 top: 0;
675 left: 0;
676 right: 0;
677 bottom: 0;
678 background: rgba(155, 89, 182, 0.9);
679 display: flex;
680 align-items: center;
681 justify-content: center;
682 opacity: 0;
683 transition: opacity 0.2s;
684 }
685
686 .mediagrab-media-item:hover .download-overlay {
687 opacity: 1;
688 }
689
690 .mediagrab-media-item .download-overlay span {
691 color: white;
692 font-size: 32px;
693 }
694
695 .mediagrab-media-list {
696 display: flex;
697 flex-direction: column;
698 gap: 10px;
699 }
700
701 .mediagrab-video-item {
702 background: white;
703 border-radius: 8px;
704 padding: 15px;
705 box-shadow: 0 2px 8px rgba(0,0,0,0.1);
706 transition: all 0.2s;
707 }
708
709 .mediagrab-video-item:hover {
710 box-shadow: 0 4px 16px rgba(0,0,0,0.2);
711 }
712
713 .mediagrab-video-item .video-info {
714 display: flex;
715 align-items: center;
716 gap: 10px;
717 margin-bottom: 10px;
718 }
719
720 .mediagrab-video-item .video-icon {
721 font-size: 32px;
722 }
723
724 .mediagrab-video-item .video-name {
725 flex: 1;
726 font-size: 14px;
727 font-weight: 500;
728 color: #333;
729 word-break: break-all;
730 }
731
732 .mediagrab-video-item .video-download-btn {
733 background: linear-gradient(135deg, #9b59b6 0%, #e91e63 100%);
734 color: white;
735 border: none;
736 padding: 8px 16px;
737 border-radius: 6px;
738 cursor: pointer;
739 font-weight: 600;
740 transition: all 0.2s;
741 }
742
743 .mediagrab-video-item .video-download-btn:hover {
744 transform: translateY(-2px);
745 box-shadow: 0 4px 12px rgba(155, 89, 182, 0.4);
746 }
747
748 .mediagrab-downloads-list {
749 display: flex;
750 flex-direction: column;
751 gap: 10px;
752 }
753
754 .mediagrab-download-item {
755 background: white;
756 border-radius: 8px;
757 padding: 15px;
758 box-shadow: 0 2px 8px rgba(0,0,0,0.1);
759 }
760
761 .mediagrab-download-item .download-header {
762 display: flex;
763 justify-content: space-between;
764 align-items: center;
765 margin-bottom: 10px;
766 }
767
768 .mediagrab-download-item .download-name {
769 font-size: 14px;
770 font-weight: 500;
771 color: #333;
772 word-break: break-all;
773 }
774
775 .mediagrab-download-item .download-status {
776 padding: 4px 8px;
777 border-radius: 4px;
778 font-size: 11px;
779 font-weight: 600;
780 text-transform: uppercase;
781 }
782
783 .mediagrab-download-item .download-status.completed {
784 background: #d4edda;
785 color: #155724;
786 }
787
788 .mediagrab-download-item .download-status.downloading {
789 background: #cce5ff;
790 color: #004085;
791 }
792
793 .mediagrab-download-item .download-status.failed {
794 background: #f8d7da;
795 color: #721c24;
796 }
797
798 .mediagrab-download-item .download-progress {
799 width: 100%;
800 height: 6px;
801 background: #e9ecef;
802 border-radius: 3px;
803 overflow: hidden;
804 margin-top: 8px;
805 }
806
807 .mediagrab-download-item .download-progress-bar {
808 height: 100%;
809 background: linear-gradient(90deg, #9b59b6 0%, #e91e63 100%);
810 transition: width 0.3s ease;
811 }
812
813 .mediagrab-download-item .download-info {
814 display: flex;
815 justify-content: space-between;
816 margin-top: 8px;
817 font-size: 12px;
818 color: #6c757d;
819 }
820
821 .mediagrab-empty-state {
822 text-align: center;
823 padding: 40px 20px;
824 color: #6c757d;
825 }
826
827 .mediagrab-empty-state .empty-icon {
828 font-size: 64px;
829 margin-bottom: 15px;
830 }
831
832 .mediagrab-empty-state .empty-text {
833 font-size: 16px;
834 font-weight: 500;
835 }
836
837 /* Scrollbar styling */
838 .mediagrab-content::-webkit-scrollbar {
839 width: 8px;
840 }
841
842 .mediagrab-content::-webkit-scrollbar-track {
843 background: #f1f1f1;
844 }
845
846 .mediagrab-content::-webkit-scrollbar-thumb {
847 background: linear-gradient(180deg, #9b59b6 0%, #e91e63 100%);
848 border-radius: 4px;
849 }
850
851 .mediagrab-content::-webkit-scrollbar-thumb:hover {
852 background: linear-gradient(180deg, #8e44ad 0%, #c2185b 100%);
853 }
854 `;
855
856 document.head.appendChild(style);
857 }
858
859 setupEventListeners() {
860 // Close button
861 this.panel.querySelector('.mediagrab-close-btn').addEventListener('click', () => {
862 this.togglePanel();
863 });
864
865 // Tab switching
866 this.panel.querySelectorAll('.mediagrab-tab').forEach(tab => {
867 tab.addEventListener('click', () => {
868 const tabName = tab.dataset.tab;
869 this.switchTab(tabName);
870 });
871 });
872
873 // Action buttons
874 document.getElementById('download-all-images').addEventListener('click', () => {
875 this.downloadAllImages();
876 });
877
878 document.getElementById('download-all-videos').addEventListener('click', () => {
879 this.downloadAllVideos();
880 });
881
882 document.getElementById('refresh-images').addEventListener('click', () => {
883 this.refreshMedia();
884 });
885
886 document.getElementById('refresh-videos').addEventListener('click', () => {
887 this.refreshMedia();
888 });
889
890 document.getElementById('clear-completed').addEventListener('click', () => {
891 this.downloadManager.clearCompleted();
892 this.updateDownloadsTab();
893 });
894
895 // Listen for download updates
896 window.addEventListener('mediagrab-download-update', () => {
897 this.updateDownloadsTab();
898 });
899
900 // Auto-refresh media periodically
901 setInterval(() => {
902 if (this.isOpen) {
903 this.updateMediaCounts();
904 }
905 }, 3000);
906 }
907
908 togglePanel() {
909 this.isOpen = !this.isOpen;
910 this.panel.classList.toggle('open', this.isOpen);
911
912 if (this.isOpen) {
913 this.refreshMedia();
914 }
915 }
916
917 switchTab(tabName) {
918 this.currentTab = tabName;
919
920 // Update tab buttons
921 this.panel.querySelectorAll('.mediagrab-tab').forEach(tab => {
922 tab.classList.toggle('active', tab.dataset.tab === tabName);
923 });
924
925 // Update tab content
926 this.panel.querySelectorAll('.mediagrab-tab-content').forEach(content => {
927 content.classList.toggle('active', content.id === `${tabName}-content`);
928 });
929
930 // Refresh content
931 if (tabName === 'images') {
932 this.updateImagesTab();
933 } else if (tabName === 'videos') {
934 this.updateVideosTab();
935 } else if (tabName === 'downloads') {
936 this.updateDownloadsTab();
937 }
938 }
939
940 refreshMedia() {
941 this.mediaDetector.scanPage();
942 this.updateMediaCounts();
943
944 if (this.currentTab === 'images') {
945 this.updateImagesTab();
946 } else if (this.currentTab === 'videos') {
947 this.updateVideosTab();
948 }
949 }
950
951 updateMediaCounts() {
952 const media = this.mediaDetector.getDetectedMedia();
953 document.getElementById('images-count').textContent = media.images.length;
954 document.getElementById('videos-count').textContent = media.videos.length;
955 document.getElementById('downloads-count').textContent = this.downloadManager.getDownloads().length;
956 }
957
958 updateImagesTab() {
959 const media = this.mediaDetector.getDetectedMedia();
960 const grid = document.getElementById('images-grid');
961
962 if (media.images.length === 0) {
963 grid.innerHTML = `
964 <div class="mediagrab-empty-state">
965 <div class="empty-icon">🖼️</div>
966 <div class="empty-text">No images detected on this page</div>
967 </div>
968 `;
969 return;
970 }
971
972 grid.innerHTML = media.images.map(img => `
973 <div class="mediagrab-media-item" data-url="${img.url}" data-filename="${img.filename}">
974 <img src="${img.url}" alt="${img.filename}" loading="lazy">
975 <div class="download-overlay">
976 <span>⬇️</span>
977 </div>
978 </div>
979 `).join('');
980
981 // Add click handlers
982 grid.querySelectorAll('.mediagrab-media-item').forEach(item => {
983 item.addEventListener('click', () => {
984 const url = item.dataset.url;
985 const filename = item.dataset.filename;
986 this.downloadManager.download({ url, filename, type: 'image' });
987 });
988 });
989 }
990
991 updateVideosTab() {
992 const media = this.mediaDetector.getDetectedMedia();
993 const list = document.getElementById('videos-list');
994
995 if (media.videos.length === 0) {
996 list.innerHTML = `
997 <div class="mediagrab-empty-state">
998 <div class="empty-icon">🎬</div>
999 <div class="empty-text">No videos detected on this page</div>
1000 </div>
1001 `;
1002 return;
1003 }
1004
1005 list.innerHTML = media.videos.map(video => `
1006 <div class="mediagrab-video-item">
1007 <div class="video-info">
1008 <div class="video-icon">🎬</div>
1009 <div class="video-name">${video.filename}</div>
1010 <button class="video-download-btn" data-url="${video.url}" data-filename="${video.filename}">
1011 Download
1012 </button>
1013 </div>
1014 </div>
1015 `).join('');
1016
1017 // Add click handlers
1018 list.querySelectorAll('.video-download-btn').forEach(btn => {
1019 btn.addEventListener('click', () => {
1020 const url = btn.dataset.url;
1021 const filename = btn.dataset.filename;
1022 this.downloadManager.download({ url, filename, type: 'video' });
1023 });
1024 });
1025 }
1026
1027 updateDownloadsTab() {
1028 const downloads = this.downloadManager.getDownloads();
1029 const list = document.getElementById('downloads-list');
1030
1031 if (downloads.length === 0) {
1032 list.innerHTML = `
1033 <div class="mediagrab-empty-state">
1034 <div class="empty-icon">📥</div>
1035 <div class="empty-text">No downloads yet</div>
1036 </div>
1037 `;
1038 return;
1039 }
1040
1041 list.innerHTML = downloads.map(download => `
1042 <div class="mediagrab-download-item">
1043 <div class="download-header">
1044 <div class="download-name">${download.filename}</div>
1045 <div class="download-status ${download.status}">${download.status}</div>
1046 </div>
1047 ${download.status === 'downloading' ? `
1048 <div class="download-progress">
1049 <div class="download-progress-bar" style="width: ${download.progress}%"></div>
1050 </div>
1051 <div class="download-info">
1052 <span>${Math.round(download.progress)}%</span>
1053 <span>${formatFileSize(download.downloadedSize)} / ${formatFileSize(download.size)}</span>
1054 </div>
1055 ` : ''}
1056 ${download.status === 'failed' ? `
1057 <div class="download-info">
1058 <span style="color: #dc3545;">${download.error}</span>
1059 </div>
1060 ` : ''}
1061 </div>
1062 `).join('');
1063
1064 this.updateMediaCounts();
1065 }
1066
1067 async downloadAllImages() {
1068 const media = this.mediaDetector.getDetectedMedia();
1069 if (media.images.length === 0) {
1070 alert('No images to download');
1071 return;
1072 }
1073
1074 console.log(`MediaGrab Pro: Downloading ${media.images.length} images`);
1075 await this.downloadManager.downloadBatch(media.images);
1076 this.switchTab('downloads');
1077 }
1078
1079 async downloadAllVideos() {
1080 const media = this.mediaDetector.getDetectedMedia();
1081 if (media.videos.length === 0) {
1082 alert('No videos to download');
1083 return;
1084 }
1085
1086 console.log(`MediaGrab Pro: Downloading ${media.videos.length} videos`);
1087 await this.downloadManager.downloadBatch(media.videos);
1088 this.switchTab('downloads');
1089 }
1090 }
1091
1092 // ============================================
1093 // INITIALIZATION
1094 // ============================================
1095
1096 function init() {
1097 console.log('MediaGrab Pro: Starting initialization');
1098
1099 // Wait for page to be ready
1100 if (document.readyState === 'loading') {
1101 document.addEventListener('DOMContentLoaded', init);
1102 return;
1103 }
1104
1105 // Initialize components
1106 const mediaDetector = new MediaDetector();
1107 const downloadManager = new DownloadManager();
1108 const sidePanelUI = new SidePanelUI(mediaDetector, downloadManager);
1109
1110 // Start the extension
1111 setTimeout(() => {
1112 mediaDetector.init();
1113 sidePanelUI.init();
1114 console.log('MediaGrab Pro: Fully initialized and ready');
1115 }, 1000);
1116 }
1117
1118 // Start the extension
1119 init();
1120
1121})();