Download static/app.js from Cnass/rpgmaker: direct link, hf CLI and curl.
- Browser
- Download file 12.2 kB
-
https://huggingface.co/spaces/Cnass/rpgmaker/resolve/main/static/app.js
- Command line
-
hf download hf://spaces/Cnass/rpgmaker/static/app.js
-
curl -L -o app.js https://huggingface.co/spaces/Cnass/rpgmaker/resolve/main/static/app.js
12.2 kB
| // Riaslink Port Builder — frontend logic. | |
| // No framework, no build step: this is the entire client-side behavior for | |
| // the build form, talking to the plain FastAPI routes in app.py. | |
| (() => { | |
| 'use strict'; | |
| const form = document.getElementById('build-form'); | |
| const buildBtn = document.getElementById('build-btn'); | |
| const buildBtnLabel = buildBtn.querySelector('.build-btn-label'); | |
| const statusPill = document.getElementById('status-pill'); | |
| const logOutput = document.getElementById('log-output'); | |
| const copyLogBtn = document.getElementById('copy-log-btn'); | |
| const copyLogIcon = document.getElementById('copy-log-icon'); | |
| const downloadArea = document.getElementById('download-area'); | |
| const downloadLink = document.getElementById('download-link'); | |
| const fileInput = document.getElementById('www_zip'); | |
| const fileDrop = document.getElementById('file-drop'); | |
| const fileDropLabel = document.getElementById('file-drop-label'); | |
| const uploadRing = document.getElementById('upload-ring'); | |
| const uploadRingProgress = document.getElementById('upload-ring-progress'); | |
| const uploadRingLabel = document.getElementById('upload-ring-label'); | |
| const accentColor = document.getElementById('accent_hex'); | |
| const accentColorText = document.getElementById('accent_hex_text'); | |
| // ------------------------------------------------------------------- | |
| // File drop UX (native <input type="file"> still handles the actual | |
| // selection — this just adds drag-over styling and a filename label). | |
| // ------------------------------------------------------------------- | |
| fileInput.addEventListener('change', () => { | |
| updateFileDropLabel(); | |
| }); | |
| ['dragenter', 'dragover'].forEach((evt) => { | |
| fileDrop.addEventListener(evt, (e) => { | |
| e.preventDefault(); | |
| fileDrop.classList.add('drag-over'); | |
| }); | |
| }); | |
| ['dragleave', 'drop'].forEach((evt) => { | |
| fileDrop.addEventListener(evt, (e) => { | |
| e.preventDefault(); | |
| fileDrop.classList.remove('drag-over'); | |
| }); | |
| }); | |
| fileDrop.addEventListener('drop', (e) => { | |
| const dropped = e.dataTransfer?.files; | |
| if (dropped && dropped.length > 0) { | |
| fileInput.files = dropped; | |
| updateFileDropLabel(); | |
| } | |
| }); | |
| function updateFileDropLabel() { | |
| const file = fileInput.files[0]; | |
| if (file) { | |
| fileDropLabel.textContent = file.name; | |
| fileDropLabel.classList.add('has-file'); | |
| } else { | |
| fileDropLabel.textContent = 'Dosya seç ya da buraya sürükle'; | |
| fileDropLabel.classList.remove('has-file'); | |
| } | |
| } | |
| // Matches the fixed stroke-dasharray in style.css (2 * pi * r, r=15.5). | |
| const CIRCLE_CIRCUMFERENCE = 97.389; | |
| function showUploadRing() { | |
| fileDrop.classList.add('uploading'); | |
| uploadRing.classList.remove('hidden'); | |
| setUploadProgress(0); | |
| } | |
| function hideUploadRing() { | |
| fileDrop.classList.remove('uploading'); | |
| uploadRing.classList.add('hidden'); | |
| } | |
| function setUploadProgress(fraction) { | |
| const clamped = Math.max(0, Math.min(1, fraction)); | |
| const offset = CIRCLE_CIRCUMFERENCE * (1 - clamped); | |
| uploadRingProgress.style.strokeDashoffset = String(offset); | |
| uploadRingLabel.textContent = `${Math.round(clamped * 100)}%`; | |
| } | |
| // ------------------------------------------------------------------- | |
| // Color picker <-> hex text field, kept in sync both directions. | |
| // ------------------------------------------------------------------- | |
| accentColor.addEventListener('input', () => { | |
| accentColorText.value = accentColor.value; | |
| }); | |
| accentColorText.addEventListener('input', () => { | |
| const value = accentColorText.value.trim(); | |
| if (/^#[0-9a-fA-F]{6}$/.test(value)) { | |
| accentColor.value = value; | |
| } | |
| }); | |
| // ------------------------------------------------------------------- | |
| // Log rendering helpers | |
| // ------------------------------------------------------------------- | |
| let logHasContent = false; | |
| function clearLog() { | |
| logOutput.innerHTML = ''; | |
| logHasContent = false; | |
| } | |
| function appendLogLine(text, kind) { | |
| if (!logHasContent) { | |
| logOutput.innerHTML = ''; | |
| logHasContent = true; | |
| } | |
| const line = document.createElement('div'); | |
| if (kind) { | |
| line.className = `log-line-${kind}`; | |
| } | |
| line.textContent = text; | |
| logOutput.appendChild(line); | |
| logOutput.scrollTop = logOutput.scrollHeight; | |
| } | |
| function setStatus(state, label) { | |
| statusPill.dataset.state = state; | |
| statusPill.textContent = label; | |
| } | |
| function getLogText() { | |
| // Each log line is its own <div> (see appendLogLine) — reading | |
| // logOutput.textContent directly would run every line together | |
| // with no separator, so each line's own text is collected and | |
| // joined with real newlines instead. | |
| return Array.from(logOutput.children) | |
| .map((line) => line.textContent) | |
| .join('\n'); | |
| } | |
| copyLogBtn.addEventListener('click', async () => { | |
| const text = getLogText(); | |
| if (!text) return; | |
| try { | |
| if (navigator.clipboard && window.isSecureContext) { | |
| await navigator.clipboard.writeText(text); | |
| } else { | |
| // navigator.clipboard is only available in secure contexts | |
| // (HTTPS) — this covers plain-HTTP deployments with the | |
| // older document.execCommand approach instead. | |
| const textarea = document.createElement('textarea'); | |
| textarea.value = text; | |
| textarea.style.position = 'fixed'; | |
| textarea.style.opacity = '0'; | |
| document.body.appendChild(textarea); | |
| textarea.select(); | |
| document.execCommand('copy'); | |
| document.body.removeChild(textarea); | |
| } | |
| showCopyFeedback(true); | |
| } catch (err) { | |
| showCopyFeedback(false); | |
| } | |
| }); | |
| let copyFeedbackTimeout = null; | |
| function showCopyFeedback(success) { | |
| clearTimeout(copyFeedbackTimeout); | |
| copyLogBtn.classList.toggle('copied', success); | |
| copyLogIcon.textContent = success ? '✅' : '⚠️'; | |
| copyLogBtn.title = success ? 'Kopyalandı' : 'Kopyalanamadı'; | |
| copyFeedbackTimeout = setTimeout(() => { | |
| copyLogBtn.classList.remove('copied'); | |
| copyLogIcon.textContent = '📋'; | |
| copyLogBtn.title = 'Günlüğü kopyala'; | |
| }, 1600); | |
| } | |
| // ------------------------------------------------------------------- | |
| // Form submission | |
| // ------------------------------------------------------------------- | |
| form.addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| if (!fileInput.files[0]) { | |
| appendLogLine('Önce bir www.zip dosyası seç.', 'error'); | |
| return; | |
| } | |
| buildBtn.disabled = true; | |
| buildBtnLabel.textContent = 'Başlatılıyor...'; | |
| downloadArea.classList.add('hidden'); | |
| clearLog(); | |
| setStatus('running', 'Çalışıyor'); | |
| const formData = new FormData(form); | |
| // FormData already grabs www_zip from the <input type="file">, and | |
| // the checkbox only appears in FormData when checked — FastAPI's | |
| // Form(bool) parses "on"/"true"/"1" etc., but an absent field would | |
| // be missing entirely rather than "false", so it's added explicitly | |
| // here to always send a real value either way. | |
| formData.set('fps_default_visible', document.getElementById('fps_default_visible').checked ? 'true' : 'false'); | |
| let buildId; | |
| try { | |
| const data = await uploadWithProgress('/api/build', formData); | |
| buildId = data.build_id; | |
| } catch (err) { | |
| appendLogLine(`Build başlatılamadı: ${err.message}`, 'error'); | |
| setStatus('failed', 'Hata'); | |
| hideUploadRing(); | |
| resetButton(); | |
| return; | |
| } | |
| hideUploadRing(); | |
| buildBtnLabel.textContent = 'Build çalışıyor...'; | |
| streamBuildLog(buildId); | |
| }); | |
| // fetch() has no upload-progress event — only XMLHttpRequest exposes | |
| // xhr.upload.onprogress, which is what drives the circular ring below. | |
| // Wrapped in a Promise so the call site can stay a plain `await`. | |
| function uploadWithProgress(url, formData) { | |
| return new Promise((resolve, reject) => { | |
| const xhr = new XMLHttpRequest(); | |
| xhr.open('POST', url); | |
| xhr.upload.onprogress = (event) => { | |
| if (event.lengthComputable) { | |
| if (uploadRing.classList.contains('hidden')) { | |
| showUploadRing(); | |
| } | |
| setUploadProgress(event.loaded / event.total); | |
| } | |
| }; | |
| xhr.onload = () => { | |
| if (xhr.status >= 200 && xhr.status < 300) { | |
| try { | |
| resolve(JSON.parse(xhr.responseText)); | |
| } catch (err) { | |
| reject(new Error('Sunucu yanıtı okunamadı.')); | |
| } | |
| } else { | |
| reject(new Error(`Sunucu hatası (${xhr.status}): ${xhr.responseText}`)); | |
| } | |
| }; | |
| xhr.onerror = () => reject(new Error('Ağ hatası — yükleme başarısız oldu.')); | |
| xhr.send(formData); | |
| }); | |
| } | |
| function resetButton() { | |
| buildBtn.disabled = false; | |
| buildBtnLabel.textContent = 'APK Oluştur'; | |
| } | |
| // ------------------------------------------------------------------- | |
| // Server-Sent Events: live log streaming from /api/build/{id}/stream | |
| // ------------------------------------------------------------------- | |
| function streamBuildLog(buildId) { | |
| const source = new EventSource(`/api/build/${buildId}/stream`); | |
| source.onmessage = (event) => { | |
| let payload; | |
| try { | |
| payload = JSON.parse(event.data); | |
| } catch { | |
| return; | |
| } | |
| if (payload.type === 'log') { | |
| appendLogLine(payload.line); | |
| } else if (payload.type === 'error') { | |
| appendLogLine(payload.message, 'error'); | |
| setStatus('failed', 'Hata'); | |
| resetButton(); | |
| source.close(); | |
| } else if (payload.type === 'done') { | |
| source.close(); | |
| resetButton(); | |
| if (payload.status === 'success') { | |
| setStatus('success', 'Tamamlandı'); | |
| appendLogLine('✅ Build tamamlandı.', 'success'); | |
| if (payload.download_url) { | |
| downloadLink.href = payload.download_url; | |
| downloadArea.classList.remove('hidden'); | |
| } | |
| } else { | |
| setStatus('failed', 'Başarısız'); | |
| if (payload.error_message) { | |
| appendLogLine(`❌ ${payload.error_message}`, 'error'); | |
| } | |
| } | |
| } | |
| }; | |
| source.onerror = () => { | |
| // EventSource auto-retries on transient network errors, so this | |
| // only means something is actually wrong (server gone, build id | |
| // invalid) if the connection doesn't recover — close it and let | |
| // the person retry manually rather than retrying forever. | |
| source.close(); | |
| if (statusPill.dataset.state === 'running') { | |
| appendLogLine('Bağlantı kesildi. Sayfayı yenileyip tekrar dene.', 'error'); | |
| setStatus('failed', 'Bağlantı Hatası'); | |
| } | |
| resetButton(); | |
| }; | |
| } | |
| })(); | |