Spaces:
Running
Running
| <html lang="de"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Web Image Downloader</title> | |
| <!-- Load Modern CSS & Icons --> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap" rel="stylesheet"> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <!-- Load JSZip for zipping files --> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script> | |
| <style> | |
| :root { | |
| --primary-color: #6366f1; | |
| --primary-hover: #4f46e5; | |
| --bg-color: #0f172a; | |
| --card-bg: #1e293b; | |
| --text-main: #f1f5f9; | |
| --text-muted: #94a3b8; | |
| --border-color: #334155; | |
| --success-color: #10b981; | |
| --danger-color: #ef4444; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background-color: var(--bg-color); | |
| color: var(--text-main); | |
| line-height: 1.6; | |
| display: flex; | |
| flex-direction: column; | |
| min-height: 100vh; | |
| } | |
| /* Header Section */ | |
| header { | |
| background-color: var(--card-bg); | |
| border-bottom: 1px solid var(--border-color); | |
| padding: 1.5rem 2rem; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| } | |
| .brand { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| font-size: 1.5rem; | |
| font-weight: 700; | |
| color: var(--text-main); | |
| } | |
| .brand i { color: var(--primary-color); } | |
| .footer-link { | |
| font-size: 0.85rem; | |
| color: var(--text-muted); | |
| text-decoration: none; | |
| transition: color 0.2s; | |
| } | |
| .footer-link:hover { | |
| color: var(--primary-color); | |
| } | |
| /* Main Container */ | |
| main { | |
| flex: 1; | |
| max-width: 900px; | |
| width: 100%; | |
| margin: 0 auto; | |
| padding: 2rem; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 2rem; | |
| } | |
| /* Input Section */ | |
| .input-group { | |
| background: var(--card-bg); | |
| padding: 2rem; | |
| border-radius: 16px; | |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3); | |
| border: 1px solid var(--border-color); | |
| text-align: center; | |
| } | |
| h1 { | |
| margin-bottom: 1rem; | |
| font-size: 1.8rem; | |
| font-weight: 600; | |
| } | |
| p.subtitle { | |
| color: var(--text-muted); | |
| margin-bottom: 2rem; | |
| } | |
| .url-input-wrapper { | |
| display: flex; | |
| gap: 10px; | |
| max-width: 600px; | |
| margin: 0 auto; | |
| flex-wrap: wrap; | |
| } | |
| input[type="text"] { | |
| flex: 1; | |
| min-width: 250px; | |
| padding: 12px 16px; | |
| border-radius: 8px; | |
| border: 1px solid var(--border-color); | |
| background-color: var(--bg-color); | |
| color: var(--text-main); | |
| font-size: 1rem; | |
| outline: none; | |
| transition: border-color 0.3s; | |
| } | |
| input[type="text"]:focus { | |
| border-color: var(--primary-color); | |
| } | |
| button { | |
| padding: 12px 24px; | |
| border-radius: 8px; | |
| border: none; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .btn-primary { | |
| background-color: var(--primary-color); | |
| color: white; | |
| } | |
| .btn-primary:hover { | |
| background-color: var(--primary-hover); | |
| transform: translateY(-1px); | |
| } | |
| .btn-primary:disabled { | |
| opacity: 0.6; | |
| cursor: not-allowed; | |
| transform: none; | |
| } | |
| .btn-success { | |
| background-color: var(--success-color); | |
| color: white; | |
| width: 100%; | |
| justify-content: center; | |
| margin-top: 1rem; | |
| font-size: 1.1rem; | |
| } | |
| .btn-success:hover { | |
| background-color: #059669; | |
| } | |
| .btn-success:disabled { | |
| background-color: #334155; | |
| cursor: not-allowed; | |
| } | |
| /* Results Area */ | |
| .results-container { | |
| display: none; /* Hidden by default */ | |
| flex-direction: column; | |
| gap: 1.5rem; | |
| } | |
| .stats-bar { | |
| display: flex; | |
| justify-content: space-between; | |
| background: var(--bg-color); | |
| padding: 1rem; | |
| border-radius: 8px; | |
| border: 1px solid var(--border-color); | |
| font-size: 0.9rem; | |
| } | |
| .stat-item span { | |
| font-weight: 700; | |
| color: var(--primary-color); | |
| } | |
| .gallery { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); | |
| gap: 1.5rem; | |
| } | |
| .image-card { | |
| background: var(--card-bg); | |
| border-radius: 12px; | |
| overflow: hidden; | |
| transition: transform 0.2s; | |
| border: 1px solid var(--border-color); | |
| position: relative; | |
| } | |
| .image-card:hover { | |
| transform: translateY(-5px); | |
| box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.5); | |
| } | |
| .image-card img { | |
| width: 100%; | |
| height: 200px; | |
| object-fit: cover; | |
| display: block; | |
| transition: opacity 0.3s; | |
| } | |
| .image-card img.lazy-load { | |
| opacity: 0; | |
| } | |
| .image-card img.loaded { | |
| opacity: 1; | |
| } | |
| .card-info { | |
| padding: 12px; | |
| } | |
| .card-info h3 { | |
| font-size: 0.95rem; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| margin-bottom: 8px; | |
| } | |
| .card-actions { | |
| display: flex; | |
| gap: 8px; | |
| } | |
| .btn-icon { | |
| flex: 1; | |
| padding: 8px; | |
| font-size: 0.8rem; | |
| justify-content: center; | |
| background-color: var(--bg-color); | |
| color: var(--text-muted); | |
| border: 1px solid var(--border-color); | |
| } | |
| .btn-icon:hover { | |
| background-color: var(--border-color); | |
| color: var(--text-main); | |
| } | |
| .loader { | |
| text-align: center; | |
| padding: 3rem; | |
| color: var(--text-muted); | |
| display: none; | |
| } | |
| .spinner { | |
| border: 4px solid rgba(255,255,255,0.1); | |
| width: 36px; | |
| height: 36px; | |
| border-radius: 50%; | |
| border-left-color: var(--primary-color); | |
| animation: spin 1s linear infinite; | |
| margin: 0 auto 1rem auto; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .empty-state { | |
| text-align: center; | |
| padding: 3rem; | |
| color: var(--text-muted); | |
| border: 2px dashed var(--border-color); | |
| border-radius: 16px; | |
| } | |
| .error-msg { | |
| color: var(--danger-color); | |
| background: rgba(239, 68, 68, 0.1); | |
| padding: 1rem; | |
| border-radius: 8px; | |
| text-align: center; | |
| display: none; | |
| border: 1px solid var(--danger-color); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="brand"> | |
| <i class="fa-solid fa-image"></i> | |
| <span>ImageScraper Pro</span> | |
| </div> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="footer-link"> | |
| Built with anycoder | |
| </a> | |
| </header> | |
| <main> | |
| <!-- Input Section --> | |
| <section class="input-group"> | |
| <h1>Webseiten Bilder Downloader</h1> | |
| <p class="subtitle">Geben Sie eine URL ein, um alle darin enthaltenen Bilder automatisch zu finden und herunterzuladen.</p> | |
| <div class="url-input-wrapper"> | |
| <input type="text" id="urlInput" placeholder="https://www.beispiel.de/galerie" /> | |
| <button id="scanBtn" class="btn-primary"> | |
| <i class="fa-solid fa-magnifying-glass"></i> Scan & Finden | |
| </button> | |
| </div> | |
| <div id="errorMsg" class="error-msg"></div> | |
| </section> | |
| <!-- Loading State --> | |
| <div id="loader" class="loader"> | |
| <div class="spinner"></div> | |
| <p>Analysiere Seite und lade Bilder...</p> | |
| </div> | |
| <!-- Results Section --> | |
| <section id="resultsSection" class="results-container"> | |
| <div class="stats-bar"> | |
| <div class="stat-item">Gefundene Bilder: <span id="totalCount">0</span></div> | |
| <div class="stat-item">Geladen: <span id="loadedCount">0</span></div> | |
| <div class="stat-item">Gesamtgröße: <span id="totalSize">0 MB</span></div> | |
| </div> | |
| <div id="gallery" class="gallery"> | |
| <!-- Image Cards will be injected here --> | |
| </div> | |
| <div id="emptyState" class="empty-state"> | |
| <i class="fa-solid fa-image" style="font-size: 3rem; margin-bottom: 1rem; opacity: 0.5;"></i> | |
| <p>Keine Bilder gefunden. Stellen Sie sicher, dass die URL gültig ist und Bilder enthält.</p> | |
| </div> | |
| <button id="downloadAllBtn" class="btn-success" disabled> | |
| <i class="fa-solid fa-file-archive"></i> Alle Bilder als ZIP herunterladen | |
| </button> | |
| </section> | |
| </main> | |
| <script> | |
| // --- Configuration & State --- | |
| const state = { | |
| images: [], // Stores { src, name, blob, size, status } | |
| isScanning: false, | |
| zip: null | |
| }; | |
| // --- DOM Elements --- | |
| const urlInput = document.getElementById('urlInput'); | |
| const scanBtn = document.getElementById('scanBtn'); | |
| const loader = document.getElementById('loader'); | |
| const resultsSection = document.getElementById('resultsSection'); | |
| const gallery = document.getElementById('gallery'); | |
| const emptyState = document.getElementById('emptyState'); | |
| const downloadAllBtn = document.getElementById('downloadAllBtn'); | |
| const errorMsg = document.getElementById('errorMsg'); | |
| const statsTotal = document.getElementById('totalCount'); | |
| const statsLoaded = document.getElementById('loadedCount'); | |
| const statsSize = document.getElementById('totalSize'); | |
| // --- Helper Functions --- | |
| // Format bytes to human readable | |
| function formatBytes(bytes, decimals = 2) { | |
| if (!+bytes) return '0 Bytes'; | |
| const k = 1024; | |
| const dm = decimals < 0 ? 0 : decimals; | |
| const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; | |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); | |
| return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`; | |
| } | |
| // Extract filename from URL | |
| function getFilenameFromUrl(url) { | |
| try { | |
| const urlObj = new URL(url); | |
| const pathParts = urlObj.pathname.split('/'); | |
| let filename = pathParts[pathParts.length - 1]; | |
| if (!filename || filename.includes('.')) { | |
| filename = `image_${Date.now()}.jpg`; // Fallback | |
| } | |
| // Clean filename | |
| return filename.replace(/[^a-z0-9]/gi, '_').toLowerCase(); | |
| } catch (e) { | |
| return `image_${Date.now()}.jpg`; | |
| } | |
| } | |
| // Check if URL is valid | |
| function isValidUrl(string) { | |
| try { | |
| new URL(string); | |
| return true; | |
| } catch (_) { | |
| return false; | |
| } | |
| } | |
| // Get Base URL for relative paths | |
| function getBaseUrl(url) { | |
| const urlObj = new URL(url); | |
| return `${urlObj.protocol}//${urlObj.hostname}`; | |
| } | |
| // --- Core Logic --- | |
| async function fetchImagesFromUrl(targetUrl) { | |
| // Reset UI | |
| state.images = []; | |
| gallery.innerHTML = ''; | |
| errorMsg.style.display = 'none'; | |
| downloadAllBtn.disabled = true; | |
| resultsSection.style.display = 'flex'; | |
| emptyState.style.display = 'block'; | |
| statsTotal.innerText = '0'; | |
| statsLoaded.innerText = '0'; | |
| statsSize.innerText = '0 MB'; | |
| // Browser Security Check (CORS) | |
| // We cannot directly fetch HTML of another domain via JS Fetch due to CORS. | |
| // We will use a Proxy or the Hugging Face Text-Embeddings API workaround if needed, | |
| // BUT for a standalone client-side app, we have two options: | |
| // 1. Ask user to enable CORS (Not practical). | |
| // 2. Use a public CORS proxy (Unreliable). | |
| // 4. Use an API (Best for this context). | |
| // For this demo, we will try a direct fetch first. | |
| // If it fails due to CORS, we will suggest using a proxy or a backend. | |
| // *Alternative for Demo:* We will use a trick with "allorigins" to bypass CORS for HTML parsing. | |
| const proxyUrl = `https://api.allorigins.win/get?url=${encodeURIComponent(targetUrl)}`; | |
| try { | |
| loader.style.display = 'block'; | |
| scanBtn.disabled = true; | |
| const response = await fetch(proxyUrl); | |
| if (!response.ok) throw new Error("Netzwerkfehler beim Abrufen der Seite."); | |
| const data = await response.json(); | |
| const htmlContent = data.contents; | |
| // Parse HTML | |
| const parser = new DOMParser(); | |
| const doc = parser.parseFromString(htmlContent, 'text/html'); | |
| // Find all images | |
| const imgElements = doc.querySelectorAll('img'); | |
| const baseUrl = getBaseUrl(targetUrl); | |
| let foundCount = 0; | |
| imgElements.forEach((img, index) => { | |
| let src = img.getAttribute('src') || img.getAttribute('data-src'); | |
| if (!src) return; | |
| // Handle relative URLs | |
| if (src.startsWith('/')) { | |
| src = baseUrl + src; | |
| } | |
| // Filter obvious non-image assets (like icons, spacers) | |
| if (src.includes('.svg') || src.includes('icon') || src.includes('logo')) { | |
| // Optional: Skip logos if desired, but let's keep them for completeness | |
| } | |
| state.images.push({ | |
| id: index, | |
| src: src, | |
| name: getFilenameFromUrl(src), | |
| status: 'pending', | |
| blob: null, | |
| size: 0 | |
| }); | |
| foundCount++; | |
| }); | |
| statsTotal.innerText = foundCount; | |
| if (foundCount === 0) { | |
| emptyState.style.display = 'block'; | |
| loader.style.display = 'none'; | |
| scanBtn.disabled = false; | |
| return; | |
| } | |
| emptyState.style.display = 'none'; | |
| renderGallery(); | |
| await downloadAllImages(); | |
| } catch (error) { | |
| console.error(error); | |
| let message = "Fehler beim Laden der Seite. "; | |
| if (error.message.includes("Failed to fetch")) { | |
| message += "Möglicherweise CORS-Schutz. Versuchen Sie es mit einem Proxy oder stellen Sie sicher, dass die URL öffentlich zugänglich ist."; | |
| } else { | |
| message += error.message; | |
| } | |
| errorMsg.innerText = message; | |
| errorMsg.style.display = 'block'; | |
| } finally { | |
| loader.style.display = 'none'; | |
| scanBtn.disabled = false; | |
| } | |
| } | |
| function renderGallery() { | |
| gallery.innerHTML = ''; | |
| state.images.forEach((img, index) => { | |
| const card = document.createElement('div'); | |
| card.className = 'image-card'; | |
| // Create a placeholder image to avoid broken image icons | |
| const imgTag = document.createElement('img'); | |
| imgTag.src = img.src; | |
| imgTag.alt = img.name; | |
| imgTag.className = 'lazy-load'; | |
| // Track load status | |
| imgTag.onload = () => { | |
| imgTag.classList.remove('lazy-load'); | |
| imgTag.classList.add('loaded'); | |
| updateStats(); | |
| }; | |
| imgTag.onerror = () => { | |
| imgTag.src = 'https://via.placeholder.com/300x200?text=Bild+konnte+nicht+geladen+werden'; | |
| imgTag.classList.remove('lazy-load'); | |
| updateStats(); | |
| }; | |
| const infoDiv = document.createElement('div'); | |
| infoDiv.className = 'card-info'; | |
| const titleH3 = document.createElement('h3'); | |
| titleH3.innerText = img.name; | |
| const actionsDiv = document.createElement('div'); | |
| actionsDiv.className = 'card-actions'; | |
| const downloadBtn = document.createElement('button'); | |
| downloadBtn.className = 'btn-icon'; | |
| downloadBtn.innerHTML = '<i class="fa-solid fa-download"></i> Einzel'; | |
| downloadBtn.onclick = () => downloadSingleImage(img); | |
| const viewBtn = document.createElement('button'); | |
| viewBtn.className = 'btn-icon'; | |
| viewBtn.innerHTML = '<i class="fa-solid fa-external-link-alt"></i> Öffnen'; | |
| viewBtn.onclick = () => window.open(img.src, '_blank'); | |
| actionsDiv.appendChild(downloadBtn); | |
| actionsDiv.appendChild(viewBtn); | |
| infoDiv.appendChild(titleH3); | |
| infoDiv.appendChild(actionsDiv); | |
| card.appendChild(imgTag); | |
| card.appendChild(infoDiv); | |
| gallery.appendChild(card); | |
| }); | |
| } | |
| async function downloadAllImages() { | |
| if (state.images.length === 0) return; | |
| // Initialize ZIP | |
| state.zip = new JSZip(); | |
| let loadedCount = 0; | |
| let totalSize = 0; | |
| const totalImages = state.images.length; | |
| // Download images in batches to prevent browser overload | |
| const batchSize = 5; | |
| for (let i = 0; i < totalImages; i += batchSize) { | |
| const batch = state.images.slice(i, i + batchSize); | |
| await Promise.all(batch.map(async (img) => { | |
| try { | |
| img.status = 'downloading'; | |
| const response = await fetch(img.src, { mode: 'cors' }); // May fail on strict CORS | |
| if (!response.ok) throw new Error('Network response was not ok'); | |
| const blob = await response.blob(); | |
| img.blob = blob; | |
| img.size = blob.size; | |
| img.status = 'completed'; | |
| totalSize += blob.size; | |
| loadedCount++; | |
| // Add to ZIP | |
| state.zip.file(img.name, blob); | |
| // Update UI for this specific item | |
| const card = gallery.children[i]; | |
| if (card) { | |
| const btn = card.querySelector('.btn-icon'); | |
| if(btn) btn.innerHTML = '<i class="fa-solid fa-check-circle" style="color:var(--success-color)"></i> Fertig'; | |
| } | |
| } catch (error) { | |
| console.error(`Failed to download ${img.src}:`, error); | |
| img.status = 'failed'; | |
| loadedCount++; // Count as processed but failed | |
| // Update UI | |
| const card = gallery.children[i]; | |
| if (card) { | |
| const btn = card.querySelector('.btn-icon'); | |
| if(btn) { | |
| btn.innerHTML = '<i class="fa-solid fa-xmark" style="color:var(--danger-color)"></i> Fehler'; | |
| btn.style.color = 'var(--danger-color)'; | |
| } | |
| } | |
| } | |
| updateStats(); | |
| })); | |
| } | |
| // Enable Download All Button | |
| if (loadedCount > 0) { | |
| downloadAllBtn.disabled = false; | |
| downloadAllBtn.innerHTML = `<i class="fa-solid fa-file-archive"></i> ZIP herunterladen (${formatBytes(totalSize)})`; | |
| } | |
| } | |
| function downloadSingleImage(imgData) { | |
| if (imgData.blob) { | |
| const url = window.URL.createObjectURL(imgData.blob); | |
| const a = document.createElement('a'); | |
| a.style.display = 'none'; | |
| a.href = url; | |
| a.download = imgData.name; | |
| document.body.appendChild(a); | |
| a.click(); | |
| window.URL.revokeObjectURL(url); | |
| document.body.removeChild(a); | |
| } else { | |
| // Fallback if blob not available (e.g. CORS error earlier) | |
| window.open(imgData.src, '_blank'); | |
| alert("Das Bild konnte nicht direkt heruntergeladen werden (wahrscheinlich CORS). Es wird in einem neuen Tab geöffnet."); | |
| } | |
| } | |
| function updateStats() { | |
| const loaded = state.images.filter(i => i.status === 'completed').length; | |
| const failed = state.images.filter(i => i.status === 'failed').length; | |
| const total = state.images.length; | |
| statsLoaded.innerText = `${loaded} (${failed > 0 ? failed + ' fehlgeschlagen' : ''})`; | |
| const totalSize = state.images.reduce((acc, curr) => acc + curr.size, 0); | |
| statsSize.innerText = formatBytes(totalSize); | |
| if (loaded === total && total > 0) { | |
| // All done logic | |
| } | |
| } | |
| // --- Event Listeners --- | |
| scanBtn.addEventListener('click', () => { | |
| const url = urlInput.value.trim(); | |
| if (!url) { | |
| alert("Bitte geben Sie eine gültige URL ein."); | |
| return; | |
| } | |
| if (!isValidUrl(url)) { | |
| alert("Ungültige URL. Bitte beginnen Sie mit http:// oder https://"); | |
| return; | |
| } | |
| fetchImagesFromUrl(url); | |
| }); | |
| // Allow Enter key to trigger scan | |
| urlInput.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') { | |
| scanBtn.click(); | |
| } | |
| }); | |
| downloadAllBtn.addEventListener('click', () => { | |
| if (state.zip) { | |
| state.zip.generateAsync({ type: "blob" }).then(function(content) { | |
| saveAs(content, "alle_bilder.zip"); | |
| }); | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> |