Spaces:
Running
Running
| {% extends "base.html" %} | |
| {% block title %}Upload Images{% endblock %} | |
| {% block head %} | |
| <!-- Mobile viewport optimization --> | |
| <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no, maximum-scale=1, viewport-fit=cover"> | |
| <style> | |
| :root { | |
| --app-bg: #181a1c; | |
| --card-bg: #212529; | |
| --border-color: #495057; | |
| --primary-color: #0d6efd; | |
| --primary-bg-subtle: rgba(13, 110, 253, 0.1); | |
| } | |
| body { | |
| background-color: var(--app-bg); | |
| color: #e9ecef; | |
| } | |
| /* Layout wrapper */ | |
| .upload-wrapper { | |
| min-height: calc(100vh - 56px); | |
| min-height: calc(100dvh - 56px); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 1rem; | |
| padding-bottom: env(safe-area-inset-bottom); | |
| } | |
| .main-card { | |
| background-color: var(--card-bg); | |
| border: 1px solid var(--border-color); | |
| border-radius: 16px; | |
| box-shadow: 0 4px 24px rgba(0,0,0,0.4); | |
| width: 100%; | |
| max-width: 600px; | |
| overflow: hidden; | |
| } | |
| /* Touch-friendly Drop Zone */ | |
| .file-drop-area { | |
| position: relative; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| flex-direction: column; | |
| width: 100%; | |
| min-height: 200px; /* Large target area */ | |
| padding: 30px; | |
| border: 2px dashed var(--border-color); | |
| border-radius: 12px; | |
| transition: all 0.2s ease; | |
| background: rgba(255,255,255,0.02); | |
| cursor: pointer; | |
| } | |
| .file-drop-area:active, .file-drop-area.drag-over { | |
| border-color: var(--primary-color); | |
| background: var(--primary-bg-subtle); | |
| transform: scale(0.99); | |
| } | |
| .file-drop-area.has-files { | |
| border-color: #198754; | |
| border-style: solid; | |
| background: rgba(25, 135, 84, 0.1); | |
| } | |
| /* The invisible input covers the whole area */ | |
| .file-input { | |
| position: absolute; | |
| left: 0; | |
| top: 0; | |
| height: 100%; | |
| width: 100%; | |
| opacity: 0; | |
| cursor: pointer; | |
| z-index: 10; | |
| } | |
| /* Icon styling */ | |
| .upload-icon { | |
| font-size: 3rem; | |
| color: #adb5bd; | |
| margin-bottom: 1rem; | |
| transition: color 0.2s; | |
| } | |
| .file-drop-area.has-files .upload-icon { | |
| color: #198754; | |
| } | |
| .file-msg { | |
| font-size: 1.1rem; | |
| font-weight: 500; | |
| color: #dee2e6; | |
| text-align: center; | |
| margin-bottom: 0.5rem; | |
| } | |
| .file-hint { | |
| font-size: 0.85rem; | |
| color: #adb5bd; | |
| } | |
| /* Buttons */ | |
| .btn-action { | |
| min-height: 54px; | |
| font-size: 1.1rem; | |
| font-weight: 600; | |
| border-radius: 12px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 10px; | |
| transition: transform 0.1s; | |
| } | |
| .btn-action:active { | |
| transform: scale(0.98); | |
| } | |
| /* Animations */ | |
| .fade-in-up { animation: fadeInUp 0.4s ease-out forwards; } | |
| @keyframes fadeInUp { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| </style> | |
| {% endblock %} | |
| {% block content %} | |
| <div class="upload-wrapper"> | |
| <div class="main-card fade-in-up"> | |
| <!-- Header --> | |
| <div class="p-4 border-bottom border-secondary" style="border-color: #495057 !important;"> | |
| <div class="text-center"> | |
| <h1 class="h4 mb-1">Upload Images</h1> | |
| <p class="text-secondary small mb-0">Select pages or screenshots to analyze</p> | |
| </div> | |
| </div> | |
| <div class="card-body p-4"> | |
| <!-- Remote Camera Component --> | |
| {% include 'camera_receiver_component.html' %} | |
| <form id="upload-form"> | |
| <!-- Large Drop Zone --> | |
| <div class="mb-4"> | |
| <div class="d-flex justify-content-between align-items-center mb-2"> | |
| <label class="form-label mb-0">Files</label> | |
| <button type="button" class="btn btn-sm btn-outline-info" onclick="toggleCameraReceiver()"> | |
| <i class="bi bi-camera-video me-1"></i> Use Remote Camera | |
| </button> | |
| </div> | |
| <div class="file-drop-area" id="drop-zone"> | |
| <i class="bi bi-images upload-icon" id="drop-icon"></i> | |
| <span class="file-msg" id="file-label-main">Tap to select images</span> | |
| <span class="file-hint" id="file-label-sub">Support for PNG, JPG, JPEG</span> | |
| <!-- Invisible Input --> | |
| <input type="file" class="file-input" id="images-upload" name="images" accept="image/*" multiple required> | |
| </div> | |
| </div> | |
| <!-- Action Button --> | |
| <button type="submit" class="btn btn-primary w-100 btn-action shadow" id="submitBtn"> | |
| <span class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span> | |
| <span class="btn-text">Start Processing</span> | |
| <i class="bi bi-arrow-right-circle btn-icon"></i> | |
| </button> | |
| </form> | |
| <div id="status" class="mt-3"></div> | |
| <!-- Back Link --> | |
| <div class="mt-4 text-center"> | |
| <a href="/" class="text-decoration-none text-secondary d-inline-flex align-items-center py-2 px-3"> | |
| <i class="bi bi-arrow-left me-2"></i> Back to PDF Upload | |
| </a> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {% endblock %} | |
| {% block scripts %} | |
| <script> | |
| // UX Elements | |
| const dropZone = document.getElementById('drop-zone'); | |
| const fileInput = document.getElementById('images-upload'); | |
| const mainLabel = document.getElementById('file-label-main'); | |
| const subLabel = document.getElementById('file-label-sub'); | |
| const icon = document.getElementById('drop-icon'); | |
| const statusDiv = document.getElementById('status'); | |
| // Handle File Selection (Visual Feedback) | |
| fileInput.addEventListener('change', function() { | |
| const count = this.files ? this.files.length : 0; | |
| if (count > 0) { | |
| dropZone.classList.add('has-files'); | |
| icon.className = 'bi bi-check-circle-fill upload-icon'; | |
| if (count === 1) { | |
| mainLabel.textContent = this.files[0].name; | |
| subLabel.textContent = "1 image selected"; | |
| } else { | |
| mainLabel.textContent = `${count} Images Selected`; | |
| subLabel.textContent = "Ready to upload"; | |
| } | |
| } else { | |
| resetUI(); | |
| } | |
| }); | |
| function resetUI() { | |
| dropZone.classList.remove('has-files'); | |
| icon.className = 'bi bi-images upload-icon'; | |
| mainLabel.textContent = "Tap to select images"; | |
| subLabel.textContent = "Support for PNG, JPG, JPEG"; | |
| fileInput.value = ''; // Clear value | |
| } | |
| // Drag & Drop Visuals | |
| ['dragenter', 'dragover'].forEach(eventName => { | |
| dropZone.addEventListener(eventName, (e) => { | |
| e.preventDefault(); e.stopPropagation(); | |
| dropZone.classList.add('drag-over'); | |
| }, false); | |
| }); | |
| ['dragleave', 'drop'].forEach(eventName => { | |
| dropZone.addEventListener(eventName, (e) => { | |
| e.preventDefault(); e.stopPropagation(); | |
| dropZone.classList.remove('drag-over'); | |
| }, false); | |
| }); | |
| function toggleCameraReceiver() { | |
| const component = document.getElementById('camera-receiver-component'); | |
| if (component.style.display === 'none') { | |
| component.style.display = 'block'; | |
| if (typeof initCameraReceiver === 'function') { | |
| initCameraReceiver(); | |
| } | |
| } else { | |
| component.style.display = 'none'; | |
| } | |
| } | |
| // Form Submission | |
| document.getElementById('upload-form').addEventListener('submit', async function(e) { | |
| e.preventDefault(); | |
| const btn = document.getElementById('submitBtn'); | |
| const spinner = btn.querySelector('.spinner-border'); | |
| const btnText = btn.querySelector('.btn-text'); | |
| const btnIcon = btn.querySelector('.btn-icon'); | |
| // Check if files selected | |
| if (!fileInput.files || fileInput.files.length === 0) { | |
| statusDiv.innerHTML = `<div class="alert alert-warning d-flex align-items-center"><i class="bi bi-exclamation-triangle me-2"></i> Please select at least one image.</div>`; | |
| return; | |
| } | |
| // Loading State | |
| btn.disabled = true; | |
| spinner.classList.remove('d-none'); | |
| btnIcon.classList.add('d-none'); | |
| btnText.textContent = "Uploading..."; | |
| statusDiv.innerHTML = ''; | |
| const formData = new FormData(e.target); | |
| try { | |
| const response = await fetch('/upload_images', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| const result = await response.json(); | |
| if (response.ok) { | |
| // Success State | |
| btn.className = "btn btn-success w-100 btn-action shadow"; | |
| btnText.textContent = "Success!"; | |
| statusDiv.innerHTML = `<div class="alert alert-success border-0 bg-success bg-opacity-10 text-success"><i class="bi bi-check-lg me-2"></i> Uploaded ${result.files.length} images. Redirecting...</div>`; | |
| // Redirect | |
| setTimeout(() => { | |
| window.location.href = `/cropv2/${result.session_id}/0`; | |
| }, 800); | |
| } else { | |
| throw new Error(result.error || 'Upload failed'); | |
| } | |
| } catch (error) { | |
| // Error State | |
| statusDiv.innerHTML = `<div class="alert alert-danger border-0 bg-danger bg-opacity-10 text-danger"><i class="bi bi-x-circle me-2"></i> Error: ${error.message}</div>`; | |
| btn.disabled = false; | |
| spinner.classList.add('d-none'); | |
| btnIcon.classList.remove('d-none'); | |
| btnText.textContent = "Try Again"; | |
| } | |
| }); | |
| </script> | |
| {% endblock %} | |