Implement queued jobs workflow and uploaded source file browser
This commit is contained in:
+110
-4
@@ -166,9 +166,28 @@ class PreviewRequest(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
# Endpoints
|
# Endpoints
|
||||||
|
UPLOAD_METADATA_FILE = os.path.join(upscaler.UPLOAD_DIR, "metadata.json")
|
||||||
|
|
||||||
|
def load_upload_metadata():
|
||||||
|
if os.path.exists(UPLOAD_METADATA_FILE):
|
||||||
|
try:
|
||||||
|
with open(UPLOAD_METADATA_FILE, "r") as f:
|
||||||
|
return json.load(f)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def save_upload_metadata(metadata):
|
||||||
|
try:
|
||||||
|
with open(UPLOAD_METADATA_FILE, "w") as f:
|
||||||
|
json.dump(metadata, f, indent=4)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
@app.post("/api/upload")
|
@app.post("/api/upload")
|
||||||
async def upload_video(file: UploadFile = File(...)):
|
async def upload_video(file: UploadFile = File(...)):
|
||||||
"""Upload video to workspace directory and parse metadata"""
|
"""Upload video to workspace directory and parse metadata"""
|
||||||
|
import time
|
||||||
file_id = str(uuid.uuid4())
|
file_id = str(uuid.uuid4())
|
||||||
ext = os.path.splitext(file.filename)[1].lower()
|
ext = os.path.splitext(file.filename)[1].lower()
|
||||||
if ext not in [".mp4", ".mkv", ".avi", ".mov", ".webm"]:
|
if ext not in [".mp4", ".mkv", ".avi", ".mov", ".webm"]:
|
||||||
@@ -186,6 +205,18 @@ async def upload_video(file: UploadFile = File(...)):
|
|||||||
os.remove(save_path)
|
os.remove(save_path)
|
||||||
raise HTTPException(status_code=400, detail="Could not read video file metadata. File may be corrupted.")
|
raise HTTPException(status_code=400, detail="Could not read video file metadata. File may be corrupted.")
|
||||||
|
|
||||||
|
# Save upload metadata
|
||||||
|
metadata = load_upload_metadata()
|
||||||
|
metadata[file_id] = {
|
||||||
|
"file_id": file_id,
|
||||||
|
"original_filename": file.filename,
|
||||||
|
"ext": ext,
|
||||||
|
"size_bytes": os.path.getsize(save_path),
|
||||||
|
"upload_time": time.time(),
|
||||||
|
"metadata": info
|
||||||
|
}
|
||||||
|
save_upload_metadata(metadata)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"file_id": file_id,
|
"file_id": file_id,
|
||||||
"filename": file.filename,
|
"filename": file.filename,
|
||||||
@@ -193,6 +224,82 @@ async def upload_video(file: UploadFile = File(...)):
|
|||||||
"metadata": info
|
"metadata": info
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@app.get("/api/uploads")
|
||||||
|
def list_uploads():
|
||||||
|
"""List all uploaded video source files"""
|
||||||
|
metadata = load_upload_metadata()
|
||||||
|
valid_uploads = []
|
||||||
|
metadata_updated = False
|
||||||
|
|
||||||
|
if os.path.exists(upscaler.UPLOAD_DIR):
|
||||||
|
all_files = os.listdir(upscaler.UPLOAD_DIR)
|
||||||
|
|
||||||
|
for filename in all_files:
|
||||||
|
if filename == "metadata.json":
|
||||||
|
continue
|
||||||
|
|
||||||
|
file_path = os.path.join(upscaler.UPLOAD_DIR, filename)
|
||||||
|
file_id, ext = os.path.splitext(filename)
|
||||||
|
|
||||||
|
if file_id in metadata:
|
||||||
|
valid_uploads.append(metadata[file_id])
|
||||||
|
else:
|
||||||
|
info = upscaler.get_video_info(file_path)
|
||||||
|
if info:
|
||||||
|
entry = {
|
||||||
|
"file_id": file_id,
|
||||||
|
"original_filename": filename,
|
||||||
|
"ext": ext,
|
||||||
|
"size_bytes": os.path.getsize(file_path),
|
||||||
|
"upload_time": os.path.getmtime(file_path),
|
||||||
|
"metadata": info
|
||||||
|
}
|
||||||
|
metadata[file_id] = entry
|
||||||
|
valid_uploads.append(entry)
|
||||||
|
metadata_updated = True
|
||||||
|
|
||||||
|
# Clean up missing files from metadata
|
||||||
|
for fid in list(metadata.keys()):
|
||||||
|
ext = metadata[fid].get("ext", ".mp4")
|
||||||
|
expected_file = os.path.join(upscaler.UPLOAD_DIR, f"{fid}{ext}")
|
||||||
|
if not os.path.exists(expected_file):
|
||||||
|
del metadata[fid]
|
||||||
|
metadata_updated = True
|
||||||
|
|
||||||
|
if metadata_updated:
|
||||||
|
save_upload_metadata(metadata)
|
||||||
|
|
||||||
|
# Sort by upload time desc
|
||||||
|
valid_uploads.sort(key=lambda x: x.get("upload_time", 0), reverse=True)
|
||||||
|
return valid_uploads
|
||||||
|
|
||||||
|
@app.delete("/api/uploads/{file_id}")
|
||||||
|
@app.post("/api/uploads/delete/{file_id}")
|
||||||
|
def delete_upload(file_id: str):
|
||||||
|
"""Delete an uploaded source file"""
|
||||||
|
metadata = load_upload_metadata()
|
||||||
|
if file_id not in metadata:
|
||||||
|
exts = [".mp4", ".mkv", ".avi", ".mov", ".webm"]
|
||||||
|
file_path = None
|
||||||
|
for ext in exts:
|
||||||
|
p = os.path.join(upscaler.UPLOAD_DIR, f"{file_id}{ext}")
|
||||||
|
if os.path.exists(p):
|
||||||
|
file_path = p
|
||||||
|
break
|
||||||
|
if not file_path:
|
||||||
|
raise HTTPException(status_code=404, detail="Upload file not found.")
|
||||||
|
os.remove(file_path)
|
||||||
|
return {"file_id": file_id, "status": "deleted"}
|
||||||
|
|
||||||
|
ext = metadata[file_id].get("ext", ".mp4")
|
||||||
|
file_path = os.path.join(upscaler.UPLOAD_DIR, f"{file_id}{ext}")
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
os.remove(file_path)
|
||||||
|
|
||||||
|
del metadata[file_id]
|
||||||
|
save_upload_metadata(metadata)
|
||||||
|
return {"file_id": file_id, "status": "deleted"}
|
||||||
|
|
||||||
@app.post("/api/upscale/start")
|
@app.post("/api/upscale/start")
|
||||||
def start_upscale(req: StartUpscaleRequest):
|
def start_upscale(req: StartUpscaleRequest):
|
||||||
"""Queue upscaling task"""
|
"""Queue upscaling task"""
|
||||||
@@ -405,10 +512,6 @@ def delete_job(job_id: str):
|
|||||||
if not job:
|
if not job:
|
||||||
raise HTTPException(status_code=404, detail="Job not found.")
|
raise HTTPException(status_code=404, detail="Job not found.")
|
||||||
|
|
||||||
# Safely delete uploaded input file
|
|
||||||
if job.video_path:
|
|
||||||
safe_delete_file(job.video_path)
|
|
||||||
|
|
||||||
# Safely delete original preview video if present
|
# Safely delete original preview video if present
|
||||||
for ext in [".mp4", ".mkv", ".avi", ".mov", ".webm"]:
|
for ext in [".mp4", ".mkv", ".avi", ".mov", ".webm"]:
|
||||||
orig_prev_path = os.path.join(upscaler.OUTPUT_DIR, f"original_{job_id}{ext}")
|
orig_prev_path = os.path.join(upscaler.OUTPUT_DIR, f"original_{job_id}{ext}")
|
||||||
@@ -459,6 +562,9 @@ def purge_all_jobs():
|
|||||||
# Reset in-memory database
|
# Reset in-memory database
|
||||||
jobs_db.clear()
|
jobs_db.clear()
|
||||||
|
|
||||||
|
# Reset upload metadata file
|
||||||
|
save_upload_metadata({})
|
||||||
|
|
||||||
return {"status": "all purged"}
|
return {"status": "all purged"}
|
||||||
|
|
||||||
# Websocket endpoint for real-time progress updates
|
# Websocket endpoint for real-time progress updates
|
||||||
|
|||||||
+192
-8
@@ -344,6 +344,7 @@ function handleVideoUpload(file) {
|
|||||||
// Show config view
|
// Show config view
|
||||||
stepUpload.classList.remove("active");
|
stepUpload.classList.remove("active");
|
||||||
stepConfig.classList.add("active");
|
stepConfig.classList.add("active");
|
||||||
|
loadUploadsBrowser();
|
||||||
} else {
|
} else {
|
||||||
alert("Upload failed: " + (JSON.parse(xhr.responseText).detail || xhr.statusText));
|
alert("Upload failed: " + (JSON.parse(xhr.responseText).detail || xhr.statusText));
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
@@ -1025,12 +1026,6 @@ async function checkActiveJobs() {
|
|||||||
|
|
||||||
if (activeJob) {
|
if (activeJob) {
|
||||||
currentJobId = activeJob.job_id;
|
currentJobId = activeJob.job_id;
|
||||||
|
|
||||||
document.querySelectorAll(".step-container").forEach(el => el.classList.remove("active"));
|
|
||||||
stepProgress.classList.add("active");
|
|
||||||
jobIdDisplay.textContent = currentJobId;
|
|
||||||
|
|
||||||
connectProgressWebSocket(currentJobId);
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error checking active jobs:", err);
|
console.error("Error checking active jobs:", err);
|
||||||
@@ -1292,7 +1287,7 @@ async function loadQueue() {
|
|||||||
const jobs = await res.json();
|
const jobs = await res.json();
|
||||||
|
|
||||||
const displayJobs = jobs.filter(job =>
|
const displayJobs = jobs.filter(job =>
|
||||||
["queued", "analyzing", "extracting", "upscaling", "assembling", "failed", "cancelled"].includes(job.status)
|
["queued", "analyzing", "extracting", "upscaling", "assembling", "completed", "failed", "cancelled"].includes(job.status)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (displayJobs.length === 0) {
|
if (displayJobs.length === 0) {
|
||||||
@@ -1323,19 +1318,25 @@ async function loadQueue() {
|
|||||||
|
|
||||||
if (job.status === "failed" && job.error) {
|
if (job.status === "failed" && job.error) {
|
||||||
metaHtml += `<span style="color: var(--danger);"><i class="fa-solid fa-triangle-exclamation"></i> Error: ${job.error}</span>`;
|
metaHtml += `<span style="color: var(--danger);"><i class="fa-solid fa-triangle-exclamation"></i> Error: ${job.error}</span>`;
|
||||||
} else if (job.status !== "queued" && job.status !== "failed" && job.status !== "cancelled") {
|
} else if (job.status !== "queued" && job.status !== "failed" && job.status !== "cancelled" && job.status !== "completed") {
|
||||||
metaHtml += `<span><i class="fa-solid fa-clock"></i> ETA: ${job.eta || 'Calculating...'}</span>`;
|
metaHtml += `<span><i class="fa-solid fa-clock"></i> ETA: ${job.eta || 'Calculating...'}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
let actionsHtml = "";
|
let actionsHtml = "";
|
||||||
if (["queued", "analyzing", "extracting", "upscaling", "assembling"].includes(job.status)) {
|
if (["queued", "analyzing", "extracting", "upscaling", "assembling"].includes(job.status)) {
|
||||||
actionsHtml = `
|
actionsHtml = `
|
||||||
|
<button class="btn btn-primary btn-small btn-view-job" data-id="${job.job_id}">
|
||||||
|
<i class="fa-solid fa-eye"></i> View
|
||||||
|
</button>
|
||||||
<button class="btn btn-danger btn-small btn-abort-job" data-id="${job.job_id}">
|
<button class="btn btn-danger btn-small btn-abort-job" data-id="${job.job_id}">
|
||||||
<i class="fa-solid fa-circle-stop"></i> Abort
|
<i class="fa-solid fa-circle-stop"></i> Abort
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
actionsHtml = `
|
actionsHtml = `
|
||||||
|
<button class="btn btn-secondary btn-small btn-view-job" data-id="${job.job_id}">
|
||||||
|
<i class="fa-solid fa-eye"></i> View
|
||||||
|
</button>
|
||||||
<button class="btn btn-secondary btn-small btn-delete-job" data-id="${job.job_id}">
|
<button class="btn btn-secondary btn-small btn-delete-job" data-id="${job.job_id}">
|
||||||
<i class="fa-solid fa-trash"></i> Clear
|
<i class="fa-solid fa-trash"></i> Clear
|
||||||
</button>
|
</button>
|
||||||
@@ -1398,6 +1399,27 @@ async function loadQueue() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const viewBtn = item.querySelector(".btn-view-job");
|
||||||
|
if (viewBtn) {
|
||||||
|
viewBtn.addEventListener("click", () => {
|
||||||
|
currentJobId = job.job_id;
|
||||||
|
if (socket) {
|
||||||
|
try {
|
||||||
|
socket.close();
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
document.querySelectorAll(".step-container").forEach(el => el.classList.remove("active"));
|
||||||
|
if (job.status === "completed") {
|
||||||
|
document.getElementById("step-finished").classList.add("active");
|
||||||
|
document.getElementById("download-link").href = `/api/download/${job.output_file}`;
|
||||||
|
} else {
|
||||||
|
document.getElementById("step-progress").classList.add("active");
|
||||||
|
document.getElementById("job-id-display").textContent = currentJobId;
|
||||||
|
connectProgressWebSocket(currentJobId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
queueList.appendChild(item);
|
queueList.appendChild(item);
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -1473,11 +1495,173 @@ async function loadModels() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useUploadedVideo(entry) {
|
||||||
|
uploadFileId = entry.file_id;
|
||||||
|
uploadMetadata = entry.metadata;
|
||||||
|
|
||||||
|
// Populate Config Step Info
|
||||||
|
metaName.textContent = entry.original_filename;
|
||||||
|
metaSize.textContent = formatBytes(entry.size_bytes);
|
||||||
|
|
||||||
|
// Populate Accordions Stream details
|
||||||
|
const streams = entry.metadata.streams || [];
|
||||||
|
const videoStream = streams.find(s => s.codec_type === "video") || {};
|
||||||
|
const audioStream = streams.find(s => s.codec_type === "audio") || {};
|
||||||
|
|
||||||
|
// Video stream metadata
|
||||||
|
document.getElementById("meta-res-codec").textContent = (videoStream.codec_name || "unknown").toUpperCase();
|
||||||
|
document.getElementById("meta-res").textContent = `${entry.metadata.width} x ${entry.metadata.height}`;
|
||||||
|
document.getElementById("meta-fps").textContent = `${entry.metadata.fps} FPS`;
|
||||||
|
document.getElementById("meta-duration").textContent = `${entry.metadata.duration}s`;
|
||||||
|
document.getElementById("meta-total-frames").textContent = entry.metadata.total_frames || "-";
|
||||||
|
document.getElementById("meta-aspect-ratio").textContent = videoStream.display_aspect_ratio || "N/A";
|
||||||
|
|
||||||
|
// Audio stream metadata
|
||||||
|
if (audioStream.codec_name) {
|
||||||
|
document.getElementById("meta-audio-codec").textContent = (audioStream.codec_name || "unknown").toUpperCase();
|
||||||
|
document.getElementById("meta-audio-channels").textContent = audioStream.channels || "N/A";
|
||||||
|
document.getElementById("meta-audio-samplerate").textContent = audioStream.sample_rate ? `${audioStream.sample_rate} Hz` : "N/A";
|
||||||
|
document.getElementById("meta-audio-bitrate").textContent = audioStream.bit_rate ? formatBytes(parseInt(audioStream.bit_rate)) + "/s" : "N/A";
|
||||||
|
} else {
|
||||||
|
document.getElementById("meta-audio-codec").textContent = "No Audio Stream";
|
||||||
|
document.getElementById("meta-audio-channels").textContent = "-";
|
||||||
|
document.getElementById("meta-audio-samplerate").textContent = "-";
|
||||||
|
document.getElementById("meta-audio-bitrate").textContent = "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure timeline slider constraints
|
||||||
|
timelineSlider.min = 0.1;
|
||||||
|
timelineSlider.max = entry.metadata.duration - 0.5;
|
||||||
|
timelineSlider.value = (entry.metadata.duration * 0.15).toFixed(2);
|
||||||
|
timelineTime.textContent = `${timelineSlider.value}s`;
|
||||||
|
|
||||||
|
// Configure Trimming panel range values
|
||||||
|
const trimStartRange = document.getElementById("trim-start-range");
|
||||||
|
const trimEndRange = document.getElementById("trim-end-range");
|
||||||
|
const trimStartValue = document.getElementById("trim-start-value");
|
||||||
|
const trimEndValue = document.getElementById("trim-end-value");
|
||||||
|
|
||||||
|
trimStartRange.min = 0;
|
||||||
|
trimStartRange.max = entry.metadata.duration;
|
||||||
|
trimStartRange.value = 0;
|
||||||
|
trimStartValue.textContent = "0.00s";
|
||||||
|
|
||||||
|
trimEndRange.min = 0;
|
||||||
|
trimEndRange.max = entry.metadata.duration;
|
||||||
|
trimEndRange.value = entry.metadata.duration;
|
||||||
|
trimEndValue.textContent = `${entry.metadata.duration.toFixed(2)}s`;
|
||||||
|
|
||||||
|
// Show config view
|
||||||
|
stepUpload.classList.remove("active");
|
||||||
|
stepConfig.classList.add("active");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadUploadsBrowser() {
|
||||||
|
const browserList = document.getElementById("uploads-browser-list");
|
||||||
|
if (!browserList) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/uploads");
|
||||||
|
if (!res.ok) return;
|
||||||
|
|
||||||
|
const uploads = await res.json();
|
||||||
|
if (uploads.length === 0) {
|
||||||
|
browserList.innerHTML = `
|
||||||
|
<div style="text-align: center; padding: 2rem; color: var(--text-muted);">
|
||||||
|
<i class="fa-solid fa-photo-film" style="font-size: 2rem; margin-bottom: 0.5rem; color: rgba(255,255,255,0.15);"></i>
|
||||||
|
<p>No uploaded files found. Upload a video above to get started!</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
browserList.innerHTML = "";
|
||||||
|
|
||||||
|
uploads.forEach(entry => {
|
||||||
|
const item = document.createElement("div");
|
||||||
|
item.className = "queue-item";
|
||||||
|
|
||||||
|
const sizeStr = formatBytes(entry.size_bytes || 0);
|
||||||
|
const durationStr = entry.metadata ? `${entry.metadata.duration}s` : "-";
|
||||||
|
const resStr = entry.metadata ? `${entry.metadata.width}x${entry.metadata.height}` : "-";
|
||||||
|
const fpsStr = entry.metadata ? `${entry.metadata.fps} FPS` : "-";
|
||||||
|
|
||||||
|
item.innerHTML = `
|
||||||
|
<div class="queue-item-details">
|
||||||
|
<div class="queue-item-title">
|
||||||
|
<i class="fa-solid fa-file-video" style="color: var(--primary);"></i> ${entry.original_filename}
|
||||||
|
</div>
|
||||||
|
<div class="queue-item-meta">
|
||||||
|
<span>Size: ${sizeStr}</span>
|
||||||
|
<span>Resolution: ${resStr}</span>
|
||||||
|
<span>Duration: ${durationStr}</span>
|
||||||
|
<span>FPS: ${fpsStr}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="queue-item-status-bar">
|
||||||
|
<div class="queue-item-actions">
|
||||||
|
<button class="btn btn-primary btn-small btn-use-upload" data-id="${entry.file_id}">
|
||||||
|
<i class="fa-solid fa-play"></i> Use
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-danger btn-small btn-delete-upload" data-id="${entry.file_id}">
|
||||||
|
<i class="fa-solid fa-trash"></i> Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
item.querySelector(".btn-use-upload").addEventListener("click", () => {
|
||||||
|
useUploadedVideo(entry);
|
||||||
|
});
|
||||||
|
|
||||||
|
item.querySelector(".btn-delete-upload").addEventListener("click", async () => {
|
||||||
|
if (confirm(`Are you sure you want to delete "${entry.original_filename}" from the workspace?`)) {
|
||||||
|
try {
|
||||||
|
const delRes = await fetch(`/api/uploads/${entry.file_id}`, { method: "DELETE" });
|
||||||
|
if (delRes.ok) {
|
||||||
|
loadUploadsBrowser();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error deleting upload:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
browserList.appendChild(item);
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error loading uploads browser:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function initializeApp() {
|
async function initializeApp() {
|
||||||
await loadModels();
|
await loadModels();
|
||||||
await checkActiveJobs();
|
await checkActiveJobs();
|
||||||
await loadGallery(); // Load enhancement gallery at page load
|
await loadGallery(); // Load enhancement gallery at page load
|
||||||
await loadQueue(); // Load active jobs queue at page load
|
await loadQueue(); // Load active jobs queue at page load
|
||||||
|
await loadUploadsBrowser(); // Load previously uploaded videos browser
|
||||||
|
|
||||||
|
// Register btn-refresh-uploads click handler
|
||||||
|
const refreshUploadsBtn = document.getElementById("refresh-uploads-btn");
|
||||||
|
if (refreshUploadsBtn) {
|
||||||
|
refreshUploadsBtn.addEventListener("click", loadUploadsBrowser);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register back-to-workspace-btn click handler
|
||||||
|
const backToWorkspaceBtn = document.getElementById("back-to-workspace-btn");
|
||||||
|
if (backToWorkspaceBtn) {
|
||||||
|
backToWorkspaceBtn.addEventListener("click", () => {
|
||||||
|
if (socket) {
|
||||||
|
try {
|
||||||
|
socket.close();
|
||||||
|
} catch(e) {}
|
||||||
|
}
|
||||||
|
document.querySelectorAll(".step-container").forEach(el => el.classList.remove("active"));
|
||||||
|
document.getElementById("step-upload").classList.add("active");
|
||||||
|
uploadFileId = null;
|
||||||
|
loadUploadsBrowser();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeApp();
|
initializeApp();
|
||||||
|
|||||||
+21
-1
@@ -49,6 +49,23 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Previously Uploaded Files Browser -->
|
||||||
|
<div class="glass-card" style="margin-top: 2rem; padding: 1.5rem;">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border-color); padding-bottom: 0.75rem; margin-bottom: 1rem;">
|
||||||
|
<h3 style="margin: 0; font-family: var(--font-heading); font-size: 1.25rem;"><i class="fa-solid fa-folder-tree" style="color: var(--primary); margin-right: 0.5rem;"></i> Previously Uploaded Source Videos</h3>
|
||||||
|
<button type="button" id="refresh-uploads-btn" class="btn btn-secondary btn-small">
|
||||||
|
<i class="fa-solid fa-arrows-rotate"></i> Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="uploads-browser-list" style="display: flex; flex-direction: column; gap: 0.75rem;">
|
||||||
|
<!-- Dynamically populated files -->
|
||||||
|
<div style="text-align: center; padding: 2rem; color: var(--text-muted);">
|
||||||
|
<i class="fa-solid fa-photo-film" style="font-size: 2rem; margin-bottom: 0.5rem; color: rgba(255,255,255,0.15);"></i>
|
||||||
|
<p>No uploaded files found. Upload a video above to get started!</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Step 2: Configuration & Preview -->
|
<!-- Step 2: Configuration & Preview -->
|
||||||
@@ -455,7 +472,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Action control -->
|
<!-- Action control -->
|
||||||
<div style="display: flex; justify-content: flex-end;">
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 1.5rem;">
|
||||||
|
<button type="button" id="back-to-workspace-btn" class="btn btn-secondary">
|
||||||
|
<i class="fa-solid fa-arrow-left"></i> Add Another Job
|
||||||
|
</button>
|
||||||
<button type="button" id="cancel-upscale-btn" class="btn btn-danger">
|
<button type="button" id="cancel-upscale-btn" class="btn btn-danger">
|
||||||
<i class="fa-solid fa-circle-stop"></i> Abort Upscaling Job
|
<i class="fa-solid fa-circle-stop"></i> Abort Upscaling Job
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user