您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Facebook image, post, story, whole profile post, image downloader
// ==UserScript== // @name Facebook Image Downloader // @namespace http://tampermonkey.net/ // @version 1.01 // @description Facebook image, post, story, whole profile post, image downloader // @author Bibek Chand Sah // @match https://www.facebook.com/* // @match https://facebook.com/* // @match https://m.facebook.com/* // @grant GM_download // @grant GM_addStyle // @icon https://cdn-icons-png.flaticon.com/512/5968/5968764.png // @license MIT // ==/UserScript== (function() { 'use strict'; // Add styles for the download button, terminal UI, and notifications GM_addStyle(` #facebook-downloader-container { position: fixed; top: 60px; right: 10px; z-index: 9999; display: inline-block; } #facebook-downloader-btn { position: relative; background: #1877f2; color: white; border: none; padding: 10px 15px; border-radius: 5px; cursor: pointer; font-weight: bold; box-shadow: 0 2px 5px rgba(0,0,0,0.3); display: flex; align-items: center; gap: 8px; transition: all 0.3s ease; user-select: none; } #drag-handle { position: absolute; top: -15px; left: -15px; background: rgba(255,255,255,0.9); border: 2px solid #1877f2; border-radius: 50%; width: 25px; height: 25px; display: flex; align-items: center; justify-content: center; font-size: 14px; cursor: grab; transition: all 0.2s ease; animation: subtle-pulse 3s ease-in-out infinite; box-shadow: 0 2px 8px rgba(0,0,0,0.2); z-index: 1; } #facebook-downloader-btn:hover { background: #166fe5; transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0,0,0,0.4); } #facebook-downloader-btn:hover ~ #terminal-toggle-btn { opacity: 1; visibility: visible; transform: translateX(0); } #drag-handle { background: rgba(255,255,255,0.9); border: 2px solid #1877f2; border-radius: 50%; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; font-size: 16px; cursor: grab; transition: all 0.2s ease; animation: subtle-pulse 3s ease-in-out infinite; box-shadow: 0 2px 8px rgba(0,0,0,0.2); } @keyframes subtle-pulse { 0%, 100% { opacity: 0.8; } 50% { opacity: 1; transform: scale(1.05); } } #drag-handle:hover { background: rgba(255,255,255,1); transform: scale(1.2); animation: none; box-shadow: 0 4px 12px rgba(0,0,0,0.3); } #drag-handle:active { cursor: grabbing; transform: scale(0.95); } #facebook-downloader-btn.dragging { opacity: 0.8; transform: rotate(2deg); z-index: 10000; box-shadow: 0 8px 16px rgba(0,0,0,0.3); } #facebook-downloader-btn:hover .drag-handle { animation: none; transform: scale(1.2); } #terminal-toggle-btn { position: absolute; top: 50%; left: -35px; transform: translateY(-50%); background: #333; color: white; border: none; padding: 5px 8px; border-radius: 3px; cursor: pointer; font-weight: bold; box-shadow: 0 2px 5px rgba(0,0,0,0.3); font-size: 12px; opacity: 0; transition: all 0.3s ease; z-index: 1; } #contributor-btn { position: absolute; top: 50%; left: -70px; transform: translateY(-50%); background: #24292e; color: white; border: none; padding: 5px 8px; border-radius: 3px; cursor: pointer; font-weight: bold; box-shadow: 0 2px 5px rgba(0,0,0,0.3); font-size: 12px; opacity: 0; transition: all 0.3s ease; z-index: 1; text-decoration: none; display: flex; align-items: center; gap: 3px; } #contributor-btn:hover { background: #0366d6; transform: translateY(-50%) translateY(-1px); box-shadow: 0 4px 8px rgba(0,0,0,0.4); } #position-dropdown { position: absolute; top: 50%; left: -105px; transform: translateY(-50%); background: #4a90e2; color: white; border: none; padding: 5px 8px; border-radius: 3px; cursor: pointer; font-weight: bold; box-shadow: 0 2px 5px rgba(0,0,0,0.3); font-size: 12px; opacity: 0; transition: all 0.3s ease; z-index: 1; display: flex; align-items: center; gap: 3px; } #position-dropdown:hover { background: #357abd; transform: translateY(-50%) translateY(-1px); box-shadow: 0 4px 8px rgba(0,0,0,0.4); } .position-menu { position: absolute; bottom: -135px; left: -140px; background: white; border: 1px solid #ddd; border-radius: 5px; box-shadow: 0 4px 12px rgba(0,0,0,0.2); padding: 5px; display: none; z-index: 1000; min-width: 120px; } .position-menu.show { display: block; } .position-option { padding: 8px 12px; cursor: pointer; border-radius: 3px; font-size: 11px; color: #333; transition: background 0.2s ease; } .position-option:hover { background: #f0f0f0; } .position-option.active { background: #4a90e2; color: white; } #facebook-downloader-btn:hover #terminal-toggle-btn, #facebook-downloader-btn:hover #contributor-btn, #facebook-downloader-btn:hover #position-dropdown { opacity: 1; } #terminal-toggle-btn:hover { background: #444; box-shadow: 0 4px 8px rgba(0,0,0,0.4); } #terminal-console { position: fixed; bottom: -300px; left: 0; right: 0; height: 300px; background: #1e1e1e; border-top: 2px solid #333; z-index: 9998; transition: bottom 0.3s ease; display: flex; flex-direction: column; } #terminal-console.show { bottom: 0; } #terminal-header { background: #333; color: white; padding: 8px 16px; font-size: 14px; font-weight: bold; display: flex; justify-content: space-between; align-items: center; } #terminal-close { background: none; border: none; color: #ccc; font-size: 18px; cursor: pointer; padding: 0; width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; } #terminal-close:hover { color: white; } #terminal-content { flex: 1; background: #1e1e1e; color: #00ff00; font-family: 'Courier New', monospace; font-size: 12px; padding: 16px; overflow-y: auto; white-space: pre-wrap; line-height: 1.4; } #terminal-content::-webkit-scrollbar { width: 8px; } #terminal-content::-webkit-scrollbar-track { background: #2d2d2d; } #terminal-content::-webkit-scrollbar-thumb { background: #555; border-radius: 4px; } #terminal-content::-webkit-scrollbar-thumb:hover { background: #777; } .log-info { color: #00ff00; } .log-success { color: #00ff00; font-weight: bold; } .log-error { color: #ff4444; font-weight: bold; } .log-warning { color: #ffaa00; } .log-progress { color: #44aaff; } #download-progress { position: fixed; top: 60px; right: 10px; z-index: 9999; background: rgba(0,0,0,0.8); color: white; padding: 10px; border-radius: 5px; display: none; max-width: 300px; word-wrap: break-word; } .notification { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 10000; background: rgba(0, 0, 0, 0.9); color: white; padding: 20px 30px; border-radius: 10px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); font-size: 16px; font-weight: bold; text-align: center; min-width: 300px; animation: fadeInOut 3s ease-in-out; } .notification.error { background: rgba(220, 53, 69, 0.9); border: 2px solid #dc3545; } .notification.warning { background: rgba(255, 193, 7, 0.9); border: 2px solid #ffc107; color: #000; } .notification.info { background: #20a464e6; border: 2px solid #00ff73ff; } @keyframes fadeInOut { 0% { opacity: 0; transform: translate(-50%, -50%) scale(0.8); } 15% { opacity: 1; transform: translate(-50%, -50%) scale(1); } 85% { opacity: 1; transform: translate(-50%, -50%) scale(1); } 100% { opacity: 0; transform: translate(-50%, -50%) scale(0.8); } } .fb-download-icon { position: absolute; top: 10px; right: 10px; width: 30px; height: 30px; background: rgba(0, 0, 0, 0.7); border-radius: 50%; cursor: pointer; z-index: 1000; display: flex; align-items: center; justify-content: center; transition: all 0.3s ease; opacity: 0; font-size: 14px; } .fb-download-icon:hover { background: rgba(0, 0, 0, 0.9); transform: scale(1.1); } div.x10l6tqk.x13vifvy:hover .fb-download-icon { opacity: 1; } div.xh8yej3:hover .fb-download-icon { opacity: 1; } div.x6s0dn4.x78zum5.xdt5ytf.xl56j7k.x1n2onr6 { position: relative !important; } div.x6s0dn4.x78zum5.xdt5ytf.xl56j7k.x1n2onr6:hover .fb-download-icon { opacity: 1; } div.xdj266r.x14z9mp.xat24cr.x1lziwak.xexx8yu.xyri2b.x18d9i69.x1c1uobl.x18d0r48.x1ey2m1c.xtijo5x.x1o0tod.x10l6tqk.x13vifvy.xl8spv7.xt2wqj3 { position: relative !important; } div.xdj266r.x14z9mp.xat24cr.x1lziwak.xexx8yu.xyri2b.x18d9i69.x1c1uobl.x18d0r48.x1ey2m1c.xtijo5x.x1o0tod.x10l6tqk.x13vifvy.xl8spv7.xt2wqj3:hover .fb-download-icon { opacity: 1; } `); // Create container for download button and drag handle const downloaderContainer = document.createElement('div'); downloaderContainer.id = 'facebook-downloader-container'; // Create bulk download button const downloadBtn = document.createElement('button'); downloadBtn.id = 'facebook-downloader-btn'; downloadBtn.innerHTML = 'Download All Images'; // Create separate drag handle const dragHandle = document.createElement('div'); dragHandle.id = 'drag-handle'; dragHandle.innerHTML = '🌠'; dragHandle.title = 'Drag to move'; // Create terminal toggle button const terminalToggleBtn = document.createElement('button'); terminalToggleBtn.id = 'terminal-toggle-btn'; terminalToggleBtn.textContent = '⬇️'; terminalToggleBtn.title = 'Toggle Terminal Console'; // Create contributor button const contributorBtn = document.createElement('a'); contributorBtn.id = 'contributor-btn'; contributorBtn.href = 'https://github.com/bibekchandsah/fb-ig-image-auto-download'; contributorBtn.target = '_blank'; contributorBtn.title = 'View on GitHub - Contribute'; contributorBtn.innerHTML = '<span style="font-size: 14px;">⭐</span>'; // Create position dropdown const positionDropdown = document.createElement('button'); positionDropdown.id = 'position-dropdown'; positionDropdown.title = 'Change icon position'; positionDropdown.innerHTML = '📍'; // Create dropdown menu const positionMenu = document.createElement('div'); positionMenu.className = 'position-menu'; positionMenu.innerHTML = ` <div class="position-option active" data-position="top-left">Top Left</div> <div class="position-option" data-position="top-right">Top Right</div> <div class="position-option" data-position="bottom-right">Bottom Right</div> <div class="position-option" data-position="bottom-left">Bottom Left</div> <div class="position-option" data-position="center">Center</div> `; positionDropdown.appendChild(positionMenu); // Assemble the container - download button contains all control buttons downloadBtn.appendChild(dragHandle); downloadBtn.appendChild(terminalToggleBtn); downloadBtn.appendChild(contributorBtn); downloadBtn.appendChild(positionDropdown); downloaderContainer.appendChild(downloadBtn); document.body.appendChild(downloaderContainer); // Add drag functionality to the separate handle let isDragging = false; let dragOffset = { x: 0, y: 0 }; dragHandle.addEventListener('mousedown', function(e) { e.preventDefault(); e.stopPropagation(); isDragging = true; const rect = downloaderContainer.getBoundingClientRect(); dragOffset.x = e.clientX - rect.left; dragOffset.y = e.clientY - rect.top; downloaderContainer.classList.add('dragging'); document.body.style.userSelect = 'none'; }); // Prevent drag handle from triggering download on any click event dragHandle.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); }); document.addEventListener('mousemove', function(e) { if (!isDragging) return; e.preventDefault(); const x = e.clientX - dragOffset.x; const y = e.clientY - dragOffset.y; // Keep container within viewport bounds const maxX = window.innerWidth - downloaderContainer.offsetWidth; const maxY = window.innerHeight - downloaderContainer.offsetHeight; const constrainedX = Math.max(0, Math.min(x, maxX)); const constrainedY = Math.max(0, Math.min(y, maxY)); downloaderContainer.style.left = constrainedX + 'px'; downloaderContainer.style.top = constrainedY + 'px'; downloaderContainer.style.right = 'auto'; }); document.addEventListener('mouseup', function(e) { if (isDragging) { isDragging = false; downloaderContainer.classList.remove('dragging'); document.body.style.userSelect = ''; // Save position to localStorage const rect = downloaderContainer.getBoundingClientRect(); localStorage.setItem('fb-downloader-pos', JSON.stringify({ left: rect.left, top: rect.top })); } }); // Additional safety: end drag on mouse leave (prevents sticking) document.addEventListener('mouseleave', function(e) { if (isDragging) { isDragging = false; downloaderContainer.classList.remove('dragging'); document.body.style.userSelect = ''; } }); // End drag if Escape key is pressed document.addEventListener('keydown', function(e) { if (e.key === 'Escape' && isDragging) { isDragging = false; downloaderContainer.classList.remove('dragging'); document.body.style.userSelect = ''; } }); // Restore saved position const savedPos = localStorage.getItem('fb-downloader-pos'); if (savedPos) { try { const pos = JSON.parse(savedPos); downloaderContainer.style.left = pos.left + 'px'; downloaderContainer.style.top = pos.top + 'px'; downloaderContainer.style.right = 'auto'; } catch (e) { console.log('Could not restore container position:', e); } } // Create terminal console const terminalConsole = document.createElement('div'); terminalConsole.id = 'terminal-console'; const terminalHeader = document.createElement('div'); terminalHeader.id = 'terminal-header'; terminalHeader.innerHTML = ` <span>Facebook Downloader Terminal</span> <button id="terminal-close">×</button> `; const terminalContent = document.createElement('div'); terminalContent.id = 'terminal-content'; terminalContent.textContent = 'Terminal initialized. Ready for operations...\n'; terminalConsole.appendChild(terminalHeader); terminalConsole.appendChild(terminalContent); document.body.appendChild(terminalConsole); // Add event listeners for terminal (after elements are in DOM) terminalToggleBtn.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); toggleTerminal(); }); // Prevent contributor button from triggering download contributorBtn.addEventListener('click', function(e) { e.stopPropagation(); // Don't prevent default since we want the link to work }); // Position dropdown functionality let currentPosition = localStorage.getItem('fb-icon-position') || 'top-left'; const positionSettings = { 'top-left': { top: '10px', left: '10px', right: 'auto', bottom: 'auto', transform: 'none' }, 'top-right': { top: '10px', right: '10px', left: 'auto', bottom: 'auto', transform: 'none' }, 'bottom-right': { bottom: '10px', right: '10px', top: 'auto', left: 'auto', transform: 'none' }, 'bottom-left': { bottom: '10px', left: '10px', top: 'auto', right: 'auto', transform: 'none' }, 'center': { top: '50%', right: '50%', left: 'auto', bottom: 'auto', transform: 'none' } }; function updateIconPosition(position) { const settings = positionSettings[position]; const iconStyle = ` .fb-download-icon { position: absolute; top: ${settings.top}; right: ${settings.right}; bottom: ${settings.bottom}; left: ${settings.left}; transform: ${settings.transform}; background: rgba(0, 0, 0, 0.7); color: white; padding: 5px; border-radius: 50%; cursor: pointer; font-size: 16px; z-index: 1000; opacity: 0; transition: all 0.3s ease; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; } `; // Remove old style if exists const oldStyle = document.getElementById('fb-icon-position-style'); if (oldStyle) oldStyle.remove(); // Add new style const styleElement = document.createElement('style'); styleElement.id = 'fb-icon-position-style'; styleElement.textContent = iconStyle; document.head.appendChild(styleElement); // Update active option in menu positionMenu.querySelectorAll('.position-option').forEach(option => { option.classList.remove('active'); if (option.dataset.position === position) { option.classList.add('active'); } }); currentPosition = position; localStorage.setItem('fb-icon-position', position); // Log the change if (typeof logToTerminal === 'function') { logToTerminal(`Icon position changed to: ${position}`, 'info'); } } // Initialize with saved position updateIconPosition(currentPosition); // Position dropdown event listeners positionDropdown.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); positionMenu.classList.toggle('show'); }); // Position option click handlers positionMenu.addEventListener('click', function(e) { if (e.target.classList.contains('position-option')) { e.preventDefault(); e.stopPropagation(); const position = e.target.dataset.position; updateIconPosition(position); positionMenu.classList.remove('show'); } }); // Close dropdown when clicking outside document.addEventListener('click', function(e) { if (!positionDropdown.contains(e.target)) { positionMenu.classList.remove('show'); } }); document.getElementById('terminal-close').addEventListener('click', () => { if (terminalVisible) { toggleTerminal(); } }); // Create progress indicator const progressDiv = document.createElement('div'); progressDiv.id = 'download-progress'; document.body.appendChild(progressDiv); let downloadCount = 0; let isDownloading = false; let addedIcons = new Set(); let terminalVisible = false; // Notification system function showNotification(message, type = 'info', duration = 3000) { // Remove any existing notifications const existingNotifications = document.querySelectorAll('.notification'); existingNotifications.forEach(notif => notif.remove()); // Create notification element const notification = document.createElement('div'); notification.className = `notification ${type}`; notification.textContent = message; // Add to page document.body.appendChild(notification); // Remove after duration setTimeout(() => { if (notification.parentNode) { notification.parentNode.removeChild(notification); } }, duration); return notification; } // Terminal logging functions function logToTerminal(message, type = 'info') { const timestamp = new Date().toLocaleTimeString(); const logLine = `[${timestamp}] ${message}\n`; const content = document.getElementById('terminal-content'); const logElement = document.createElement('span'); logElement.className = `log-${type}`; logElement.textContent = logLine; content.appendChild(logElement); content.scrollTop = content.scrollHeight; } function clearTerminal() { const content = document.getElementById('terminal-content'); content.innerHTML = ''; logToTerminal('Terminal cleared', 'info'); } // Terminal toggle functionality function toggleTerminal() { terminalVisible = !terminalVisible; const terminal = document.getElementById('terminal-console'); if (terminalVisible) { terminal.classList.add('show'); terminalToggleBtn.textContent = '⬆️'; terminalToggleBtn.title = 'Hide Terminal Console'; logToTerminal('Terminal opened', 'info'); } else { terminal.classList.remove('show'); terminalToggleBtn.textContent = '⬇️'; terminalToggleBtn.title = 'Show Terminal Console'; } } // Notification system function showNotification(message, type = 'info', duration = 3000) { // Remove any existing notifications const existingNotifications = document.querySelectorAll('.notification'); existingNotifications.forEach(notif => notif.remove()); // Create notification element const notification = document.createElement('div'); notification.className = `notification ${type}`; notification.textContent = message; // Add to page document.body.appendChild(notification); // Remove after duration setTimeout(() => { if (notification.parentNode) { notification.parentNode.removeChild(notification); } }, duration); return notification; } function extractImageUrl(img) { if (img.src) { return img.src; } else if (img.dataset && img.dataset.src) { return img.dataset.src; } else if (img.getAttribute('data-src')) { return img.getAttribute('data-src'); } return null; } // Function to extract post date from Facebook post function extractPostDate(divElement) { try { // Find the closest post container first const postContainer = divElement.closest('div.x1n2onr6.x1ja2u2z.x1jx94hy.xw5cjc7.x1dmpuos.x1vsv7so.xau1kf4.x9f619.xh8yej3.x6ikm8r.x10wlt62.xquyuld') || divElement.querySelector('div.x1n2onr6.x1ja2u2z.x1jx94hy.xw5cjc7.x1dmpuos.x1vsv7so.xau1kf4.x9f619.xh8yej3.x6ikm8r.x10wlt62.xquyuld'); if (!postContainer) { console.log('Facebook Date Extractor: No post container found'); return null; } // Try multiple approaches to find date elements let dateText = null; // Approach 1: Look for the specific date structure you mentioned const dateDiv = postContainer.querySelector('div.xdj266r.x14z9mp.xat24cr.x1lziwak.xexx8yu.xyri2b.x18d9i69.x1c1uobl.x6s0dn4.x17zd0t2.x78zum5.x1q0g3np.x1a02dak'); if (dateDiv) { console.log('Facebook Date Extractor: Found date div'); const dateSpan = dateDiv.querySelector('span.xdj266r.x14z9mp.xat24cr.x1lziwak.xexx8yu.xyri2b.x18d9i69.x1c1uobl.x1hl2dhg.x16tdsg8.x1vvkbs.x4k7w5x.x1h91t0o.x1h9r5lt.x1jfb8zj.xv2umb2.x1beo9mf.xaigb6o.x12ejxvf.x3igimt.xarpa2k.xedcshv.x1lytzrv.x1t2pt76.x7ja8zs.x1qrby5j'); if (dateSpan) { console.log('Facebook Date Extractor: Found date span'); const dateAnchor = dateSpan.querySelector('a.x1i10hfl.xjbqb8w.x1ejq31n.x18oe1m7.x1sy0etr.xstzfhl.x972fbf.x10w94by.x1qhh985.x14e42zd.x9f619.x1ypdohk.xt0psk2.x3ct3a4.xdj266r.x14z9mp.xat24cr.x1lziwak.xexx8yu.xyri2b.x18d9i69.x1c1uobl.x16tdsg8.x1hl2dhg.xggy1nq.x1a2a7pz.xkrqix3.x1sur9pj.xi81zsa.x1s688f'); if (dateAnchor) { console.log('Facebook Date Extractor: Found date anchor'); const finalSpan = dateAnchor.querySelector('span.x1rg5ohu.x6ikm8r.x10wlt62.x16dsc37.xt0b8zv'); if (finalSpan) { dateText = finalSpan.textContent || finalSpan.innerText; console.log('Facebook Date Extractor: Found date text (Approach 1):', dateText); } } } } // Approach 2: Look for any time/date related elements if (!dateText || dateText.length < 3) { console.log('Facebook Date Extractor: Trying Approach 2 - time elements'); const timeElements = postContainer.querySelectorAll('time, [datetime], [data-utime], [title*="20"], [aria-label*="20"]'); for (let timeEl of timeElements) { const text = timeEl.textContent || timeEl.innerText || timeEl.getAttribute('datetime') || timeEl.getAttribute('data-utime') || timeEl.getAttribute('title') || timeEl.getAttribute('aria-label'); if (text && text.trim().length > 2) { dateText = text; console.log('Facebook Date Extractor: Found date text (Approach 2):', dateText); break; } } } // Approach 3: Look for spans that might contain date text (more aggressive) if (!dateText || dateText.length < 3) { console.log('Facebook Date Extractor: Trying Approach 3 - span search'); const spans = postContainer.querySelectorAll('span, a'); for (let span of spans) { const text = span.textContent || span.innerText; if (text && text.trim().length > 2 && text.trim().length < 50) { // Check for date patterns if (/\\b(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\\s+\\d{1,2}[,\\s]*\\d{4}/i.test(text) || /\\b\\d{1,2}[/\\-]\\d{1,2}[/\\-]\\d{4}\\b/.test(text) || /\\b\\d{4}[/\\-]\\d{1,2}[/\\-]\\d{1,2}\\b/.test(text) || /\\b(yesterday|today|\\d+\\s*(h|hour|hours|m|min|minute|minutes|d|day|days)\\s*ago)\\b/i.test(text) || /\\b(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\\s+\\d{1,2}\\b/i.test(text) || text.includes('20') && (text.includes('Jan') || text.includes('Feb') || text.includes('Mar') || text.includes('Apr') || text.includes('May') || text.includes('Jun') || text.includes('Jul') || text.includes('Aug') || text.includes('Sep') || text.includes('Oct') || text.includes('Nov') || text.includes('Dec'))) { dateText = text; console.log('Facebook Date Extractor: Found date text (Approach 3):', dateText); break; } } } } // Approach 4: Really aggressive search - look for any text that might be a date if (!dateText || dateText.length < 3) { console.log('Facebook Date Extractor: Trying Approach 4 - aggressive search'); const allElements = postContainer.querySelectorAll('*'); for (let elem of allElements) { const text = elem.textContent || elem.innerText; if (text && elem.children.length === 0 && text.trim().length > 2 && text.trim().length < 30) { // Look for very specific patterns if (/^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\\s+\\d{1,2}[,\\s]*\\d{4}$/i.test(text.trim()) || /^\\d{1,2}[/\\-]\\d{1,2}[/\\-]\\d{4}$/.test(text.trim()) || /^(yesterday|today|\\d+\\s*(h|hour|hours|m|min|minute|minutes|d|day|days)\\s*ago)$/i.test(text.trim())) { dateText = text.trim(); console.log('Facebook Date Extractor: Found date text (Approach 4):', dateText); break; } } } } if (dateText) { console.log('Facebook Date Extractor: Raw date text:', dateText); // First, handle the Facebook dash-separated character issue let cleanDate = dateText; // Remove the dash-separated character formatting that Facebook uses // This converts "-S---e--p-------t-e----m--b---er-- --25--" to "September 25" cleanDate = cleanDate.replace(/-+/g, '').replace(/\s+/g, ' ').trim(); console.log('Facebook Date Extractor: After removing dashes:', cleanDate); // Now remove any remaining invalid filename characters cleanDate = cleanDate.replace(/[<>:"/\\|?*\x00-\x1f\x7f-\x9f]/g, '').trim(); console.log('Facebook Date Extractor: After removing invalid chars:', cleanDate); // Replace multiple spaces with single spaces cleanDate = cleanDate.replace(/\s+/g, ' '); console.log('Facebook Date Extractor: Final cleaning:', cleanDate); // Validate the cleaned date if (cleanDate.length > 2 && cleanDate.length < 50) { // Check if it contains reasonable date content const hasMonth = /\b(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec|January|February|March|April|May|June|July|August|September|October|November|December)\b/i.test(cleanDate); const hasNumber = /\d/.test(cleanDate); const hasTimeIndicator = /\b(AM|PM|at|ago|hour|min|day|yesterday|today)\b/i.test(cleanDate); if (hasMonth || hasNumber || hasTimeIndicator) { console.log('Facebook Date Extractor: Final date:', cleanDate); return cleanDate; } } console.log('Facebook Date Extractor: Date validation failed'); return null; } console.log('Facebook Date Extractor: No date found'); return null; } catch (error) { console.log('Facebook Date Extractor: Error extracting post date:', error); return null; } } // Function to determine image type based on CSS classes function getImageType(img) { const className = img.className; // Check for new image class (priority check) if (className.includes('xz74otr') && className.includes('x16uus16') && className.includes('xbiv7yw') && className.includes('xjbqb8w') && className.includes('xu25z0z') && className.includes('x1fmog5m') && className.includes('x1o0tod') && className.includes('x10l6tqk') && className.includes('xwa60dl') && className.includes('x1cb1t30') && className.includes('xh8yej3') && className.includes('x1ja2u2z')) { return 'story-view'; } // Check for post view (existing class) else if (className.includes('x15mokao') && className.includes('x1ga7v0g') && className.includes('x16uus16') && className.includes('xbiv7yw') && className.includes('x1bwycvy') && className.includes('x193iq5w') && className.includes('x4fas0m') && className.includes('x19kjcj4')) { return 'post-view'; } // Check for multiple photos (existing class) else if (className.includes('xz74otr') && className.includes('x15mokao') && className.includes('x1ga7v0g') && className.includes('x16uus16') && className.includes('xbiv7yw')) { return 'multiple-photos'; } // Check for post photo (priority 3) else if (className.includes('x15mokao') && className.includes('x1ga7v0g') && className.includes('x16uus16') && className.includes('xbiv7yw') && className.includes('xl1xv1r')) { return 'post-photo'; } // Check for profile photo (priority 1) else if (className.includes('x1rg5ohu') && className.includes('xl1xv1r') && !className.includes('x15mokao')) { return 'profile-photo'; } // Check for cover photo (priority 2) else if (className.includes('xz74otr') && className.includes('x1ey2m1c') && className.includes('x9f619')) { return 'cover-photo'; } // Fallback else { return 'unknown-photo'; } } // Function to generate filename function generateFilename(altText, index, imageType, postDate) { let filename = `fb-image-${index}-`; // Add post date if available if (postDate) { filename += `${postDate}-`; } filename += `${imageType}-`; if (altText) { let cleanAlt = altText.replace(/[<>:"/\\|?*]/g, '').trim(); if (cleanAlt.length > 50) { cleanAlt = cleanAlt.substring(0, 50) + '...'; } filename += cleanAlt; } else { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); filename += timestamp; } return filename + '.jpg'; } // Function to download image async function downloadImage(img, divElement) { const imageUrl = extractImageUrl(img); if (!imageUrl) { logToTerminal('Could not extract image URL from current image', 'error'); showNotification('❌ Failed to extract image URL!\nThe image source could not be found.', 'error', 4000); return; } const altText = img.alt || img.getAttribute('alt') || ''; const imageType = getImageType(img); const postDate = extractPostDate(divElement); downloadCount++; const filename = generateFilename(altText, downloadCount, imageType, postDate); try { logToTerminal(`Starting download: ${filename}`, 'info'); GM_download(imageUrl, filename); logToTerminal(`Successfully downloaded: ${filename}`, 'success'); showNotification(`✅ Image downloaded!\n${filename}`, 'info', 2500); console.log(`Downloaded: ${filename}`); } catch (error) { logToTerminal(`Failed to download image: ${filename} - ${error.message}`, 'error'); showNotification(`❌ Download failed!\n${filename}\n${error.message}`, 'error', 4000); console.error('Failed to download image:', error); } } // Function to add download icon to a div function addDownloadIcon(divElement) { // Check if icon already exists if (divElement.querySelector('.fb-download-icon') || addedIcons.has(divElement)) { return false; } // Find the specific image with the target classes - try all six selectors let img = divElement.querySelector('img.x1rg5ohu.x5yr21d.xl1xv1r.xh8yej3'); // Priority 1 if (!img) { // Try the second image selector img = divElement.querySelector('img.xz74otr.x1ey2m1c.x9f619.x5yr21d.xtijo5x.x1o0tod.x10l6tqk.x13vifvy.xh8yej3'); // Priority 2 } if (!img) { // Try the third image selector img = divElement.querySelector('img.x15mokao.x1ga7v0g.x16uus16.xbiv7yw.x1ey2m1c.x5yr21d.xtijo5x.x1o0tod.x10l6tqk.x13vifvy.xh8yej3.xl1xv1r'); // Priority 3 } if (!img) { // Try the fourth image selector (multiple photos) img = divElement.querySelector('img.xz74otr.x15mokao.x1ga7v0g.x16uus16.xbiv7yw.x1ey2m1c.x5yr21d.xtijo5x.x1o0tod.x10l6tqk.x13vifvy.xh8yej3'); // Priority 4 } if (!img) { // Try the fifth image selector (post view) img = divElement.querySelector('img.x15mokao.x1ga7v0g.x16uus16.xbiv7yw.x1bwycvy.x193iq5w.x4fas0m.x19kjcj4'); // Priority 5 } if (!img) { // Try the sixth image selector (special photo) img = divElement.querySelector('img.xz74otr.x16uus16.xbiv7yw.xjbqb8w.xu25z0z.x1fmog5m.x1o0tod.x10l6tqk.xwa60dl.x1cb1t30.xh8yej3.x1ja2u2z'); // Priority 6 } if (!img) { return false; } // Create download icon const iconContainer = document.createElement('div'); iconContainer.className = 'fb-download-icon'; iconContainer.title = 'Download this image'; iconContainer.textContent = '💾'; // Add click event iconContainer.addEventListener('click', async (e) => { e.preventDefault(); e.stopPropagation(); // Find current image at click time - try all six selectors let currentImg = divElement.querySelector('img.x1rg5ohu.x5yr21d.xl1xv1r.xh8yej3'); if (!currentImg) { currentImg = divElement.querySelector('img.xz74otr.x1ey2m1c.x9f619.x5yr21d.xtijo5x.x1o0tod.x10l6tqk.x13vifvy.xh8yej3'); } if (!currentImg) { currentImg = divElement.querySelector('img.x15mokao.x1ga7v0g.x16uus16.xbiv7yw.x1ey2m1c.x5yr21d.xtijo5x.x1o0tod.x10l6tqk.x13vifvy.xh8yej3.xl1xv1r'); } if (!currentImg) { currentImg = divElement.querySelector('img.xz74otr.x15mokao.x1ga7v0g.x16uus16.xbiv7yw.x1ey2m1c.x5yr21d.xtijo5x.x1o0tod.x10l6tqk.x13vifvy.xh8yej3'); } if (!currentImg) { currentImg = divElement.querySelector('img.x15mokao.x1ga7v0g.x16uus16.xbiv7yw.x1bwycvy.x193iq5w.x4fas0m.x19kjcj4'); } if (!currentImg) { currentImg = divElement.querySelector('img.xz74otr.x16uus16.xbiv7yw.xjbqb8w.xu25z0z.x1fmog5m.x1o0tod.x10l6tqk.xwa60dl.x1cb1t30.xh8yej3.x1ja2u2z'); } if (currentImg) { await downloadImage(currentImg, divElement); // Visual feedback const originalBg = iconContainer.style.background; iconContainer.style.background = 'rgba(0, 128, 0, 0.8)'; setTimeout(() => { iconContainer.style.background = originalBg; }, 1000); } else { // Check if this might be a video by looking for video elements const hasVideo = divElement.querySelector('video') !== null; const hasVideoIcon = divElement.querySelector('[aria-label*="video" i]') !== null; if (hasVideo || hasVideoIcon) { logToTerminal('Video content detected - cannot download videos as images', 'warning'); showNotification('📹 This appears to be a video, not an image!\nVideos cannot be downloaded with this tool.', 'warning', 4000); } else { logToTerminal('No current image found for download - content may be a video or unsupported format', 'error'); showNotification('🚫 No image found!\nThis might be a video or unsupported content.', 'error', 4000); } } }); divElement.appendChild(iconContainer); addedIcons.add(divElement); return true; } // Function to wait for a specified time function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // Function to scroll to load more content async function scrollToLoadMore() { const initialHeight = document.body.scrollHeight; window.scrollTo(0, document.body.scrollHeight); // Wait for potential new content to load await delay(3000); const newHeight = document.body.scrollHeight; return newHeight > initialHeight; } // Function to collect and download new images from current viewport async function collectAndDownloadNewImages(downloadedUrls) { const allDivs = [ ...document.querySelectorAll('div.xh8yej3'), ...document.querySelectorAll('div.x1qjc9v5.x1q0q8m5.x1qhh985.x18b5jzi.x10w94by.x1t7ytsu.x14e42zd.x13fuv20.x972fbf.x1ey2m1c.x9f619.x78zum5.xdt5ytf.x1iyjqo2.xs83m0k.xtijo5x.x1o0tod.x1qughib.xat24cr.x14z9mp.x1lziwak.xdj266r.x2lwn1j.xeuugli.x18d9i69.xyri2b.x1c1uobl.xexx8yu.x10l6tqk.x13vifvy.x1ja2u2z'), ...document.querySelectorAll('div.x10l6tqk.x13vifvy'), ...document.querySelectorAll('div.x6s0dn4.x78zum5.xdt5ytf.xl56j7k.x1n2onr6'), ...document.querySelectorAll('div.xdj266r.x14z9mp.xat24cr.x1lziwak.xexx8yu.xyri2b.x18d9i69.x1c1uobl.x18d0r48.x1ey2m1c.xtijo5x.x1o0tod.x10l6tqk.x13vifvy.xl8spv7.xt2wqj3') ]; let newImagesDownloaded = 0; logToTerminal(`Scanning ${allDivs.length} posts for new images`, 'progress'); for (const div of allDivs) { // Try to find image with any of the supported selectors let img = div.querySelector('img.x1rg5ohu.x5yr21d.xl1xv1r.xh8yej3') || div.querySelector('img.xz74otr.x1ey2m1c.x9f619.x5yr21d.xtijo5x.x1o0tod.x10l6tqk.x13vifvy.xh8yej3') || div.querySelector('img.x15mokao.x1ga7v0g.x16uus16.xbiv7yw.x1ey2m1c.x5yr21d.xtijo5x.x1o0tod.x10l6tqk.x13vifvy.xh8yej3.xl1xv1r') || div.querySelector('img.xz74otr.x15mokao.x1ga7v0g.x16uus16.xbiv7yw.x1ey2m1c.x5yr21d.xtijo5x.x1o0tod.x10l6tqk.x13vifvy.xh8yej3') || div.querySelector('img.x15mokao.x1ga7v0g.x16uus16.xbiv7yw.x1bwycvy.x193iq5w.x4fas0m.x19kjcj4') || div.querySelector('img.xz74otr.x16uus16.xbiv7yw.xjbqb8w.xu25z0z.x1fmog5m.x1o0tod.x10l6tqk.xwa60dl.x1cb1t30.xh8yej3.x1ja2u2z'); if (img) { const imageUrl = extractImageUrl(img); if (imageUrl && !downloadedUrls.has(imageUrl)) { downloadedUrls.add(imageUrl); const altText = img.alt || img.getAttribute('alt') || ''; const imageType = getImageType(img); const postDate = extractPostDate(div); downloadCount++; const filename = generateFilename(altText, downloadCount, imageType, postDate); try { logToTerminal(`Downloading: ${filename}`, 'progress'); GM_download(imageUrl, filename); newImagesDownloaded++; progressDiv.textContent = `Downloaded ${downloadCount} images - ${filename.substring(0, 50)}...`; await delay(300); } catch (error) { logToTerminal(`Failed to download: ${filename} - ${error.message}`, 'error'); console.error('Failed to download image:', error); } } } } if (newImagesDownloaded > 0) { logToTerminal(`Downloaded ${newImagesDownloaded} new images from current viewport`, 'success'); } return newImagesDownloaded; } // Function to scroll and download images simultaneously async function scrollAndDownloadImages() { const downloadedUrls = new Set(); let hasMoreContent = true; let noNewImagesCount = 0; let noNewContentCount = 0; let scrollAttempts = 0; logToTerminal('Starting bulk download process', 'info'); logToTerminal('Collecting images from initial viewport', 'progress'); // Download images from the initial viewport await collectAndDownloadNewImages(downloadedUrls); while (hasMoreContent && noNewImagesCount < 5 && noNewContentCount < 5) { const beforeHeight = document.body.scrollHeight; const beforeDownloadCount = downloadCount; // Scroll down logToTerminal(`Scrolling down (attempt ${scrollAttempts + 1})`, 'progress'); window.scrollTo(0, document.body.scrollHeight); scrollAttempts++; // Wait for content to load await delay(2000); // Download new images that appeared const newImages = await collectAndDownloadNewImages(downloadedUrls); const afterHeight = document.body.scrollHeight; const afterDownloadCount = downloadCount; // Check if we found new content or images if (afterHeight > beforeHeight) { noNewContentCount = 0; // Reset counter if new content appeared logToTerminal(`New content loaded, page height: ${afterHeight}px`, 'info'); } else { noNewContentCount++; logToTerminal(`No new content found (${noNewContentCount}/5)`, 'warning'); } if (afterDownloadCount > beforeDownloadCount) { noNewImagesCount = 0; // Reset counter if new images were downloaded } else { noNewImagesCount++; logToTerminal(`No new images found (${noNewImagesCount}/5)`, 'warning'); } progressDiv.textContent = `Scrolling and downloading... Found ${downloadCount} images (attempt ${scrollAttempts})`; // Check if we've reached the end if (window.innerHeight + window.scrollY >= document.body.scrollHeight - 100) { if (noNewContentCount >= 3 && noNewImagesCount >= 3) { hasMoreContent = false; logToTerminal('Reached end of content', 'info'); } } } // Final attempt to collect any remaining images logToTerminal('Performing final scan for any remaining images', 'progress'); await delay(2000); await collectAndDownloadNewImages(downloadedUrls); logToTerminal(`Bulk download completed! Total attempts: ${scrollAttempts}`, 'success'); return { totalDownloaded: downloadCount, scrollAttempts }; } // Function to find and download all images async function findAndDownloadAllImages() { if (isDownloading) { logToTerminal('Download already in progress!', 'warning'); alert('Download already in progress!'); return; } isDownloading = true; downloadCount = 0; // Reset counter progressDiv.style.display = 'block'; progressDiv.textContent = 'Starting simultaneous scroll and download...'; // Clear terminal and show it if not visible clearTerminal(); if (!terminalVisible) { toggleTerminal(); } logToTerminal('=== Starting Facebook Image Download Session ===', 'info'); logToTerminal('Initializing bulk download process...', 'progress'); try { // Scroll and download images simultaneously const { totalDownloaded, scrollAttempts } = await scrollAndDownloadImages(); const successMessage = `Download complete! Successfully downloaded ${totalDownloaded} images after ${scrollAttempts} scroll attempts.`; progressDiv.textContent = successMessage; logToTerminal(`=== Download Session Complete ===`, 'success'); logToTerminal(`Total images downloaded: ${totalDownloaded}`, 'success'); logToTerminal(`Scroll attempts: ${scrollAttempts}`, 'info'); if (totalDownloaded === 0) { const noImagesMessage = 'No images found with the specified classes. Make sure you are on a Facebook page with images.'; progressDiv.textContent = noImagesMessage; logToTerminal(noImagesMessage, 'warning'); } await delay(5000); progressDiv.style.display = 'none'; } catch (error) { const errorMessage = `Error occurred during download process: ${error.message}`; console.error('Error during download process:', error); progressDiv.textContent = 'Error occurred during download process'; logToTerminal(`=== Download Session Failed ===`, 'error'); logToTerminal(errorMessage, 'error'); await delay(3000); progressDiv.style.display = 'none'; } isDownloading = false; } // Function to scan for target divs with priority system function scanForImages() { // First, find all post containers const postContainers = document.querySelectorAll('div.x1n2onr6.x1ja2u2z.x1jx94hy.xw5cjc7.x1dmpuos.x1vsv7so.xau1kf4.x9f619.xh8yej3.x6ikm8r.x10wlt62.xquyuld'); let newIconsAdded = 0; postContainers.forEach(postContainer => { let hasAddedIcon = false; // Priority 1: Check for xh8yej3 div first const priority1Div = postContainer.querySelector('div.xh8yej3'); if (priority1Div) { const priority1Img = priority1Div.querySelector('img.x1rg5ohu.x5yr21d.xl1xv1r.xh8yej3'); if (priority1Img && addDownloadIcon(priority1Div)) { newIconsAdded++; hasAddedIcon = true; return; // Skip other divs in this post container } } // Priority 2: Check for the new long div class const priority2Div = postContainer.querySelector('div.x1qjc9v5.x1q0q8m5.x1qhh985.x18b5jzi.x10w94by.x1t7ytsu.x14e42zd.x13fuv20.x972fbf.x1ey2m1c.x9f619.x78zum5.xdt5ytf.x1iyjqo2.xs83m0k.xtijo5x.x1o0tod.x1qughib.xat24cr.x14z9mp.x1lziwak.xdj266r.x2lwn1j.xeuugli.x18d9i69.xyri2b.x1c1uobl.xexx8yu.x10l6tqk.x13vifvy.x1ja2u2z'); if (priority2Div && !hasAddedIcon) { const priority2Img = priority2Div.querySelector('img.xz74otr.x1ey2m1c.x9f619.x5yr21d.xtijo5x.x1o0tod.x10l6tqk.x13vifvy.xh8yej3'); if (priority2Img && addDownloadIcon(priority2Div)) { newIconsAdded++; hasAddedIcon = true; return; // Skip other divs in this post container } } // Priority 3: Check for regular divs (x10l6tqk x13vifvy) - handle differently for multiple photos const priority3Divs = postContainer.querySelectorAll('div.x10l6tqk.x13vifvy'); priority3Divs.forEach(div => { // Check if this div has multiple photos image const multiplePhotosImg = div.querySelector('img.xz74otr.x15mokao.x1ga7v0g.x16uus16.xbiv7yw.x1ey2m1c.x5yr21d.xtijo5x.x1o0tod.x10l6tqk.x13vifvy.xh8yej3'); if (multiplePhotosImg) { // For multiple photos, add icon to each individual div if (addDownloadIcon(div)) { newIconsAdded++; } } else if (!hasAddedIcon) { // For single post photos, use priority system const priority3Img = div.querySelector('img.x15mokao.x1ga7v0g.x16uus16.xbiv7yw.x1ey2m1c.x5yr21d.xtijo5x.x1o0tod.x10l6tqk.x13vifvy.xh8yej3.xl1xv1r'); if (priority3Img && addDownloadIcon(div)) { newIconsAdded++; hasAddedIcon = true; } } }); }); // Also scan for standalone divs not inside post containers (fallback) const standaloneDivs1 = document.querySelectorAll('div.xh8yej3:not(.xdj266r .xh8yej3)'); const standaloneDivs2 = document.querySelectorAll('div.x1qjc9v5.x1q0q8m5.x1qhh985.x18b5jzi.x10w94by.x1t7ytsu.x14e42zd.x13fuv20.x972fbf.x1ey2m1c.x9f619.x78zum5.xdt5ytf.x1iyjqo2.xs83m0k.xtijo5x.x1o0tod.x1qughib.xat24cr.x14z9mp.x1lziwak.xdj266r.x2lwn1j.xeuugli.x18d9i69.xyri2b.x1c1uobl.xexx8yu.x10l6tqk.x13vifvy.x1ja2u2z:not(.xdj266r .x1qjc9v5)'); const standaloneDivs3 = document.querySelectorAll('div.x10l6tqk.x13vifvy:not(.xdj266r .x10l6tqk.x13vifvy)'); const standaloneDivs4 = document.querySelectorAll('div.x6s0dn4.x78zum5.xdt5ytf.xl56j7k.x1n2onr6'); const standaloneDivs5 = document.querySelectorAll('div.xdj266r.x14z9mp.xat24cr.x1lziwak.xexx8yu.xyri2b.x18d9i69.x1c1uobl.x18d0r48.x1ey2m1c.xtijo5x.x1o0tod.x10l6tqk.x13vifvy.xl8spv7.xt2wqj3'); [...standaloneDivs1, ...standaloneDivs2, ...standaloneDivs3, ...standaloneDivs4, ...standaloneDivs5].forEach(div => { if (addDownloadIcon(div)) { newIconsAdded++; } }); if (newIconsAdded > 0) { logToTerminal(`Added ${newIconsAdded} download icons to new posts`, 'info'); console.log(`Added ${newIconsAdded} download icons to Facebook posts`); } } // Start monitoring function startMonitoring() { logToTerminal('Facebook Simple Image Downloader script loaded successfully', 'success'); logToTerminal('Individual download icons will appear on hover', 'info'); logToTerminal('Click the Download button or press Ctrl+Shift+D for bulk download', 'info'); logToTerminal('Press Ctrl+` to toggle this terminal', 'info'); console.log('Facebook Simple Image Downloader loaded'); // Initial scan setTimeout(scanForImages, 2000); // Set up observer for new content const observer = new MutationObserver(() => { setTimeout(scanForImages, 1000); }); observer.observe(document.body, { childList: true, subtree: true }); // Periodic scan setInterval(scanForImages, 5000); } // Add click event to bulk download button downloadBtn.addEventListener('click', function(e) { // Only trigger download if the button itself (or its text) is clicked, not child elements if (e.target === downloadBtn || e.target.tagName === undefined) { findAndDownloadAllImages(); } }); // Add keyboard shortcuts document.addEventListener('keydown', function(e) { // Ctrl + Shift + D for bulk download if (e.ctrlKey && e.shiftKey && e.key === 'D') { e.preventDefault(); findAndDownloadAllImages(); } // Ctrl + ` for terminal toggle else if (e.ctrlKey && e.key === '`') { e.preventDefault(); toggleTerminal(); } }); // Wait for page to load if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', startMonitoring); } else { startMonitoring(); } console.log('Facebook Image Downloader loaded. Individual download icons will appear on hover. Click the "Download All Images" button or press Ctrl+Shift+D to start bulk downloading.'); })();