Spaces:
Sleeping
Sleeping
| /* ──────────────────────────── Three.js 3D PDF icon ──── */ | |
| (function () { | |
| const container = document.getElementById('three-header'); | |
| if (!container || typeof THREE === "undefined") return; | |
| const W = 120, H = 120; | |
| const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); | |
| renderer.setPixelRatio(window.devicePixelRatio); | |
| renderer.setSize(W, H); | |
| container.appendChild(renderer.domElement); | |
| const scene = new THREE.Scene(); | |
| const camera = new THREE.PerspectiveCamera(38, W / H, 0.1, 100); | |
| camera.position.set(0, 0, 5); | |
| scene.add(new THREE.AmbientLight(0xffffff, 0.6)); | |
| const dirLight = new THREE.DirectionalLight(0xffffff, 0.9); | |
| dirLight.position.set(4, 6, 4); | |
| scene.add(dirLight); | |
| const bodyGeo = new THREE.BoxGeometry(1.4, 1.8, 0.12); | |
| const bodyMat = new THREE.MeshStandardMaterial({ color: 0xffffff }); | |
| const body = new THREE.Mesh(bodyGeo, bodyMat); | |
| scene.add(body); | |
| const cornerShape = new THREE.Shape(); | |
| cornerShape.moveTo(0, 0); | |
| cornerShape.lineTo(0.38, 0); | |
| cornerShape.lineTo(0, -0.38); | |
| cornerShape.closePath(); | |
| const cornerGeo = new THREE.ExtrudeGeometry(cornerShape, { depth: 0.13, bevelEnabled: false }); | |
| const cornerMat = new THREE.MeshStandardMaterial({ color: 0x5048c8 }); | |
| const corner = new THREE.Mesh(cornerGeo, cornerMat); | |
| corner.position.set(0.7, 0.9, 0); | |
| scene.add(corner); | |
| const stripeGeo = new THREE.BoxGeometry(0.9, 0.06, 0.14); | |
| const stripeMat = new THREE.MeshStandardMaterial({ color: 0xe24b4a }); | |
| [-0.2, 0.1, 0.4].forEach(y => { | |
| const s = new THREE.Mesh(stripeGeo, stripeMat); | |
| s.position.set(-0.12, y, 0); | |
| scene.add(s); | |
| }); | |
| const planeGeo = new THREE.PlaneGeometry(1.45, 1.85); | |
| const planeMat = new THREE.MeshStandardMaterial({ color: 0x5048c8 }); | |
| const plane = new THREE.Mesh(planeGeo, planeMat); | |
| plane.position.set(0.10, -0.10, -0.20); | |
| scene.add(plane); | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| body.rotation.y += 0.012; | |
| corner.rotation.y = body.rotation.y; | |
| plane.rotation.y = body.rotation.y; | |
| scene.children.forEach(c => { | |
| if (c.isMesh) c.rotation.y = body.rotation.y; | |
| }); | |
| renderer.render(scene, camera); | |
| } | |
| animate(); | |
| })(); | |
| /* ──────────────────────────── App state ───────────────── */ | |
| const FREE_LIMIT = 15; | |
| let selectedFiles = []; | |
| const dropZone = document.getElementById('drop-zone'); | |
| const fileInput = document.getElementById('file-input'); | |
| const previewGrid = document.getElementById('preview-grid'); | |
| const counterBar = document.getElementById('counter-bar'); | |
| const countNum = document.getElementById('count-num'); | |
| const countDot = document.getElementById('count-dot'); | |
| const countLimit = document.getElementById('count-limit-text'); | |
| const convertBtn = document.getElementById('convert-btn'); | |
| const btnLabel = document.getElementById('btn-label'); | |
| const loadingBar = document.getElementById('loading-bar'); | |
| const clearBtn = document.getElementById('clear-btn'); | |
| const overlay = document.getElementById('overlay'); | |
| const toast = document.getElementById('toast'); | |
| if (dropZone && fileInput) { | |
| dropZone.addEventListener('dragover', e => { | |
| e.preventDefault(); | |
| dropZone.classList.add('drag-over'); | |
| }); | |
| dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over')); | |
| dropZone.addEventListener('drop', e => { | |
| e.preventDefault(); | |
| dropZone.classList.remove('drag-over'); | |
| addFiles(e.dataTransfer.files); | |
| }); | |
| fileInput.addEventListener('change', () => { | |
| addFiles(fileInput.files); | |
| fileInput.value = ''; | |
| }); | |
| } | |
| function addFiles(fileList) { | |
| const incoming = Array.from(fileList).filter(f => f.type.startsWith('image/')); | |
| const newTotal = selectedFiles.length + incoming.length; | |
| if (newTotal > FREE_LIMIT) { | |
| const allowed = incoming.slice(0, FREE_LIMIT - selectedFiles.length); | |
| allowed.forEach(pushFile); | |
| updateUI(); | |
| showPremiumDialog(); | |
| return; | |
| } | |
| incoming.forEach(pushFile); | |
| updateUI(); | |
| } | |
| function pushFile(file) { | |
| const url = URL.createObjectURL(file); | |
| selectedFiles.push({ file, url }); | |
| } | |
| function removeFile(index) { | |
| URL.revokeObjectURL(selectedFiles[index].url); | |
| selectedFiles.splice(index, 1); | |
| updateUI(); | |
| } | |
| function clearAll() { | |
| selectedFiles.forEach(f => URL.revokeObjectURL(f.url)); | |
| selectedFiles = []; | |
| updateUI(); | |
| } | |
| function updateUI() { | |
| const n = selectedFiles.length; | |
| if (n > 0) { | |
| counterBar.classList.add('visible'); | |
| countNum.textContent = n; | |
| countLimit.textContent = `${n} / ${FREE_LIMIT} free slots used`; | |
| if (countDot) { | |
| countDot.className = 'count-dot' + (n >= FREE_LIMIT ? ' danger' : n >= 10 ? ' warn' : ''); | |
| } | |
| } else { | |
| counterBar.classList.remove('visible'); | |
| } | |
| previewGrid.innerHTML = ''; | |
| selectedFiles.forEach(({ url }, i) => { | |
| const item = document.createElement('div'); | |
| item.className = 'preview-item'; | |
| item.innerHTML = ` | |
| <img src="${url}" alt="preview ${i + 1}" loading="lazy" /> | |
| <span class="order-badge">${i + 1}</span> | |
| <button class="remove-btn" onclick="removeFile(${i})" title="Remove"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke-width="2.5" stroke-linecap="round"> | |
| <line x1="18" y1="6" x2="6" y2="18"></line> | |
| <line x1="6" y1="6" x2="18" y2="18"></line> | |
| </svg> | |
| </button> | |
| `; | |
| previewGrid.appendChild(item); | |
| }); | |
| convertBtn.disabled = n === 0; | |
| clearBtn.style.display = n > 0 ? 'block' : 'none'; | |
| } | |
| async function convertToPDF() { | |
| if (selectedFiles.length === 0) { | |
| showToast("Please select at least one image."); | |
| return; | |
| } | |
| if (selectedFiles.length > FREE_LIMIT) { | |
| showPremiumDialog(); | |
| return; | |
| } | |
| setLoading(true); | |
| const formData = new FormData(); | |
| selectedFiles.forEach(({ file }) => { | |
| formData.append('images', file); | |
| }); | |
| try { | |
| const res = await fetch('/convert', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| const contentType = res.headers.get("content-type") || ""; | |
| if (!res.ok) { | |
| let errorMessage = "Conversion failed."; | |
| if (contentType.includes("application/json")) { | |
| const json = await res.json(); | |
| if (json.error === "limit_exceeded") { | |
| showPremiumDialog(); | |
| errorMessage = json.message || "Free limit reached."; | |
| } else { | |
| errorMessage = json.error || json.message || errorMessage; | |
| } | |
| } | |
| showToast(errorMessage); | |
| return; | |
| } | |
| const blob = await res.blob(); | |
| const url = window.URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = 'converted.pdf'; | |
| document.body.appendChild(a); | |
| a.click(); | |
| a.remove(); | |
| window.URL.revokeObjectURL(url); | |
| showToast('PDF downloaded successfully!'); | |
| } catch (err) { | |
| console.error(err); | |
| showToast('Server error. Please try again.'); | |
| } finally { | |
| setLoading(false); | |
| } | |
| } | |
| function setLoading(on) { | |
| convertBtn.disabled = on; | |
| btnLabel.style.display = on ? 'none' : 'inline'; | |
| loadingBar.style.display = on ? 'block' : 'none'; | |
| } | |
| function showPremiumDialog() { | |
| overlay.classList.add('show'); | |
| } | |
| function closeDialog(e) { | |
| if (!e || e.target === overlay) { | |
| overlay.classList.remove('show'); | |
| } | |
| } | |
| function showToast(msg) { | |
| toast.textContent = msg; | |
| toast.classList.add('show'); | |
| setTimeout(() => toast.classList.remove('show'), 3000); | |
| } | |
| window.removeFile = removeFile; | |
| window.clearAll = clearAll; | |
| window.convertToPDF = convertToPDF; | |
| window.closeDialog = closeDialog; | |
| window.showToast = showToast; |