diff --git a/templates/index.html b/templates/index.html
index 4eee1b8..6446353 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -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; }
@@ -269,9 +280,22 @@
Naredi reel
-
-
Nalaganje...
-
+
+
+
+
+
Nalaganje...
+
aktivno
+
+
+
Pripravljam...
+
+
+
+
+
@@ -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 = `
${error || "Neznana napaka"}`;
+ }
+
+ 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 ──────────────────────────────────