Add download preview flow and search fallback
Some checks failed
build-push / docker (push) Has been cancelled
Some checks failed
build-push / docker (push) Has been cancelled
This commit is contained in:
@@ -13,6 +13,15 @@ const startTime = document.getElementById("startTime");
|
||||
const endTime = document.getElementById("endTime");
|
||||
const downloadResult = document.getElementById("downloadResult");
|
||||
const cardTemplate = document.getElementById("searchCardTemplate");
|
||||
const previewModal = document.getElementById("previewModal");
|
||||
const previewTitle = document.getElementById("previewTitle");
|
||||
const previewThumbnail = document.getElementById("previewThumbnail");
|
||||
const previewDuration = document.getElementById("previewDuration");
|
||||
const qualitySelect = document.getElementById("qualitySelect");
|
||||
const confirmDownload = document.getElementById("confirmDownload");
|
||||
const closePreviewModal = document.getElementById("closePreviewModal");
|
||||
|
||||
let pendingDownload = null;
|
||||
|
||||
function setStatus(label, progress) {
|
||||
statusLabel.textContent = label;
|
||||
@@ -97,7 +106,35 @@ async function uploadFile(file) {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
uploadResult.textContent = "uploading...";
|
||||
await api("/api/upload", { method: "POST", body: formData });
|
||||
try {
|
||||
await api("/api/upload", { method: "POST", body: formData });
|
||||
} catch (error) {
|
||||
uploadResult.textContent = error.message;
|
||||
}
|
||||
}
|
||||
|
||||
function openPreviewModal(preview) {
|
||||
previewTitle.textContent = preview.title;
|
||||
previewThumbnail.src = preview.thumbnail;
|
||||
previewThumbnail.alt = preview.title;
|
||||
previewDuration.textContent = preview.duration;
|
||||
qualitySelect.innerHTML = "";
|
||||
for (const item of preview.qualities || []) {
|
||||
const option = document.createElement("option");
|
||||
option.value = item.value;
|
||||
option.textContent = item.label;
|
||||
qualitySelect.appendChild(option);
|
||||
}
|
||||
startTime.value = preview.startDefault || "00:00:00";
|
||||
endTime.value = preview.endDefault || "00:00:00";
|
||||
previewModal.classList.remove("hidden");
|
||||
previewModal.classList.add("flex");
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
previewModal.classList.add("hidden");
|
||||
previewModal.classList.remove("flex");
|
||||
pendingDownload = null;
|
||||
}
|
||||
|
||||
dropzone.addEventListener("dragover", (event) => {
|
||||
@@ -138,21 +175,49 @@ downloadForm.addEventListener("submit", async (event) => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
pendingDownload = { url: downloadUrl.value, force };
|
||||
downloadResult.textContent = "loading preview...";
|
||||
const preview = await api("/api/download/preview", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ url: downloadUrl.value }),
|
||||
});
|
||||
openPreviewModal(preview);
|
||||
downloadResult.textContent = "preview loaded";
|
||||
} catch (error) {
|
||||
downloadResult.textContent = error.message;
|
||||
}
|
||||
});
|
||||
|
||||
confirmDownload.addEventListener("click", async () => {
|
||||
if (!pendingDownload) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const data = await api("/api/download", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
url: downloadUrl.value,
|
||||
url: pendingDownload.url,
|
||||
start: startTime.value,
|
||||
end: endTime.value,
|
||||
force,
|
||||
quality: qualitySelect.value,
|
||||
force: pendingDownload.force,
|
||||
}),
|
||||
});
|
||||
closeModal();
|
||||
downloadResult.textContent = data.message;
|
||||
} catch (error) {
|
||||
downloadResult.textContent = error.message;
|
||||
}
|
||||
});
|
||||
|
||||
closePreviewModal.addEventListener("click", closeModal);
|
||||
previewModal.addEventListener("click", (event) => {
|
||||
if (event.target === previewModal) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
connectWS();
|
||||
setStatus("idle", 0);
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div>
|
||||
<p class="text-xs uppercase tracking-[0.4em] text-zinc-500">AI Media Asset Ingest Hub</p>
|
||||
<h1 class="mt-3 text-3xl font-semibold tracking-tight text-white">Multimodal Discovery, Drag Upload, Direct Clip Ingest</h1>
|
||||
<h1 class="mt-3 text-3xl font-semibold tracking-tight text-white">SAVE THE NURSE AI Search</h1>
|
||||
</div>
|
||||
<div class="w-full max-w-md">
|
||||
<div class="mb-2 flex items-center justify-between text-xs uppercase tracking-[0.3em] text-zinc-500">
|
||||
@@ -62,9 +62,9 @@
|
||||
<input id="downloadUrl" type="url" placeholder="https://..." class="w-full rounded-2xl border border-white/10 bg-black/40 px-4 py-3 text-sm text-white placeholder:text-zinc-500" />
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<input id="startTime" type="text" value="00:00:00" class="rounded-2xl border border-white/10 bg-black/40 px-4 py-3 text-sm text-white" />
|
||||
<input id="endTime" type="text" value="00:00:10" class="rounded-2xl border border-white/10 bg-black/40 px-4 py-3 text-sm text-white" />
|
||||
<input id="endTime" type="text" value="00:00:00" class="rounded-2xl border border-white/10 bg-black/40 px-4 py-3 text-sm text-white" />
|
||||
</div>
|
||||
<button class="w-full rounded-2xl border border-white px-5 py-3 text-sm font-medium text-white transition hover:bg-white hover:text-black">Queue Clip Download</button>
|
||||
<button class="w-full rounded-2xl border border-white px-5 py-3 text-sm font-medium text-white transition hover:bg-white hover:text-black">Preview & Queue</button>
|
||||
</form>
|
||||
<p id="downloadResult" class="mt-3 text-sm text-zinc-400"></p>
|
||||
</article>
|
||||
@@ -72,6 +72,36 @@
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div id="previewModal" class="fixed inset-0 z-50 hidden items-center justify-center bg-black/80 px-4">
|
||||
<div class="w-full max-w-2xl rounded-3xl border border-white/10 bg-zinc-950 p-5 shadow-2xl">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p class="text-xs uppercase tracking-[0.3em] text-zinc-500">Download Preview</p>
|
||||
<h3 id="previewTitle" class="mt-2 text-xl font-semibold text-white"></h3>
|
||||
</div>
|
||||
<button id="closePreviewModal" class="rounded-full border border-white/10 px-3 py-2 text-xs uppercase tracking-[0.2em] text-zinc-300">Close</button>
|
||||
</div>
|
||||
<div class="mt-5 grid gap-5 md:grid-cols-[1.1fr_0.9fr]">
|
||||
<div class="overflow-hidden rounded-3xl border border-white/10 bg-black/30">
|
||||
<img id="previewThumbnail" class="aspect-video h-full w-full object-cover" alt="" />
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-2xl border border-white/10 bg-white/[0.03] p-4">
|
||||
<div class="flex items-center justify-between text-sm text-zinc-400">
|
||||
<span>Detected duration</span>
|
||||
<span id="previewDuration"></span>
|
||||
</div>
|
||||
</div>
|
||||
<label class="block space-y-2">
|
||||
<span class="text-sm text-zinc-400">Quality</span>
|
||||
<select id="qualitySelect" class="w-full rounded-2xl border border-white/10 bg-black/40 px-4 py-3 text-sm text-white"></select>
|
||||
</label>
|
||||
<button id="confirmDownload" class="w-full rounded-2xl border border-white bg-white px-5 py-3 text-sm font-medium text-black transition hover:bg-zinc-200">Confirm Download</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template id="searchCardTemplate">
|
||||
<a target="_blank" rel="noreferrer" class="group overflow-hidden rounded-3xl border border-white/10 bg-black/30 transition hover:border-white/30">
|
||||
<div class="relative aspect-video overflow-hidden bg-zinc-900">
|
||||
|
||||
Reference in New Issue
Block a user