UX: Live progress panel below upload form, stable progress bar, inline preview/download
This commit is contained in:
parent
6e2a13d8a3
commit
c34e4aa376
@ -158,6 +158,17 @@
|
||||
.tab.active { color: var(--text); border-bottom-color: var(--accent); }
|
||||
.hidden { display: none !important; }
|
||||
code { background: var(--panel-2); padding: 1px 6px; border-radius: 3px; font-family: ui-monospace, monospace; font-size: 12px; }
|
||||
.spinner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid var(--border);
|
||||
border-top-color: var(--accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
.progress-bar.smooth { transition: width 0.4s ease; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@ -269,9 +280,22 @@
|
||||
Naredi reel
|
||||
</button>
|
||||
|
||||
<div id="upload-progress" class="hidden" style="margin-top: 12px;">
|
||||
<div class="step" id="upload-status">Nalaganje...</div>
|
||||
<div class="progress"><div class="progress-bar" id="upload-bar"></div></div>
|
||||
<!-- Live progress panel pod upload formo -->
|
||||
<div id="live-progress" class="hidden" style="margin-top: 20px; padding: 14px; background: var(--panel-2); border: 1px solid var(--border); border-radius: 10px;">
|
||||
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 8px;">
|
||||
<div class="spinner" id="live-spinner"></div>
|
||||
<div style="font-weight: 600; font-size: 14px;" id="live-stage">Nalaganje...</div>
|
||||
<span class="badge processing" id="live-badge" style="margin-left: auto;">aktivno</span>
|
||||
</div>
|
||||
<div class="progress" style="margin: 8px 0;">
|
||||
<div class="progress-bar" id="live-bar" style="width: 0%;"></div>
|
||||
</div>
|
||||
<div style="font-size: 12px; color: var(--muted);" id="live-detail">Pripravljam...</div>
|
||||
<div id="live-result" class="hidden" style="margin-top: 12px; display: flex; gap: 8px;">
|
||||
<button class="small" id="live-download" style="display: none;">⬇ Download</button>
|
||||
<button class="small ghost" id="live-preview" style="display: none;">▶ Preview</button>
|
||||
</div>
|
||||
<video id="live-video" class="hidden" controls style="margin-top: 12px; max-height: 400px; width: 100%; border-radius: 8px; background: black;"></video>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -352,71 +376,174 @@
|
||||
return parseFloat(s);
|
||||
}
|
||||
|
||||
// ─── Live progress panel ────────────────────────
|
||||
const livePanel = $("#live-progress");
|
||||
const liveStage = $("#live-stage");
|
||||
const liveDetail = $("#live-detail");
|
||||
const liveBar = $("#live-bar");
|
||||
const liveBadge = $("#live-badge");
|
||||
const liveSpinner = $("#live-spinner");
|
||||
const liveResult = $("#live-result");
|
||||
const liveVideo = $("#live-video");
|
||||
const liveDownloadBtn = $("#live-download");
|
||||
const livePreviewBtn = $("#live-preview");
|
||||
|
||||
function showLive(stage, detail, pct, badge = "aktivno", badgeClass = "processing") {
|
||||
livePanel.classList.remove("hidden");
|
||||
liveStage.textContent = stage;
|
||||
liveDetail.textContent = detail || "";
|
||||
liveBadge.textContent = badge;
|
||||
liveBadge.className = "badge " + badgeClass;
|
||||
if (pct === null || pct === undefined) {
|
||||
// indeterminate
|
||||
liveBar.classList.add("indeterminate");
|
||||
liveBar.classList.remove("smooth");
|
||||
liveBar.style.width = "30%";
|
||||
} else {
|
||||
liveBar.classList.remove("indeterminate");
|
||||
liveBar.classList.add("smooth");
|
||||
liveBar.style.width = Math.max(0, Math.min(100, pct)) + "%";
|
||||
}
|
||||
}
|
||||
|
||||
function liveDone(jobId) {
|
||||
liveSpinner.style.display = "none";
|
||||
liveBar.classList.remove("indeterminate");
|
||||
liveBar.classList.add("smooth");
|
||||
liveBar.style.width = "100%";
|
||||
liveStage.textContent = "✅ Končano";
|
||||
liveBadge.textContent = "končano";
|
||||
liveBadge.className = "badge done";
|
||||
liveDetail.textContent = "Reel je pripravljen za prenos ali predogled.";
|
||||
liveResult.classList.remove("hidden");
|
||||
liveDownloadBtn.style.display = "inline-block";
|
||||
livePreviewBtn.style.display = "inline-block";
|
||||
liveDownloadBtn.onclick = () => window.open(`/api/download/${jobId}`);
|
||||
livePreviewBtn.onclick = () => {
|
||||
liveVideo.src = `/api/preview/${jobId}`;
|
||||
liveVideo.classList.remove("hidden");
|
||||
liveVideo.play();
|
||||
};
|
||||
}
|
||||
|
||||
function liveFail(error) {
|
||||
liveSpinner.style.display = "none";
|
||||
liveBar.classList.remove("indeterminate");
|
||||
liveBar.style.width = "100%";
|
||||
liveBar.style.background = "var(--error)";
|
||||
liveStage.textContent = "❌ Napaka";
|
||||
liveBadge.textContent = "napaka";
|
||||
liveBadge.className = "badge failed";
|
||||
liveDetail.innerHTML = `<span style="color: var(--error)">${error || "Neznana napaka"}</span>`;
|
||||
}
|
||||
|
||||
function liveReset() {
|
||||
livePanel.classList.add("hidden");
|
||||
liveSpinner.style.display = "block";
|
||||
liveBar.style.background = "";
|
||||
liveResult.classList.add("hidden");
|
||||
liveDownloadBtn.style.display = "none";
|
||||
livePreviewBtn.style.display = "none";
|
||||
liveVideo.classList.add("hidden");
|
||||
liveVideo.src = "";
|
||||
}
|
||||
|
||||
// Stage label mapping (server step → friendly slo + percent estimate)
|
||||
const STAGE_INFO = {
|
||||
"Naloženo, čaka na obdelavo": { pct: 30, friendly: "Naloženo, čaka v vrsti" },
|
||||
"V vrsti za obdelavo": { pct: 35, friendly: "V vrsti za obdelavo" },
|
||||
"V vrsti za YouTube prenos": { pct: 30, friendly: "V vrsti za YouTube prenos" },
|
||||
"YouTube download": { pct: 45, friendly: "Prenašam z YouTube..." },
|
||||
"Iščem refren (Whisper + energy)": { pct: 60, friendly: "Iščem refren v pesmi..." },
|
||||
"Reframe + subtitles": { pct: 75, friendly: "Reframe v 9:16 + podnapisi..." },
|
||||
"Končano": { pct: 100, friendly: "✅ Končano" },
|
||||
};
|
||||
|
||||
// ─── Submit ─────────────────────────────────────
|
||||
$("#submit-btn").addEventListener("click", async () => {
|
||||
const isYT = $("#tab-youtube").classList.contains("hidden") === false;
|
||||
const settings = collectSettings();
|
||||
|
||||
$("#submit-btn").disabled = true;
|
||||
$("#upload-progress").classList.remove("hidden");
|
||||
liveReset();
|
||||
|
||||
try {
|
||||
if (isYT) {
|
||||
const url = $("#yt-url").value.trim();
|
||||
if (!url) { alert("Vpiši YouTube URL"); return; }
|
||||
$("#upload-status").textContent = "Pošiljam YouTube job...";
|
||||
if (!url) { alert("Vpiši YouTube URL"); $("#submit-btn").disabled = false; return; }
|
||||
showLive("Pošiljam YouTube job...", url, null);
|
||||
const r = await fetch("/api/youtube", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ url, ...settings }),
|
||||
});
|
||||
if (!r.ok) throw new Error("YouTube submit napaka");
|
||||
if (!r.ok) {
|
||||
const err = await r.text();
|
||||
liveFail("YouTube submit napaka: " + err);
|
||||
return;
|
||||
}
|
||||
const job = await r.json();
|
||||
watchJob(job.id);
|
||||
refreshJobs();
|
||||
} else {
|
||||
if (!pendingFile) { alert("Izberi datoteko"); return; }
|
||||
if (!pendingFile) {
|
||||
alert("Izberi datoteko");
|
||||
$("#submit-btn").disabled = false;
|
||||
return;
|
||||
}
|
||||
const fd = new FormData();
|
||||
fd.append("file", pendingFile);
|
||||
|
||||
showLive("Nalaganje datoteke", `${pendingFile.name} (${(pendingFile.size / 1024 / 1024).toFixed(1)} MB)`, 0);
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.upload.onprogress = e => {
|
||||
if (e.lengthComputable) {
|
||||
const pct = (e.loaded / e.total) * 100;
|
||||
$("#upload-bar").style.width = pct + "%";
|
||||
$("#upload-status").textContent = `Nalagam... ${pct.toFixed(0)}%`;
|
||||
// Upload je 0–25% celotnega procesa
|
||||
const uploadPct = (e.loaded / e.total) * 25;
|
||||
const mbDone = (e.loaded / 1024 / 1024).toFixed(1);
|
||||
const mbTotal = (e.total / 1024 / 1024).toFixed(1);
|
||||
showLive(
|
||||
"Nalaganje datoteke",
|
||||
`${mbDone} / ${mbTotal} MB (${((e.loaded / e.total) * 100).toFixed(0)}%)`,
|
||||
uploadPct
|
||||
);
|
||||
}
|
||||
};
|
||||
xhr.onload = async () => {
|
||||
if (xhr.status !== 200) {
|
||||
alert("Upload napaka: " + xhr.responseText);
|
||||
liveFail("Upload napaka: " + xhr.responseText);
|
||||
$("#submit-btn").disabled = false;
|
||||
return;
|
||||
}
|
||||
const job = JSON.parse(xhr.responseText);
|
||||
$("#upload-status").textContent = "Naloženo, začenjam obdelavo...";
|
||||
const proc = await fetch("/api/process", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ job_id: job.id, ...settings }),
|
||||
});
|
||||
if (!proc.ok) throw new Error("Process start napaka");
|
||||
watchJob(job.id);
|
||||
refreshJobs();
|
||||
showLive("Naloženo, začenjam obdelavo...", `Job ${job.id}`, 28);
|
||||
try {
|
||||
const proc = await fetch("/api/process", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ job_id: job.id, ...settings }),
|
||||
});
|
||||
if (!proc.ok) {
|
||||
liveFail("Process start napaka: " + await proc.text());
|
||||
return;
|
||||
}
|
||||
watchJob(job.id);
|
||||
refreshJobs();
|
||||
} catch (e) {
|
||||
liveFail("Process napaka: " + e.message);
|
||||
}
|
||||
};
|
||||
xhr.onerror = () => liveFail("Upload prekinjen — preveri internet povezavo");
|
||||
xhr.upload.onerror = () => liveFail("Upload napaka pri prenosu");
|
||||
xhr.open("POST", "/api/upload");
|
||||
xhr.send(fd);
|
||||
}
|
||||
} catch (e) {
|
||||
alert("Napaka: " + e.message);
|
||||
liveFail(e.message);
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
$("#upload-progress").classList.add("hidden");
|
||||
$("#submit-btn").disabled = false;
|
||||
pendingFile = null;
|
||||
fileInput.value = "";
|
||||
dz.querySelector("div").textContent = "Klikni ali povleci video sem";
|
||||
}, 2000);
|
||||
// Submit button enable tudi če napaka, da lahko ponovno poskusi
|
||||
setTimeout(() => { $("#submit-btn").disabled = false; }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
@ -427,13 +554,40 @@
|
||||
try {
|
||||
const job = JSON.parse(e.data);
|
||||
updateJobInList(job);
|
||||
|
||||
// Update live panel
|
||||
const step = job.current_step || "";
|
||||
const info = STAGE_INFO[step] || { pct: null, friendly: step };
|
||||
if (job.status === "done") {
|
||||
liveDone(jobId);
|
||||
$("#submit-btn").disabled = false;
|
||||
// Reset upload form
|
||||
pendingFile = null;
|
||||
fileInput.value = "";
|
||||
dz.querySelector("div").textContent = "Klikni ali povleci video sem";
|
||||
} else if (job.status === "failed") {
|
||||
liveFail(job.error || "Obdelava ni uspela");
|
||||
$("#submit-btn").disabled = false;
|
||||
} else {
|
||||
showLive(info.friendly, `Job ${job.id} · ${job.status}`, info.pct);
|
||||
}
|
||||
|
||||
if (job.status === "done" || job.status === "failed") {
|
||||
evt.close();
|
||||
refreshJobs();
|
||||
}
|
||||
} catch {}
|
||||
} catch (err) {
|
||||
console.error("SSE parse err:", err);
|
||||
}
|
||||
};
|
||||
evt.onerror = () => {
|
||||
evt.close();
|
||||
// SSE may close at end of stream — final fetch to confirm
|
||||
fetch(`/api/jobs/${jobId}`).then(r => r.json()).then(job => {
|
||||
if (job.status === "done") liveDone(jobId);
|
||||
else if (job.status === "failed") liveFail(job.error || "");
|
||||
}).catch(() => {});
|
||||
};
|
||||
evt.onerror = () => evt.close();
|
||||
}
|
||||
|
||||
// ─── Jobs list ──────────────────────────────────
|
||||
|
||||
Loading…
Reference in New Issue
Block a user