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