Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>Instagram Downloader</title> | |
| <style> | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; background: linear-gradient(135deg,#667eea 0%,#764ba2 100%); min-height: 100vh; display: flex; justify-content: center; align-items: center; padding: 20px; } | |
| .container { background:#fff; border-radius:20px; box-shadow:0 20px 60px rgba(0,0,0,.3); padding:40px; max-width:560px; width:100%; animation: fadeIn .5s ease-in; } | |
| @keyframes fadeIn { from {opacity:0; transform: translateY(20px);} to {opacity:1; transform: translateY(0);} } | |
| .logo{ text-align:center; margin-bottom:30px; } | |
| .logo svg{ width:60px; height:60px; margin-bottom:10px; } | |
| h1{ color:#333; text-align:center; font-size:32px; margin-bottom:10px; font-weight:700; } | |
| .gradient-text{ background:linear-gradient(135deg,#667eea 0%, #764ba2 100%); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text; } | |
| .instructions{ text-align:center; color:#666; font-size:14px; margin-bottom:24px; line-height:1.6; } | |
| .input-group{ margin-bottom:16px; } | |
| label{ display:block; color:#333; font-weight:600; margin-bottom:10px; font-size:14px; } | |
| .row { display:flex; gap:12px; align-items:center; } | |
| input[type="text"]{ flex:1; padding:14px 44px 14px 14px; border:2px solid #e0e0e0; border-radius:10px; font-size:16px; transition: all .25s ease; outline:none; } | |
| input[type="text"]:focus{ border-color:#667eea; box-shadow:0 0 0 4px rgba(102,126,234,.1); } | |
| input[type="text"]::placeholder{ color:#aaa; } | |
| .copy{ width:40px; height:40px; border:none; border-radius:10px; background:#f3f4f6; cursor:pointer; display:flex; align-items:center; justify-content:center; color:#555; } | |
| .copy:hover{ background:#e5e7eb; } | |
| .download-btn{ width:100%; padding:14px; background:linear-gradient(135deg,#667eea 0%, #764ba2 100%); color:#fff; border:none; border-radius:10px; font-size:18px; font-weight:600; cursor:pointer; transition: all .25s ease; box-shadow:0 4px 15px rgba(102,126,234,.4); display:inline-flex; align-items:center; justify-content:center; gap:10px; } | |
| .download-btn:hover{ transform: translateY(-2px); box-shadow:0 6px 20px rgba(102,126,234,.6); } | |
| .download-btn:disabled{ opacity:0.6; cursor:not-allowed; } | |
| .status { margin-top:12px; min-height:22px; font-size:14px; } | |
| .status .info{ color:#555; } | |
| .status .error{ color:#b00020; } | |
| .status .success{ color:#166534; } | |
| .progress { display:none; margin-top:12px; height:8px; background:#eee; border-radius:999px; overflow:hidden; } | |
| .progress > span { display:block; height:100%; width:0%; background:linear-gradient(90deg,#667eea,#764ba2); transition: width .2s ease; } | |
| .preview { display:none; margin-top:16px; border:1px solid #eee; border-radius:10px; padding:10px; } | |
| .features{ margin-top:26px; padding-top:20px; border-top:1px solid #e0e0e0; } | |
| .feature-list{ list-style:none; columns:2; gap:16px; } | |
| .feature-list li{ color:#666; font-size:14px; margin-bottom:10px; padding-left:25px; position:relative; } | |
| .feature-list li:before{ content:"✓"; position:absolute; left:0; color:#667eea; font-weight:bold; font-size:18px; } | |
| @media(max-width:600px){ .container{ padding:30px 20px; } h1{ font-size:28px; } .feature-list{ columns:1; } } | |
| .note { background:#fff7ed; border:1px solid #fed7aa; color:#7c2d12; padding:10px; border-radius:8px; margin-top:12px; font-size:13px; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="logo"> | |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="url(#gradient)"> | |
| <defs> | |
| <linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="100%"> | |
| <stop offset="0%" style="stop-color:#667eea;stop-opacity:1"/> | |
| <stop offset="100%" style="stop-color:#764ba2;stop-opacity:1"/> | |
| </linearGradient> | |
| </defs> | |
| <path d="M34,4H14C8.5,4,4,8.5,4,14v20c0,5.5,4.5,10,10,10h20c5.5,0,10-4.5,10-10V14C44,8.5,39.5,4,34,4z M24,33c-5,0-9-4-9-9s4-9,9-9s9,4,9,9S29,33,24,33z M35,13c-1.1,0-2-0.9-2-2s0.9-2,2-2s2,0.9,2,2S36.1,13,35,13z"/> | |
| <circle cx="24" cy="24" r="6"/> | |
| </svg> | |
| </div> | |
| <h1><span class="gradient-text">Instagram Downloader</span></h1> | |
| <p class="instructions">Paste any public Instagram image or video link below and click Download.</p> | |
| <form id="downloadForm"> | |
| <div class="input-group"> | |
| <label for="linkInput">Instagram Link</label> | |
| <div class="row"> | |
| <input type="text" id="linkInput" name="link" placeholder="https://www.instagram.com/p/..." required /> | |
| <button type="button" class="copy" aria-label="Paste from clipboard" title="Paste" id="pasteBtn">📋</button> | |
| </div> | |
| </div> | |
| <button type="submit" class="download-btn" id="downloadBtn">⬇️ Download</button> | |
| <div class="progress" id="progress"><span id="bar"></span></div> | |
| <div class="status" id="status" aria-live="polite"></div> | |
| <div class="note" id="warning" style="display:none"></div> | |
| </form> | |
| <div class="features"> | |
| <ul class="feature-list"> | |
| <li>Download Instagram photos in high quality</li> | |
| <li>Download Instagram videos easily</li> | |
| <li>Progress and error feedback</li> | |
| <li>Mobile-friendly interface</li> | |
| </ul> | |
| </div> | |
| </div> | |
| <script> | |
| const form = document.getElementById('downloadForm'); | |
| const input = document.getElementById('linkInput'); | |
| const statusEl = document.getElementById('status'); | |
| const progress = document.getElementById('progress'); | |
| const bar = document.getElementById('bar'); | |
| const downloadBtn = document.getElementById('downloadBtn'); | |
| const pasteBtn = document.getElementById('pasteBtn'); | |
| const warning = document.getElementById('warning'); | |
| // Backend API endpoint (Gradio) | |
| const API_ENDPOINT = '/api/predict'; | |
| function setStatus(type, msg){ | |
| statusEl.innerHTML = msg ? `<span class="${type}">${msg}</span>` : ''; | |
| } | |
| function showWarning(msg){ | |
| warning.style.display = 'block'; | |
| warning.innerHTML = msg; | |
| } | |
| function resetUI(){ | |
| setStatus('info',''); | |
| bar.style.width = '0%'; | |
| progress.style.display = 'none'; | |
| warning.style.display = 'none'; | |
| warning.innerHTML = ''; | |
| } | |
| function startLoading(){ | |
| progress.style.display = 'block'; | |
| setStatus('info','Processing...'); | |
| bar.style.width = '20%'; | |
| } | |
| function sanitizeUrl(u){ | |
| try{ | |
| const url = new URL(u.trim()); | |
| if(!url.hostname.replace(/^www\./,'').endsWith('instagram.com')) | |
| throw new Error('Not an Instagram link'); | |
| return url.toString(); | |
| } catch{ | |
| throw new Error('Please enter a valid Instagram URL'); | |
| } | |
| } | |
| pasteBtn?.addEventListener('click', async () => { | |
| try { | |
| const text = await navigator.clipboard.readText(); | |
| if(text) input.value = text.trim(); | |
| } catch { | |
| setStatus('error','Clipboard blocked by browser'); | |
| } | |
| }); | |
| form.addEventListener('submit', async (e)=>{ | |
| e.preventDefault(); | |
| resetUI(); | |
| try { | |
| const url = sanitizeUrl(input.value); | |
| downloadBtn.disabled = true; | |
| startLoading(); | |
| setStatus('info','Sending request to backend...'); | |
| bar.style.width = '30%'; | |
| // POST to Gradio API endpoint | |
| const response = await fetch(API_ENDPOINT, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| data: [url] | |
| }) | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`Server error: ${response.status}`); | |
| } | |
| bar.style.width = '60%'; | |
| setStatus('info','Receiving response...'); | |
| const result = await response.json(); | |
| bar.style.width = '80%'; | |
| setStatus('info','Processing download...'); | |
| // Gradio returns data in result.data array | |
| // data[0] is the file path/blob, data[1] is the status message | |
| if (result.data && result.data[0]) { | |
| const fileData = result.data[0]; | |
| const statusMessage = result.data[1] || ''; | |
| // If we have a file URL or blob | |
| if (fileData && fileData.name) { | |
| // Create download link | |
| const fileUrl = fileData.url || `/file=${fileData.path}`; | |
| const fileName = fileData.orig_name || fileData.name || 'instagram-media'; | |
| // Fetch the file and trigger download | |
| const fileResponse = await fetch(fileUrl); | |
| const blob = await fileResponse.blob(); | |
| const a = document.createElement('a'); | |
| a.href = URL.createObjectURL(blob); | |
| a.download = fileName; | |
| document.body.appendChild(a); | |
| a.click(); | |
| a.remove(); | |
| setTimeout(() => URL.revokeObjectURL(a.href), 100); | |
| bar.style.width = '100%'; | |
| setStatus('success', statusMessage || '✅ Download complete!'); | |
| } else { | |
| throw new Error(statusMessage || 'No file returned from backend'); | |
| } | |
| } else { | |
| // Check if there's an error message | |
| const errorMsg = result.data && result.data[1] ? result.data[1] : 'Failed to download media'; | |
| throw new Error(errorMsg); | |
| } | |
| } catch (err){ | |
| bar.style.width = '0%'; | |
| setStatus('error', err.message || 'Something went wrong'); | |
| showWarning( | |
| `❌ Error: ${err.message}. Make sure the Instagram URL is valid and the post is public.` | |
| ); | |
| } finally { | |
| downloadBtn.disabled = false; | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> |