diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7549c66..0b68d36 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -38,6 +38,8 @@ import { CallSplit, ContentCopy, Delete, + Link, + LinkOff, MyLocation, Settings, SkipNext, @@ -133,8 +135,22 @@ export default function App() { const [job, setJob] = useState(null); const [outputs, setOutputs] = useState([]); const [isUploading, setIsUploading] = useState(false); + const [uploadProgress, setUploadProgress] = useState(0); const [error, setError] = useState(null); + type SegmentRow = { + kind: "intro" | "core" | "outro"; + label: string; + start: number; + end: number; + startEditable: boolean; + endEditable: boolean; + coreIndex?: number; + }; + + const [isLinked, setIsLinked] = useState(true); + const [customSegmentRows, setCustomSegmentRows] = useState([]); + const videoRef = useRef(null); useEffect(() => { @@ -340,16 +356,6 @@ export default function App() { return Array.from(markMap.values()).sort((a, b) => a.value - b.value); }, [markers, project, duration]); - type SegmentRow = { - kind: "intro" | "core" | "outro"; - label: string; - start: number; - end: number; - startEditable: boolean; - endEditable: boolean; - coreIndex?: number; - }; - const introBoundary = useMemo(() => { if (!project) return 0; const maxDuration = duration || project.intro_seconds; @@ -394,7 +400,7 @@ export default function App() { }); }, [duration, coreBoundaries, frameStep]); - const segmentRows = useMemo(() => { + const derivedSegmentRows = useMemo(() => { if (!duration || !project || coreBoundaries.length < 2) return [] as SegmentRow[]; const introEnd = Math.max(0, introBoundary - frameStep); const outroStart = clamp(alignedOutroBoundary, 0, duration); @@ -430,6 +436,8 @@ export default function App() { return rows; }, [duration, project, coreBoundaries, introBoundary, alignedOutroBoundary, lastFrameTime, frameStep, segments]); + const segmentRows = isLinked ? derivedSegmentRows : customSegmentRows; + useEffect(() => { if (!segmentRows.length) { setSegmentDrafts([]); @@ -602,9 +610,12 @@ export default function App() { if (videoFiles.length === 0) return; setError(null); setIsUploading(true); + setUploadProgress(0); try { - const uploadedItems = await uploadVideos(videoFiles); + const uploadedItems = await uploadVideos(videoFiles, (index, total, progress) => { + setUploadProgress((index + progress) / total); + }); const uploaded = uploadedItems[0]; await refreshVideos(); if (uploaded) { @@ -616,6 +627,7 @@ export default function App() { setOutputs([]); setJob(null); setOutputPrefix(uploaded.filename.replace(/\.[^.]+$/, "")); + setIsLinked(true); } setVideoFiles([]); setPreviewUrl(""); @@ -639,6 +651,7 @@ export default function App() { setOutputPrefix(item.filename.replace(/\.[^.]+$/, "")); setVideoFiles([]); setPreviewUrl(""); + setIsLinked(true); try { const response = await listMarkers(item.id); setMarkers(normalizeMarkers(response.markers ?? [])); @@ -799,6 +812,17 @@ export default function App() { } const snapped = snapToFrame(parsed); + + if (!isLinked) { + setCustomSegmentRows((prev) => { + const next = [...prev]; + next[rowIndex] = { ...next[rowIndex], [field]: snapped }; + return next; + }); + setPendingSegmentEditPersist(true); + return; + } + let boundaryIndex = -1; let boundaryValue = snapped; @@ -854,6 +878,12 @@ export default function App() { const handleDeleteSegment = async (row: SegmentRow) => { setError(null); + if (!isLinked) { + setCustomSegmentRows((prev) => prev.filter((r) => r !== row)); + setPendingSegmentEditPersist(true); + return; + } + if (row.kind === "intro") { await saveProject({ intro_seconds: 0 }); setPendingSegmentEditPersist(true); @@ -1014,6 +1044,14 @@ export default function App() { Project settings + {isUploading && ( + + + + Uploading... {Math.round(uploadProgress * 100)}% + + + )} Recent uploads @@ -1355,6 +1393,19 @@ export default function App() { + diff --git a/frontend/src/api.ts b/frontend/src/api.ts index c07f248..6a0b43c 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -116,10 +116,54 @@ export async function replaceMarkers( }); } -export async function uploadVideos(files: File[]): Promise { +export async function uploadVideoWithProgress(file: File, onProgress?: (progress: number) => void): Promise