anycoder-128b6083 / index.html
Galaxydude2's picture
Upload folder using huggingface_hub
17dc24a verified
<!DOCTYPE html>
<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>