| | <!DOCTYPE html> |
| | <html lang="en"> |
| |
|
| | <head> |
| | <meta charset="UTF-8"> |
| | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| | <title>StuDocu Downloader</title> |
| | <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&display=swap" rel="stylesheet"> |
| | <style> |
| | body { |
| | font-family: 'Poppins', sans-serif; |
| | background-color: #f0f2f5; |
| | margin: 0; |
| | display: flex; |
| | justify-content: center; |
| | align-items: center; |
| | min-height: 100vh; |
| | padding: 20px; |
| | box-sizing: border-box; |
| | } |
| | |
| | .container { |
| | background-color: #ffffff; |
| | padding: 30px 40px; |
| | border-radius: 12px; |
| | box-shadow: 0 8px 30px rgba(0, 0, 0, 0.08); |
| | width: 100%; |
| | max-width: 650px; |
| | text-align: center; |
| | transition: all 0.3s ease; |
| | } |
| | |
| | .header .logo { |
| | width: 40px; |
| | height: 40px; |
| | color: #007bff; |
| | margin-bottom: 10px; |
| | } |
| | |
| | .header h1 { |
| | color: #2c3e50; |
| | margin: 0 0 10px; |
| | font-weight: 600; |
| | } |
| | |
| | .header p { |
| | color: #7f8c8d; |
| | margin-bottom: 30px; |
| | font-size: 1rem; |
| | } |
| | |
| | .form-container { |
| | display: flex; |
| | margin-bottom: 20px; |
| | } |
| | |
| | #studocu-url { |
| | flex-grow: 1; |
| | padding: 14px 18px; |
| | border: 1px solid #dfe4ea; |
| | border-radius: 8px 0 0 8px; |
| | font-size: 16px; |
| | outline: none; |
| | transition: border-color 0.3s ease, box-shadow 0.3s ease; |
| | } |
| | |
| | #studocu-url:focus { |
| | border-color: #007bff; |
| | box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.15); |
| | } |
| | |
| | #download-btn { |
| | padding: 14px 25px; |
| | border: none; |
| | background-color: #007bff; |
| | color: white; |
| | font-size: 16px; |
| | font-weight: 600; |
| | border-radius: 0 8px 8px 0; |
| | cursor: pointer; |
| | outline: none; |
| | position: relative; |
| | transition: background-color 0.3s ease; |
| | display: flex; |
| | align-items: center; |
| | justify-content: center; |
| | min-width: 120px; |
| | } |
| | |
| | #download-btn:hover { |
| | background-color: #0056b3; |
| | } |
| | |
| | #download-btn:disabled { |
| | background-color: #5a9eeb; |
| | cursor: not-allowed; |
| | } |
| | |
| | .btn-loader { |
| | display: none; |
| | border: 3px solid #f3f3f3; |
| | border-top: 3px solid #0056b3; |
| | border-radius: 50%; |
| | width: 20px; |
| | height: 20px; |
| | animation: spin 1s linear infinite; |
| | } |
| | |
| | #download-btn.loading .btn-text { |
| | display: none; |
| | } |
| | |
| | #download-btn.loading .btn-loader { |
| | display: block; |
| | } |
| | |
| | @keyframes spin { |
| | 0% { |
| | transform: rotate(0deg); |
| | } |
| | |
| | 100% { |
| | transform: rotate(360deg); |
| | } |
| | } |
| | |
| | .progress-section { |
| | margin-top: 25px; |
| | text-align: left; |
| | } |
| | |
| | .progress-bar-container { |
| | width: 100%; |
| | background-color: #e9ecef; |
| | border-radius: 8px; |
| | overflow: hidden; |
| | height: 12px; |
| | } |
| | |
| | .progress-bar { |
| | width: 0%; |
| | height: 100%; |
| | background-color: #007bff; |
| | border-radius: 8px; |
| | transition: width 0.4s ease-in-out; |
| | } |
| | |
| | #status-text { |
| | margin-top: 8px; |
| | color: #495057; |
| | font-size: 0.9rem; |
| | text-align: center; |
| | } |
| | |
| | .status-indicator.error { |
| | background-color: #f8d7da; |
| | color: #721c24; |
| | border: 1px solid #f5c6cb; |
| | padding: 12px; |
| | border-radius: 8px; |
| | margin-top: 20px; |
| | font-size: 0.95rem; |
| | } |
| | |
| | |
| | .log-section { |
| | margin-top: 25px; |
| | text-align: left; |
| | border: 1px solid #dfe4ea; |
| | border-radius: 8px; |
| | padding: 15px; |
| | background-color: #fafafa; |
| | } |
| | |
| | .log-section h3 { |
| | margin-top: 0; |
| | margin-bottom: 10px; |
| | color: #2c3e50; |
| | font-size: 1rem; |
| | font-weight: 600; |
| | } |
| | |
| | .log-container { |
| | background-color: #2c3e50; |
| | color: #ecf0f1; |
| | font-family: 'Courier New', Courier, monospace; |
| | font-size: 0.85rem; |
| | height: 150px; |
| | overflow-y: auto; |
| | padding: 10px; |
| | border-radius: 6px; |
| | white-space: pre-wrap; |
| | word-wrap: break-word; |
| | } |
| | |
| | .log-entry { |
| | display: flex; |
| | margin-bottom: 5px; |
| | } |
| | |
| | .log-entry .timestamp { |
| | color: #95a5a6; |
| | margin-right: 10px; |
| | flex-shrink: 0; |
| | } |
| | |
| | .log-entry .message { |
| | flex-grow: 1; |
| | } |
| | |
| | .log-entry.error .message { |
| | color: #e74c3c; |
| | |
| | } |
| | |
| | .log-entry.success .message { |
| | color: #2ecc71; |
| | |
| | } |
| | |
| | |
| | |
| | .footer { |
| | margin-top: 30px; |
| | font-size: 0.8rem; |
| | color: #95a5a6; |
| | } |
| | </style> |
| | </head> |
| |
|
| | <body> |
| | <div class="container"> |
| | <div class="header"> |
| | <svg class="logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"> |
| | <path |
| | d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 14h-2v-2h2v2zm0-4h-2V7h2v5z" /> |
| | </svg> |
| | <h1>StuDocu Document Downloader</h1> |
| | <p>Paste a valid StuDocu document URL to generate and download a PDF.</p> |
| | </div> |
| | <div class="main-content"> |
| | <div class="form-container"> |
| | <input type="text" id="studocu-url" placeholder="https://www.studocu.com/en-us/document/..."> |
| | <button id="download-btn"> |
| | <span class="btn-text">Download</span> |
| | <span class="btn-loader"></span> |
| | </button> |
| | </div> |
| |
|
| | <div class="progress-section" id="progress-section" style="display: none;"> |
| | <div class="progress-bar-container"> |
| | <div class="progress-bar" id="progress-bar"></div> |
| | </div> |
| | <p id="status-text">Starting...</p> |
| | </div> |
| |
|
| | |
| | <div class="log-section" id="log-section" style="display: none;"> |
| | <h3>Live Logs</h3> |
| | <div class="log-container" id="log-container"></div> |
| | </div> |
| |
|
| | <div class="status-indicator error" id="error-indicator" style="display: none;"></div> |
| | </div> |
| | <div class="footer"> |
| | <p>Powered by the Heart by Us</p> |
| | </div> |
| | </div> |
| | <script> |
| | document.addEventListener('DOMContentLoaded', () => { |
| | const downloadBtn = document.getElementById('download-btn'); |
| | const urlInput = document.getElementById('studocu-url'); |
| | const progressSection = document.getElementById('progress-section'); |
| | const progressBar = document.getElementById('progress-bar'); |
| | const statusText = document.getElementById('status-text'); |
| | const errorIndicator = document.getElementById('error-indicator'); |
| | |
| | const logSection = document.getElementById('log-section'); |
| | const logContainer = document.getElementById('log-container'); |
| | |
| | |
| | const API_BASE_URL = 'https://devusman-studocu-testing.hf.space'; |
| | let pollInterval; |
| | let lastLoggedMessage = ''; |
| | |
| | const resetUI = () => { |
| | setLoading(false); |
| | progressSection.style.display = 'none'; |
| | errorIndicator.style.display = 'none'; |
| | progressBar.style.width = '0%'; |
| | statusText.textContent = ''; |
| | |
| | logSection.style.display = 'none'; |
| | logContainer.innerHTML = ''; |
| | lastLoggedMessage = ''; |
| | }; |
| | |
| | |
| | const addLog = (message, type = 'info') => { |
| | if (!message || message === lastLoggedMessage) return; |
| | lastLoggedMessage = message; |
| | |
| | const logEntry = document.createElement('div'); |
| | logEntry.className = `log-entry ${type}`; |
| | const timestamp = new Date().toLocaleTimeString(); |
| | logEntry.innerHTML = `<span class="timestamp">${timestamp}</span><span class="message">${message}</span>`; |
| | |
| | logContainer.appendChild(logEntry); |
| | logContainer.scrollTop = logContainer.scrollHeight; |
| | }; |
| | |
| | const pollProgress = (sessionId) => { |
| | pollInterval = setInterval(async () => { |
| | try { |
| | const response = await fetch(`${API_BASE_URL}/api/progress/${sessionId}`); |
| | if (!response.ok) throw new Error('Failed to get progress update.'); |
| | |
| | const data = await response.json(); |
| | |
| | progressBar.style.width = `${data.progress}%`; |
| | statusText.textContent = `(${data.progress}%) ${data.message}`; |
| | addLog(`[${data.status}] ${data.message}`); |
| | |
| | if (data.progress >= 100) { |
| | clearInterval(pollInterval); |
| | statusText.textContent = 'โ
Success! Your download will start now.'; |
| | addLog('โ
PDF generated successfully! Starting download...', 'success'); |
| | window.location.href = `${API_BASE_URL}/api/download/${sessionId}`; |
| | setTimeout(resetUI, 5000); |
| | } |
| | |
| | if (data.progress < 0) { |
| | clearInterval(pollInterval); |
| | showError(`Error: ${data.message || 'An unknown error occurred.'}`); |
| | setLoading(false); |
| | } |
| | |
| | } catch (error) { |
| | clearInterval(pollInterval); |
| | console.error('Polling failed:', error); |
| | showError('Failed to connect to the server for progress updates.'); |
| | setLoading(false); |
| | } |
| | }, 2000); |
| | }; |
| | |
| | downloadBtn.addEventListener('click', async () => { |
| | const url = urlInput.value.trim(); |
| | if (!url || !url.includes('studocu.com')) { |
| | showError('Please provide a valid StuDocu URL.'); |
| | return; |
| | } |
| | |
| | resetUI(); |
| | setLoading(true); |
| | progressSection.style.display = 'block'; |
| | logSection.style.display = 'block'; |
| | statusText.textContent = 'Requesting download...'; |
| | addLog('๐ Requesting download...'); |
| | |
| | try { |
| | const response = await fetch(`${API_BASE_URL}/api/request-download`, { |
| | method: 'POST', |
| | headers: { 'Content-Type': 'application/json' }, |
| | body: JSON.stringify({ url }), |
| | }); |
| | |
| | if (!response.ok) { |
| | const errorData = await response.json(); |
| | throw new Error(errorData.error || 'Failed to start the download process.'); |
| | } |
| | |
| | const { sessionId } = await response.json(); |
| | pollProgress(sessionId); |
| | |
| | } catch (error) { |
| | console.error('Download failed:', error); |
| | showError(error.message); |
| | setLoading(false); |
| | } |
| | }); |
| | |
| | function setLoading(isLoading) { |
| | downloadBtn.disabled = isLoading; |
| | urlInput.disabled = isLoading; |
| | downloadBtn.classList.toggle('loading', isLoading); |
| | } |
| | |
| | function showError(message) { |
| | errorIndicator.style.display = 'block'; |
| | errorIndicator.textContent = message; |
| | progressSection.style.display = 'none'; |
| | logSection.style.display = 'block'; |
| | addLog(message, 'error'); |
| | } |
| | }); |
| | </script> |
| | </body> |
| |
|
| | </html> |