1. fix bulk edit and export Now I can only edit one video at the time overwise the timestamp get overwritten. Save timestamps for each file uploaded
2. in export bulk make clear wich files will get elaborated and whick is being elaborated also add multple progress bar one for bulk one for the actual video and one for actual process (use Material You expresssive and make them clear) 3. remove save marker button; autosave then save edit 4. add paste timestamp button on hover other timestamp input 5. make unlink button not global but add a link icon colored by couple to let understand which timestamps are linked and make the single link removed (link should work that if I edit a timestamp the next/previuos based on which are linked get automatically setted to the next/previous frame timestamp) 6. I'm unable to pit whichever timestamp I want in the timestamp table for example 00:07:23.109 (the one in the frame visualizer) get automatically changed to 00:07:23.110 making the colored border not working correctly sometimes fix both
This commit is contained in:
+137
-31
@@ -27,6 +27,7 @@ import {
|
||||
TableHead,
|
||||
TableRow,
|
||||
TextField,
|
||||
InputAdornment,
|
||||
Tooltip,
|
||||
Toolbar,
|
||||
Typography,
|
||||
@@ -36,6 +37,7 @@ import {
|
||||
Brightness4,
|
||||
Brightness7,
|
||||
CallSplit,
|
||||
ContentPaste,
|
||||
ContentCopy,
|
||||
Delete,
|
||||
Link,
|
||||
@@ -55,6 +57,7 @@ import {
|
||||
getJob,
|
||||
getVideoMetadata,
|
||||
listMarkers,
|
||||
listSegmentEdits,
|
||||
listVideos,
|
||||
replaceMarkers,
|
||||
replaceSegmentEdits,
|
||||
@@ -528,7 +531,7 @@ export default function App() {
|
||||
}, [segments, selectedFrameTime]);
|
||||
|
||||
const frameHighlight = useMemo(() => {
|
||||
const tolerance = frameStep / 2;
|
||||
const tolerance = Math.max(frameStep / 2, 0.005);
|
||||
for (let i = 0; i < segments.length; i += 1) {
|
||||
const segment = segments[i];
|
||||
if (Math.abs(selectedFrameTime - segment.start) <= tolerance) {
|
||||
@@ -655,6 +658,25 @@ export default function App() {
|
||||
try {
|
||||
const response = await listMarkers(item.id);
|
||||
setMarkers(normalizeMarkers(response.markers ?? []));
|
||||
|
||||
const editsResponse = await listSegmentEdits(item.id);
|
||||
if (editsResponse.segments && editsResponse.segments.length > 0) {
|
||||
setIsLinked(false);
|
||||
setCustomSegmentRows(
|
||||
editsResponse.segments.map((se) => ({
|
||||
kind: se.segment_key.startsWith("segment_") ? "core" : (se.segment_key as any),
|
||||
label: se.segment_key,
|
||||
start: se.start_seconds,
|
||||
end: se.end_seconds,
|
||||
startEditable: true,
|
||||
endEditable: true,
|
||||
coreIndex: se.segment_key.startsWith("segment_") ? parseInt(se.segment_key.split("_")[1]) - 1 : undefined,
|
||||
}))
|
||||
);
|
||||
} else {
|
||||
setIsLinked(true);
|
||||
setCustomSegmentRows([]);
|
||||
}
|
||||
} catch (err) {
|
||||
setError((err as Error).message);
|
||||
}
|
||||
@@ -811,7 +833,7 @@ export default function App() {
|
||||
return;
|
||||
}
|
||||
|
||||
const snapped = snapToFrame(parsed);
|
||||
const snapped = parsed;
|
||||
|
||||
if (!isLinked) {
|
||||
setCustomSegmentRows((prev) => {
|
||||
@@ -864,10 +886,17 @@ export default function App() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleAddMarker = () => {
|
||||
if (!duration) return;
|
||||
const handleAddMarker = async () => {
|
||||
if (!video || !duration) return;
|
||||
const snapped = snapToFrame(cursorTime);
|
||||
setMarkers((prev) => normalizeMarkers([...prev, snapped]));
|
||||
const nextMarkers = normalizeMarkers([...markers, snapped]);
|
||||
setMarkers(nextMarkers);
|
||||
try {
|
||||
await replaceMarkers(video.id, nextMarkers);
|
||||
setPendingSegmentEditPersist(true);
|
||||
} catch (err) {
|
||||
setError((err as Error).message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGoToTime = (value: number) => {
|
||||
@@ -1390,22 +1419,8 @@ export default function App() {
|
||||
<Button variant="outlined" onClick={handleAddMarker} disabled={!duration}>
|
||||
Add marker at playhead
|
||||
</Button>
|
||||
<Button variant="outlined" onClick={handleSaveMarkers} disabled={!video || markers.length === 0}>
|
||||
Save markers
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={isLinked ? <LinkOff /> : <Link />}
|
||||
onClick={() => {
|
||||
if (isLinked) {
|
||||
setCustomSegmentRows(derivedSegmentRows);
|
||||
}
|
||||
setIsLinked(!isLinked);
|
||||
}}
|
||||
disabled={!duration}
|
||||
>
|
||||
{isLinked ? "Unlink timestamps" : "Link timestamps"}
|
||||
</Button>
|
||||
|
||||
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
@@ -1600,17 +1615,108 @@ export default function App() {
|
||||
</Button>
|
||||
</Box>
|
||||
{(job?.kind === "split" || job?.kind === "split_all") && job.status !== "completed" && (
|
||||
<Box>
|
||||
<LinearProgress variant="determinate" value={Math.round(job.progress * 100)} />
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
|
||||
{job.message || `Status: ${job.status}`}
|
||||
<Box sx={{ p: 2, bgcolor: "background.paper", borderRadius: 4, border: "1px solid", borderColor: "divider" }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
{job.kind === "split_all" ? "Bulk Export Progress" : "Export Progress"}
|
||||
</Typography>
|
||||
{job.details && (
|
||||
<Stack direction={{ xs: "column", sm: "row" }} spacing={1} sx={{ mt: 1 }}>
|
||||
<Chip size="small" label={`Stage: ${job.details.stage ?? "working"}`} />
|
||||
<Chip size="small" label={`Stage ETA: ${job.details.stage_eta ?? "calculating"}`} />
|
||||
<Chip size="small" label={`Total ETA: ${job.details.total_eta ?? "calculating"}`} />
|
||||
</Stack>
|
||||
|
||||
{job.kind === "split_all" && job.details?.label && (() => {
|
||||
const match = /^(\d+)\/(\d+) (.+)$/.exec(job.details.label);
|
||||
if (match) {
|
||||
const index = Number(match[1]);
|
||||
const total = Number(match[2]);
|
||||
const filename = match[3];
|
||||
const videoProgress = Math.max(0, Math.min(100, (job.progress - (index - 1) / total) * total * 100));
|
||||
|
||||
return (
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
Overall Bulk Progress ({index}/{total} videos)
|
||||
</Typography>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={Math.round(job.progress * 100)}
|
||||
sx={{ height: 12, borderRadius: 6, mb: 2, bgcolor: "primary.light" }}
|
||||
/>
|
||||
|
||||
<Typography variant="body2" color="text.primary" sx={{ fontWeight: "bold" }}>
|
||||
Current Video: {filename}
|
||||
</Typography>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={Math.round(videoProgress)}
|
||||
color="secondary"
|
||||
sx={{ height: 8, borderRadius: 4, mb: 2 }}
|
||||
/>
|
||||
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Current Stage: {job.details.stage || "working"}
|
||||
</Typography>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={job.details.stage_percent || 0}
|
||||
color="info"
|
||||
sx={{ height: 4, borderRadius: 2 }}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
|
||||
{job.kind === "split" && (
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="body2" color="text.primary" gutterBottom>
|
||||
Video Progress
|
||||
</Typography>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={Math.round(job.progress * 100)}
|
||||
color="secondary"
|
||||
sx={{ height: 8, borderRadius: 4, mb: 2 }}
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Current Stage: {job.details?.stage || "working"}
|
||||
</Typography>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={job.details?.stage_percent || 0}
|
||||
color="info"
|
||||
sx={{ height: 4, borderRadius: 2 }}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Stack direction={{ xs: "column", sm: "row" }} spacing={1} sx={{ mt: 2 }}>
|
||||
{job.details?.stage_eta && <Chip size="small" label={`Stage ETA: ${job.details.stage_eta}`} />}
|
||||
{job.details?.total_eta && <Chip size="small" label={`Total ETA: ${job.details.total_eta}`} />}
|
||||
<Chip size="small" color={job.status === "running" ? "primary" : "default"} label={`Status: ${job.status}`} />
|
||||
</Stack>
|
||||
|
||||
{job.kind === "split_all" && (
|
||||
<Box sx={{ mt: 3, p: 2, bgcolor: "background.default", borderRadius: 2 }}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Queue Queue
|
||||
</Typography>
|
||||
<Stack spacing={0.5}>
|
||||
{videoList.map((v, i) => {
|
||||
const match = job.details?.label ? /^(\d+)\/(\d+) (.+)$/.exec(job.details.label) : null;
|
||||
const currentIndex = match ? Number(match[1]) - 1 : -1;
|
||||
const status = i < currentIndex ? "completed" : i === currentIndex ? "running" : "pending";
|
||||
return (
|
||||
<Typography
|
||||
key={v.id}
|
||||
variant="body2"
|
||||
color={status === "completed" ? "success.main" : status === "running" ? "primary.main" : "text.disabled"}
|
||||
sx={{ fontWeight: status === "running" ? "bold" : "normal" }}
|
||||
>
|
||||
{status === "completed" ? "✓ " : status === "running" ? "▶ " : "• "}
|
||||
{v.filename}
|
||||
</Typography>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -168,6 +168,10 @@ export async function uploadVideos(
|
||||
return uploaded;
|
||||
}
|
||||
|
||||
export async function listSegmentEdits(videoId: string): Promise<{ segments: SegmentEdit[] }> {
|
||||
return request(`/api/videos/${videoId}/segment-edits`);
|
||||
}
|
||||
|
||||
export async function replaceSegmentEdits(
|
||||
videoId: string,
|
||||
segments: { segment_key: string; start_seconds: number; end_seconds: number }[]
|
||||
|
||||
Reference in New Issue
Block a user