/** * katdrop - minimal, smooth, and interactive */ (function () { 'use strict'; // DOM Elements const dropZone = document.getElementById('dropZone'); const fileInput = document.getElementById('fileInput'); const uploadBtn = document.getElementById('uploadBtn'); const fileSelection = document.getElementById('fileSelection'); const progressWrap = document.getElementById('progressWrap'); const progressBar = document.getElementById('progressBar'); const progressText = document.getElementById('progressText'); const fileList = document.getElementById('fileList'); const fileCount = document.getElementById('fileCount'); const backendStatus = document.getElementById('backendStatus'); const statusText = document.getElementById('statusText'); const toastEl = document.getElementById('toast'); let selectedFiles = []; // API Configuration (permanent default backend) const DEFAULT_REMOTE_API = 'https://katdrop-api.oiupoyt.space'; // Purge any legacy ephemeral trycloudflare or placeholder URLs from localStorage const savedApi = localStorage.getItem('katdrop_api'); if (savedApi && (savedApi.includes('trycloudflare.com') || savedApi.includes('YOUR_BACKEND_URL'))) { localStorage.removeItem('katdrop_api'); } const urlParams = new URLSearchParams(window.location.search); if (urlParams.has('api')) { const customApi = urlParams.get('api').replace(/\/+$/, ''); localStorage.setItem('katdrop_api', customApi); } const isLocalOrigin = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1' || window.location.hostname.startsWith('192.168.'); const API_BASE = window.KATDROP_API || localStorage.getItem('katdrop_api') || (isLocalOrigin ? '' : DEFAULT_REMOTE_API); // Fetch helper function apiFetch(url, options = {}) { return fetch(url, options); } // Toast let toastTimeout; function showToast(msg) { clearTimeout(toastTimeout); toastEl.textContent = `// ${msg}`; toastEl.classList.add('show'); toastTimeout = setTimeout(() => { toastEl.classList.remove('show'); }, 2200); } // Utility: Format bytes function formatBytes(bytes) { if (!bytes || bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; } // Backend Status Check (with resilience against momentary network congestion) let consecutiveHealthFailures = 0; async function checkBackendStatus() { if (!backendStatus || !statusText) return; try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 6000); const res = await apiFetch(`${API_BASE}/health`, { signal: controller.signal }); clearTimeout(timeoutId); if (res.ok) { consecutiveHealthFailures = 0; backendStatus.className = 'backend-status online'; statusText.textContent = 'online'; } else { throw new Error('Non-200'); } } catch (err) { consecutiveHealthFailures++; // Require at least 2 consecutive failures so high-traffic uploads don't trigger false offline badges if (consecutiveHealthFailures >= 2) { backendStatus.className = 'backend-status offline'; statusText.textContent = 'offline'; } } } // Autonomous & Interactive Particle Background function initInteractiveParticles() { const canvas = document.getElementById('particles'); if (!canvas) return; const ctx = canvas.getContext('2d'); let width = (canvas.width = window.innerWidth); let height = (canvas.height = window.innerHeight); window.addEventListener('resize', () => { width = canvas.width = window.innerWidth; height = canvas.height = window.innerHeight; }); // Mouse & Touch tracking for interactivity const pointer = { x: null, y: null, radius: 150 }; window.addEventListener('mousemove', e => { pointer.x = e.clientX; pointer.y = e.clientY; }); window.addEventListener('mouseleave', () => { pointer.x = null; pointer.y = null; }); window.addEventListener('touchmove', e => { if (e.touches.length > 0) { pointer.x = e.touches[0].clientX; pointer.y = e.touches[0].clientY; } }, { passive: true }); window.addEventListener('touchend', () => { pointer.x = null; pointer.y = null; }); // Click to push particles outward window.addEventListener('click', e => { for (const p of particles) { const dx = p.x - e.clientX; const dy = p.y - e.clientY; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < 220 && dist > 0) { const force = (220 - dist) / 220; p.disturbVx += (dx / dist) * force * 4; p.disturbVy += (dy / dist) * force * 4; } } }); const particles = []; const count = Math.min(80, Math.max(35, Math.floor(width / 20))); for (let i = 0; i < count; i++) { const angle = Math.random() * Math.PI * 2; const speed = 0.35 + Math.random() * 0.45; particles.push({ x: Math.random() * width, y: Math.random() * height, baseVx: Math.cos(angle) * speed, baseVy: Math.sin(angle) * speed, disturbVx: 0, disturbVy: 0, radius: Math.random() * 1.5 + 1, phase: Math.random() * Math.PI * 2, twinkleSpeed: 0.0018 + Math.random() * 0.0025 }); } function animate(time) { ctx.clearRect(0, 0, width, height); // Connect particles to each other (constellation network) for (let i = 0; i < particles.length; i++) { for (let j = i + 1; j < particles.length; j++) { const dx = particles[i].x - particles[j].x; const dy = particles[i].y - particles[j].y; const distSq = dx * dx + dy * dy; if (distSq < 15625) { // 125px const dist = Math.sqrt(distSq); ctx.strokeStyle = `rgba(255, 255, 255, ${0.075 * (1 - dist / 125)})`; ctx.lineWidth = 0.8; ctx.beginPath(); ctx.moveTo(particles[i].x, particles[i].y); ctx.lineTo(particles[j].x, particles[j].y); ctx.stroke(); } } } // Update & draw each particle for (const p of particles) { // Continuous organic autonomous motion: base velocity + subtle harmonic wave drift const waveX = Math.sin(time * 0.0012 + p.phase) * 0.22; const waveY = Math.cos(time * 0.0015 + p.phase) * 0.22; p.x += p.baseVx + p.disturbVx + waveX; p.y += p.baseVy + p.disturbVy + waveY; // Dampen interactive disturbance back to zero (leaves autonomous base speed intact) p.disturbVx *= 0.94; p.disturbVy *= 0.94; // Seamless boundary wrap with padding so particles never bunch at edges const pad = 15; if (p.x < -pad) p.x = width + pad; if (p.x > width + pad) p.x = -pad; if (p.y < -pad) p.y = height + pad; if (p.y > height + pad) p.y = -pad; // Interaction with mouse/touch pointer if (pointer.x !== null && pointer.y !== null) { const mdx = pointer.x - p.x; const mdy = pointer.y - p.y; const mDistSq = mdx * mdx + mdy * mdy; if (mDistSq < pointer.radius * pointer.radius) { const mDist = Math.sqrt(mDistSq); // Connect line to pointer ctx.strokeStyle = `rgba(255, 255, 255, ${0.2 * (1 - mDist / pointer.radius)})`; ctx.lineWidth = 0.9; ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(pointer.x, pointer.y); ctx.stroke(); // Repel gently if close if (mDist < 75 && mDist > 0) { const repel = (75 - mDist) / 75; p.disturbVx -= (mdx / mDist) * repel * 1.6; p.disturbVy -= (mdy / mDist) * repel * 1.6; } } } // Breathing particle glow const alpha = 0.25 + Math.sin(time * p.twinkleSpeed + p.phase) * 0.14; ctx.fillStyle = `rgba(255, 255, 255, ${alpha.toFixed(3)})`; ctx.beginPath(); ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2); ctx.fill(); } requestAnimationFrame(animate); } requestAnimationFrame(animate); } // Safe HTML helper function escapeHtml(str) { return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } // Countdown Timer Management let countdownTimer = null; function formatTimeLeft(seconds) { if (seconds <= 0) return '00:00'; const m = Math.floor(seconds / 60); const s = seconds % 60; return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`; } function tickCountdowns() { const timerEls = document.querySelectorAll('.file-timer'); if (timerEls.length === 0) return; const now = Date.now(); let hasExpired = false; timerEls.forEach(el => { const expires = parseInt(el.dataset.expiresMs, 10); if (isNaN(expires)) return; const remainingSec = Math.max(0, Math.ceil((expires - now) / 1000)); el.textContent = formatTimeLeft(remainingSec); if (remainingSec <= 30) { el.className = 'file-timer critical'; } else if (remainingSec <= 120) { el.className = 'file-timer warning'; } else { el.className = 'file-timer'; } if (remainingSec === 0) { hasExpired = true; const card = el.closest('li.file-card'); if (card) card.classList.add('deleting'); } }); if (hasExpired) { setTimeout(fetchFiles, 400); } } function startCountdownLoop() { if (countdownTimer) clearInterval(countdownTimer); tickCountdowns(); countdownTimer = setInterval(tickCountdowns, 1000); } // Fetch and display files async function fetchFiles() { try { const res = await apiFetch(`${API_BASE}/files`); const files = await res.json(); fileList.innerHTML = ''; if (!files || files.length === 0) { fileList.innerHTML = '