| |
| |
| |
|
|
| 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') |
| }; |
|
|
| |
| imageStepIndicator = new StepIndicator('imageStepIndicator', [ |
| 'Enter URL', |
| 'Fetch Images', |
| 'Select', |
| 'Download' |
| ]); |
|
|
| |
| showImageGridEmpty(elements.grid); |
|
|
| |
| 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); |
| }); |
|
|
| |
| elements.pageUrl.addEventListener('input', () => { |
| if (elements.pageUrl.value.trim()) { |
| imageStepIndicator.setStep(1); |
| } |
| clearFieldError(elements.pageUrl.closest('.form-group')); |
| }); |
|
|
| |
| elements.pageUrl.addEventListener('blur', () => { |
| const value = elements.pageUrl.value.trim(); |
| if (value) validateUrl(elements.pageUrl); |
| }); |
|
|
| |
| elements.pageUrl.addEventListener('keydown', (e) => { |
| if (e.key === 'Enter') { |
| e.preventDefault(); |
| elements.btnScrape.click(); |
| } |
| }); |
|
|
| |
| 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`; |
| |
| |
| if (selected > 0 && imageStepIndicator) { |
| imageStepIndicator.setStep(3); |
| } |
| } |
|
|
| 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'; |
|
|
| |
| const previewBtn = document.createElement('button'); |
| previewBtn.className = 'image-preview-btn'; |
| previewBtn.innerHTML = `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v3m0 0v3m0-3h3m-3 0H7"/></svg>`; |
| 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); |
| }); |
|
|
| |
| const thumb = document.createElement('img'); |
| thumb.className = 'image-thumb'; |
| thumb.src = img.url; |
| thumb.loading = 'lazy'; |
| thumb.referrerPolicy = 'no-referrer'; |
| thumb.alt = img.filename; |
|
|
| |
| 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); |
|
|
| 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); |
| } 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'; |
| |
| |
| 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); |
| } |
|
|
| 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); |
| } |
|
|
| 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(); |
| |
| |
| 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); |
| } |
| } |
|
|