/** * Image Scraper Module */ let currentJobId = null; let currentImages = []; let imageStepIndicator = null; function initImageScraper() { const elements = { grid: document.getElementById('imgGrid'), counter: document.getElementById('img_counter'), toolbar: document.getElementById('imageToolbar'), skeleton: document.getElementById('imageSkeleton'), btnScrape: document.getElementById('btnScrape'), btnSelectAll: document.getElementById('btnSelectAll'), btnSelectNone: document.getElementById('btnSelectNone'), btnZip: document.getElementById('btnZip'), btnPdf: document.getElementById('btnPdf'), zipName: document.getElementById('zip_name'), pdfName: document.getElementById('pdf_name'), pageUrl: document.getElementById('page_url'), progress: document.getElementById('imgProgress') }; // Initialize step indicator imageStepIndicator = new StepIndicator('imageStepIndicator', [ 'Enter URL', 'Fetch Images', 'Select', 'Download' ]); // Show empty state initially showImageGridEmpty(elements.grid); // Event listeners elements.btnScrape.addEventListener('click', () => handleScrapeImages(elements)); elements.btnSelectAll.addEventListener('click', () => selectAllImages(elements)); elements.btnSelectNone.addEventListener('click', () => clearImageSelection(elements)); elements.btnZip.addEventListener('click', () => { let name = elements.zipName.value.trim() || 'images.zip'; if (!name.toLowerCase().endsWith('.zip')) name += '.zip'; handleDownload('/api/download-zip', 'zip_name', name, elements); }); elements.btnPdf.addEventListener('click', () => { let name = elements.pdfName.value.trim() || 'images.pdf'; if (!name.toLowerCase().endsWith('.pdf')) name += '.pdf'; handleDownload('/api/download-pdf', 'pdf_name', name, elements); }); // URL input - update step elements.pageUrl.addEventListener('input', () => { if (elements.pageUrl.value.trim()) { imageStepIndicator.setStep(1); } clearFieldError(elements.pageUrl.closest('.form-group')); }); // URL validation elements.pageUrl.addEventListener('blur', () => { const value = elements.pageUrl.value.trim(); if (value) validateUrl(elements.pageUrl); }); // Enter to scrape elements.pageUrl.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); elements.btnScrape.click(); } }); // Keyboard shortcuts registerShortcut('ctrl+a', () => { if (document.getElementById('page-images').classList.contains('active') && currentImages.length > 0) { selectAllImages(elements); } }); registerShortcut('ctrl+d', () => { if (document.getElementById('page-images').classList.contains('active') && currentImages.length > 0) { clearImageSelection(elements); } }); } function getSelectedIds(grid) { const ids = []; grid.querySelectorAll('input[type="checkbox"]').forEach(cb => { if (cb.checked) ids.push(cb.dataset.id); }); return ids; } function updateCounter(elements) { const total = currentImages.length; const selected = getSelectedIds(elements.grid).length; elements.counter.textContent = `${total} images • ${selected} selected`; // Update step indicator based on selection if (selected > 0 && imageStepIndicator) { imageStepIndicator.setStep(3); // Ready to download } } function setImageButtonsEnabled(elements, enabled) { elements.btnSelectAll.disabled = !enabled; elements.btnSelectNone.disabled = !enabled; elements.btnZip.disabled = !enabled; elements.btnPdf.disabled = !enabled; } function renderImages(images, elements) { elements.grid.innerHTML = ''; images.forEach(img => { const card = createImageCard(img, elements); elements.grid.appendChild(card); }); updateCounter(elements); } function createImageCard(img, elements) { const card = document.createElement('div'); card.className = 'image-card'; // Preview button const previewBtn = document.createElement('button'); previewBtn.className = 'image-preview-btn'; previewBtn.innerHTML = ``; previewBtn.addEventListener('click', (e) => { e.stopPropagation(); const dims = (img.width && img.height) ? `${img.width}×${img.height}` : null; const size = img.bytes ? formatFileSize(img.bytes) : null; showImageModal(img.url, img.filename, dims, size); }); // Thumbnail const thumb = document.createElement('img'); thumb.className = 'image-thumb'; thumb.src = img.url; thumb.loading = 'lazy'; thumb.referrerPolicy = 'no-referrer'; thumb.alt = img.filename; // Meta const meta = document.createElement('div'); meta.className = 'image-meta'; const filename = document.createElement('div'); filename.className = 'image-filename'; filename.title = img.filename; filename.textContent = img.filename; const info = document.createElement('div'); info.className = 'image-info'; const sizeBadge = document.createElement('span'); sizeBadge.className = 'image-badge'; sizeBadge.textContent = (img.width && img.height) ? `${img.width}×${img.height}` : '?'; const checkboxContainer = document.createElement('div'); checkboxContainer.className = 'image-checkbox'; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.dataset.id = img.id; checkbox.addEventListener('click', e => e.stopPropagation()); checkbox.addEventListener('change', () => { card.classList.toggle('selected', checkbox.checked); updateCounter(elements); }); checkboxContainer.appendChild(checkbox); checkboxContainer.appendChild(document.createTextNode('Select')); info.appendChild(sizeBadge); info.appendChild(checkboxContainer); meta.appendChild(filename); meta.appendChild(info); card.appendChild(previewBtn); card.appendChild(thumb); card.appendChild(meta); card.addEventListener('click', () => { checkbox.checked = !checkbox.checked; card.classList.toggle('selected', checkbox.checked); updateCounter(elements); }); return card; } async function handleScrapeImages(elements) { const pageUrl = elements.pageUrl.value.trim(); if (!pageUrl) { setFieldError(elements.pageUrl.closest('.form-group'), 'URL required'); showToast('URL Required', 'Enter a webpage URL', 'error'); return; } if (!validateUrl(elements.pageUrl)) { showToast('Invalid URL', 'Enter a valid URL', 'error'); return; } setButtonLoading(elements.btnScrape, true, 'Scraping...'); setImageButtonsEnabled(elements, false); elements.toolbar.style.display = 'none'; elements.skeleton.style.display = 'grid'; elements.grid.innerHTML = ''; currentJobId = null; currentImages = []; showProgress(elements.progress, true, true); imageStepIndicator.setStep(1); // Fetching try { const fd = new FormData(); fd.append('page_url', pageUrl); const res = await fetch('/api/scrape-images', { method: 'POST', body: fd }); if (!res.ok) { let err = { detail: 'Request failed' }; try { err = await res.json(); } catch {} throw new Error(err.detail || 'Request failed'); } const data = await res.json(); currentJobId = data.job_id; currentImages = data.images || []; elements.skeleton.style.display = 'none'; if (currentImages.length > 0) { renderImages(currentImages, elements); elements.toolbar.style.display = 'flex'; setImageButtonsEnabled(elements, true); showToast('Scrape Complete', `Found ${currentImages.length} images`, 'success'); imageStepIndicator.setStep(2); // Select step } else { showImageGridEmpty(elements.grid); showToast('No Images', 'No suitable images found on this page', 'info'); imageStepIndicator.setStep(0); } } catch (e) { elements.skeleton.style.display = 'none'; // Show error state with retry action showErrorState(elements.grid, 'Scraping Failed', e.message || 'Could not fetch images from this URL. The page might be blocking requests or require authentication.', [ { id: 'retry', label: 'Try Again', primary: true, handler: () => handleScrapeImages(elements) }, { id: 'clear', label: 'Clear', handler: () => { elements.pageUrl.value = ''; showImageGridEmpty(elements.grid); imageStepIndicator.reset(); }} ] ); imageStepIndicator.setStep(0); } finally { setButtonLoading(elements.btnScrape, false); showProgress(elements.progress, false); } } function selectAllImages(elements) { elements.grid.querySelectorAll('input[type="checkbox"]').forEach(cb => { cb.checked = true; cb.closest('.image-card').classList.add('selected'); }); updateCounter(elements); showToast('Selected All', `${currentImages.length} images`, 'success'); imageStepIndicator.setStep(3); // Ready to download } function clearImageSelection(elements) { elements.grid.querySelectorAll('input[type="checkbox"]').forEach(cb => { cb.checked = false; cb.closest('.image-card').classList.remove('selected'); }); updateCounter(elements); showToast('Cleared', 'Selection cleared', 'info'); imageStepIndicator.setStep(2); // Back to select } async function handleDownload(endpoint, nameField, nameValue, elements) { if (!currentJobId) { showToast('No Images', 'Fetch images first', 'error'); return; } const ids = getSelectedIds(elements.grid); if (ids.length === 0) { showToast('No Selection', 'Select at least one image', 'error'); return; } const isZip = nameField === 'zip_name'; const btn = isZip ? elements.btnZip : elements.btnPdf; setButtonLoading(btn, true, 'Preparing...'); showProgress(elements.progress, true, true); try { const fd = new FormData(); fd.append('job_id', currentJobId); fd.append('image_ids', ids.join(',')); fd.append(nameField, nameValue); const res = await fetch(endpoint, { method: 'POST', body: fd }); if (!res.ok) { let err = { detail: 'Request failed' }; try { err = await res.json(); } catch {} throw new Error(err.detail || 'Request failed'); } const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = nameValue; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); showToast('Downloaded', `${ids.length} images as ${nameValue}`, 'success'); imageStepIndicator.complete(); // Show success animation showSuccessAnimation('Download Complete!', `${ids.length} images saved as ${nameValue}`); } catch (e) { showToast('Download Failed', e.message, 'error'); imageStepIndicator.setStep(2); } finally { setButtonLoading(btn, false); showProgress(elements.progress, false); updateCounter(elements); } }