|
|
<!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> |