ui video selector
This commit is contained in:
143
ui/timeline/src/JobPanel.tsx
Normal file
143
ui/timeline/src/JobPanel.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { getPresets, getJobs, createJob, cancelJob } from "./api";
|
||||
import type { MediaAsset, TranscodePreset, TranscodeJob } from "./types";
|
||||
|
||||
interface JobPanelProps {
|
||||
asset: MediaAsset;
|
||||
trimStart: number;
|
||||
trimEnd: number;
|
||||
}
|
||||
|
||||
export default function JobPanel({ asset, trimStart, trimEnd }: JobPanelProps) {
|
||||
const [presets, setPresets] = useState<TranscodePreset[]>([]);
|
||||
const [jobs, setJobs] = useState<TranscodeJob[]>([]);
|
||||
const [selectedPresetId, setSelectedPresetId] = useState<string>("");
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
// Load presets on mount
|
||||
useEffect(() => {
|
||||
getPresets().then(setPresets).catch(console.error);
|
||||
}, []);
|
||||
|
||||
// Poll jobs for this asset
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
|
||||
const fetchJobs = () => {
|
||||
getJobs()
|
||||
.then((allJobs) => {
|
||||
if (active) {
|
||||
setJobs(
|
||||
allJobs.filter((j) => j.source_asset_id === asset.id),
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
};
|
||||
|
||||
fetchJobs();
|
||||
const interval = setInterval(fetchJobs, 3000);
|
||||
return () => {
|
||||
active = false;
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [asset.id]);
|
||||
|
||||
const hasTrim = trimStart > 0 || (asset.duration != null && trimEnd < asset.duration);
|
||||
const hasPreset = selectedPresetId !== "";
|
||||
const canSubmit = hasTrim || hasPreset;
|
||||
|
||||
const buttonLabel = hasPreset
|
||||
? "Transcode"
|
||||
: hasTrim
|
||||
? "Trim (Copy)"
|
||||
: "Select trim or preset";
|
||||
|
||||
async function handleSubmit() {
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await createJob({
|
||||
source_asset_id: asset.id,
|
||||
preset_id: selectedPresetId || null,
|
||||
trim_start: hasTrim ? trimStart : null,
|
||||
trim_end: hasTrim ? trimEnd : null,
|
||||
});
|
||||
// Refresh jobs immediately
|
||||
const allJobs = await getJobs();
|
||||
setJobs(allJobs.filter((j) => j.source_asset_id === asset.id));
|
||||
} catch (e) {
|
||||
alert(e instanceof Error ? e.message : "Failed to create job");
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCancel(jobId: string) {
|
||||
try {
|
||||
await cancelJob(jobId);
|
||||
const allJobs = await getJobs();
|
||||
setJobs(allJobs.filter((j) => j.source_asset_id === asset.id));
|
||||
} catch (e) {
|
||||
console.error("Cancel failed:", e);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="job-panel">
|
||||
<div className="job-controls">
|
||||
<select
|
||||
value={selectedPresetId}
|
||||
onChange={(e) => setSelectedPresetId(e.target.value)}
|
||||
className="preset-select"
|
||||
>
|
||||
<option value="">No preset (trim only)</option>
|
||||
{presets.map((p) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{p.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={!canSubmit || submitting}
|
||||
className="enqueue-button"
|
||||
>
|
||||
{submitting ? "Submitting..." : buttonLabel}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{jobs.length > 0 && (
|
||||
<div className="job-list">
|
||||
<h3>Jobs</h3>
|
||||
{jobs.map((job) => (
|
||||
<div key={job.id} className="job-item">
|
||||
<div className="job-item-header">
|
||||
<span className="job-filename">{job.output_filename}</span>
|
||||
<span className={`job-status ${job.status}`}>{job.status}</span>
|
||||
</div>
|
||||
{job.status === "processing" && (
|
||||
<div className="job-progress-bar">
|
||||
<div
|
||||
className="job-progress-fill"
|
||||
style={{ width: `${job.progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{(job.status === "pending" || job.status === "processing") && (
|
||||
<button
|
||||
className="job-cancel"
|
||||
onClick={() => handleCancel(job.id)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
)}
|
||||
{job.status === "failed" && job.error_message && (
|
||||
<div className="job-error">{job.error_message}</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user