add an unlink buttton to unlink timestamps in the segments table
Add progress bar for uploading
This commit is contained in:
+63
-12
@@ -38,6 +38,8 @@ import {
|
|||||||
CallSplit,
|
CallSplit,
|
||||||
ContentCopy,
|
ContentCopy,
|
||||||
Delete,
|
Delete,
|
||||||
|
Link,
|
||||||
|
LinkOff,
|
||||||
MyLocation,
|
MyLocation,
|
||||||
Settings,
|
Settings,
|
||||||
SkipNext,
|
SkipNext,
|
||||||
@@ -133,8 +135,22 @@ export default function App() {
|
|||||||
const [job, setJob] = useState<Job | null>(null);
|
const [job, setJob] = useState<Job | null>(null);
|
||||||
const [outputs, setOutputs] = useState<string[]>([]);
|
const [outputs, setOutputs] = useState<string[]>([]);
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
|
const [uploadProgress, setUploadProgress] = useState(0);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(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<SegmentRow[]>([]);
|
||||||
|
|
||||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -340,16 +356,6 @@ export default function App() {
|
|||||||
return Array.from(markMap.values()).sort((a, b) => a.value - b.value);
|
return Array.from(markMap.values()).sort((a, b) => a.value - b.value);
|
||||||
}, [markers, project, duration]);
|
}, [markers, project, duration]);
|
||||||
|
|
||||||
type SegmentRow = {
|
|
||||||
kind: "intro" | "core" | "outro";
|
|
||||||
label: string;
|
|
||||||
start: number;
|
|
||||||
end: number;
|
|
||||||
startEditable: boolean;
|
|
||||||
endEditable: boolean;
|
|
||||||
coreIndex?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const introBoundary = useMemo(() => {
|
const introBoundary = useMemo(() => {
|
||||||
if (!project) return 0;
|
if (!project) return 0;
|
||||||
const maxDuration = duration || project.intro_seconds;
|
const maxDuration = duration || project.intro_seconds;
|
||||||
@@ -394,7 +400,7 @@ export default function App() {
|
|||||||
});
|
});
|
||||||
}, [duration, coreBoundaries, frameStep]);
|
}, [duration, coreBoundaries, frameStep]);
|
||||||
|
|
||||||
const segmentRows = useMemo(() => {
|
const derivedSegmentRows = useMemo(() => {
|
||||||
if (!duration || !project || coreBoundaries.length < 2) return [] as SegmentRow[];
|
if (!duration || !project || coreBoundaries.length < 2) return [] as SegmentRow[];
|
||||||
const introEnd = Math.max(0, introBoundary - frameStep);
|
const introEnd = Math.max(0, introBoundary - frameStep);
|
||||||
const outroStart = clamp(alignedOutroBoundary, 0, duration);
|
const outroStart = clamp(alignedOutroBoundary, 0, duration);
|
||||||
@@ -430,6 +436,8 @@ export default function App() {
|
|||||||
return rows;
|
return rows;
|
||||||
}, [duration, project, coreBoundaries, introBoundary, alignedOutroBoundary, lastFrameTime, frameStep, segments]);
|
}, [duration, project, coreBoundaries, introBoundary, alignedOutroBoundary, lastFrameTime, frameStep, segments]);
|
||||||
|
|
||||||
|
const segmentRows = isLinked ? derivedSegmentRows : customSegmentRows;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!segmentRows.length) {
|
if (!segmentRows.length) {
|
||||||
setSegmentDrafts([]);
|
setSegmentDrafts([]);
|
||||||
@@ -602,9 +610,12 @@ export default function App() {
|
|||||||
if (videoFiles.length === 0) return;
|
if (videoFiles.length === 0) return;
|
||||||
setError(null);
|
setError(null);
|
||||||
setIsUploading(true);
|
setIsUploading(true);
|
||||||
|
setUploadProgress(0);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const uploadedItems = await uploadVideos(videoFiles);
|
const uploadedItems = await uploadVideos(videoFiles, (index, total, progress) => {
|
||||||
|
setUploadProgress((index + progress) / total);
|
||||||
|
});
|
||||||
const uploaded = uploadedItems[0];
|
const uploaded = uploadedItems[0];
|
||||||
await refreshVideos();
|
await refreshVideos();
|
||||||
if (uploaded) {
|
if (uploaded) {
|
||||||
@@ -616,6 +627,7 @@ export default function App() {
|
|||||||
setOutputs([]);
|
setOutputs([]);
|
||||||
setJob(null);
|
setJob(null);
|
||||||
setOutputPrefix(uploaded.filename.replace(/\.[^.]+$/, ""));
|
setOutputPrefix(uploaded.filename.replace(/\.[^.]+$/, ""));
|
||||||
|
setIsLinked(true);
|
||||||
}
|
}
|
||||||
setVideoFiles([]);
|
setVideoFiles([]);
|
||||||
setPreviewUrl("");
|
setPreviewUrl("");
|
||||||
@@ -639,6 +651,7 @@ export default function App() {
|
|||||||
setOutputPrefix(item.filename.replace(/\.[^.]+$/, ""));
|
setOutputPrefix(item.filename.replace(/\.[^.]+$/, ""));
|
||||||
setVideoFiles([]);
|
setVideoFiles([]);
|
||||||
setPreviewUrl("");
|
setPreviewUrl("");
|
||||||
|
setIsLinked(true);
|
||||||
try {
|
try {
|
||||||
const response = await listMarkers(item.id);
|
const response = await listMarkers(item.id);
|
||||||
setMarkers(normalizeMarkers(response.markers ?? []));
|
setMarkers(normalizeMarkers(response.markers ?? []));
|
||||||
@@ -799,6 +812,17 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const snapped = snapToFrame(parsed);
|
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 boundaryIndex = -1;
|
||||||
let boundaryValue = snapped;
|
let boundaryValue = snapped;
|
||||||
|
|
||||||
@@ -854,6 +878,12 @@ export default function App() {
|
|||||||
|
|
||||||
const handleDeleteSegment = async (row: SegmentRow) => {
|
const handleDeleteSegment = async (row: SegmentRow) => {
|
||||||
setError(null);
|
setError(null);
|
||||||
|
if (!isLinked) {
|
||||||
|
setCustomSegmentRows((prev) => prev.filter((r) => r !== row));
|
||||||
|
setPendingSegmentEditPersist(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (row.kind === "intro") {
|
if (row.kind === "intro") {
|
||||||
await saveProject({ intro_seconds: 0 });
|
await saveProject({ intro_seconds: 0 });
|
||||||
setPendingSegmentEditPersist(true);
|
setPendingSegmentEditPersist(true);
|
||||||
@@ -1014,6 +1044,14 @@ export default function App() {
|
|||||||
Project settings
|
Project settings
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
{isUploading && (
|
||||||
|
<Box sx={{ mt: 1 }}>
|
||||||
|
<LinearProgress variant="determinate" value={uploadProgress * 100} />
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
|
||||||
|
Uploading... {Math.round(uploadProgress * 100)}%
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
<Box>
|
<Box>
|
||||||
<Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 1 }}>
|
<Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 1 }}>
|
||||||
<Typography variant="subtitle2">Recent uploads</Typography>
|
<Typography variant="subtitle2">Recent uploads</Typography>
|
||||||
@@ -1355,6 +1393,19 @@ export default function App() {
|
|||||||
<Button variant="outlined" onClick={handleSaveMarkers} disabled={!video || markers.length === 0}>
|
<Button variant="outlined" onClick={handleSaveMarkers} disabled={!video || markers.length === 0}>
|
||||||
Save markers
|
Save markers
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={isLinked ? <LinkOff /> : <Link />}
|
||||||
|
onClick={() => {
|
||||||
|
if (isLinked) {
|
||||||
|
setCustomSegmentRows(derivedSegmentRows);
|
||||||
|
}
|
||||||
|
setIsLinked(!isLinked);
|
||||||
|
}}
|
||||||
|
disabled={!duration}
|
||||||
|
>
|
||||||
|
{isLinked ? "Unlink timestamps" : "Link timestamps"}
|
||||||
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
|
|||||||
+47
-3
@@ -116,10 +116,54 @@ export async function replaceMarkers(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function uploadVideos(files: File[]): Promise<Video[]> {
|
export async function uploadVideoWithProgress(file: File, onProgress?: (progress: number) => void): Promise<Video> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open("POST", `${apiBase}/api/videos/upload`);
|
||||||
|
|
||||||
|
xhr.upload.onprogress = (event) => {
|
||||||
|
if (event.lengthComputable && onProgress) {
|
||||||
|
onProgress(event.loaded / event.total);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.onload = () => {
|
||||||
|
if (xhr.status >= 200 && xhr.status < 300) {
|
||||||
|
try {
|
||||||
|
resolve(JSON.parse(xhr.responseText));
|
||||||
|
} catch {
|
||||||
|
reject(new ApiError(xhr.status, "Invalid response"));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let message = `Request failed (${xhr.status})`;
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(xhr.responseText);
|
||||||
|
if (data?.detail) message = data.detail;
|
||||||
|
else if (data?.message) message = data.message;
|
||||||
|
} catch {}
|
||||||
|
reject(new ApiError(xhr.status, message));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.onerror = () => reject(new ApiError(0, "Network error"));
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", file);
|
||||||
|
xhr.send(formData);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uploadVideos(
|
||||||
|
files: File[],
|
||||||
|
onProgress?: (index: number, total: number, fileProgress: number) => void
|
||||||
|
): Promise<Video[]> {
|
||||||
const uploaded: Video[] = [];
|
const uploaded: Video[] = [];
|
||||||
for (const file of files) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
uploaded.push(await uploadVideo(file));
|
const file = files[i];
|
||||||
|
const video = await uploadVideoWithProgress(file, (progress) => {
|
||||||
|
if (onProgress) onProgress(i, files.length, progress);
|
||||||
|
});
|
||||||
|
uploaded.push(video);
|
||||||
}
|
}
|
||||||
return uploaded;
|
return uploaded;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,253 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
def patch_file():
|
||||||
|
with open('frontend/src/App.tsx', 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# 1. Add state variables
|
||||||
|
old1 = ''' const [job, setJob] = useState<Job | null>(null);
|
||||||
|
const [outputs, setOutputs] = useState<string[]>([]);
|
||||||
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const videoRef = useRef<HTMLVideoElement | null>(null);'''
|
||||||
|
new1 = ''' const [job, setJob] = useState<Job | null>(null);
|
||||||
|
const [outputs, setOutputs] = useState<string[]>([]);
|
||||||
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
|
const [uploadProgress, setUploadProgress] = useState(0);
|
||||||
|
const [error, setError] = useState<string | null>(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<SegmentRow[]>([]);
|
||||||
|
|
||||||
|
const videoRef = useRef<HTMLVideoElement | null>(null);'''
|
||||||
|
if old1 not in content: print('Failed old1')
|
||||||
|
content = content.replace(old1, new1)
|
||||||
|
|
||||||
|
# 2. Remove duplicate SegmentRow type declaration
|
||||||
|
old2 = ''' type SegmentRow = {
|
||||||
|
kind: "intro" | "core" | "outro";
|
||||||
|
label: string;
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
startEditable: boolean;
|
||||||
|
endEditable: boolean;
|
||||||
|
coreIndex?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const introBoundary = useMemo(() => {'''
|
||||||
|
new2 = ''' const introBoundary = useMemo(() => {'''
|
||||||
|
if old2 not in content: print('Failed old2')
|
||||||
|
content = content.replace(old2, new2)
|
||||||
|
|
||||||
|
# 3. Rename segmentRows to derivedSegmentRows
|
||||||
|
old3 = ''' const segmentRows = useMemo(() => {
|
||||||
|
if (!duration || !project || coreBoundaries.length < 2) return [] as SegmentRow[];'''
|
||||||
|
new3 = ''' const derivedSegmentRows = useMemo(() => {
|
||||||
|
if (!duration || !project || coreBoundaries.length < 2) return [] as SegmentRow[];'''
|
||||||
|
if old3 not in content: print('Failed old3')
|
||||||
|
content = content.replace(old3, new3)
|
||||||
|
|
||||||
|
# 4. Add segmentRows constant
|
||||||
|
old4 = ''' });
|
||||||
|
return rows;
|
||||||
|
}, [duration, project, coreBoundaries, introBoundary, alignedOutroBoundary, lastFrameTime, frameStep, segments]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!segmentRows.length) {'''
|
||||||
|
new4 = ''' });
|
||||||
|
return rows;
|
||||||
|
}, [duration, project, coreBoundaries, introBoundary, alignedOutroBoundary, lastFrameTime, frameStep, segments]);
|
||||||
|
|
||||||
|
const segmentRows = isLinked ? derivedSegmentRows : customSegmentRows;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!segmentRows.length) {'''
|
||||||
|
if old4 not in content: print('Failed old4')
|
||||||
|
content = content.replace(old4, new4)
|
||||||
|
|
||||||
|
# 5. handleUpload changes
|
||||||
|
old5 = ''' const handleUpload = async () => {
|
||||||
|
if (!project) {
|
||||||
|
setError("Configure project settings before uploading.");
|
||||||
|
setProjectDrawerOpen(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (videoFiles.length === 0) return;
|
||||||
|
setError(null);
|
||||||
|
setIsUploading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const uploadedItems = await uploadVideos(videoFiles);'''
|
||||||
|
new5 = ''' const handleUpload = async () => {
|
||||||
|
if (!project) {
|
||||||
|
setError("Configure project settings before uploading.");
|
||||||
|
setProjectDrawerOpen(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (videoFiles.length === 0) return;
|
||||||
|
setError(null);
|
||||||
|
setIsUploading(true);
|
||||||
|
setUploadProgress(0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const uploadedItems = await uploadVideos(videoFiles, (index, total, progress) => {
|
||||||
|
setUploadProgress((index + progress) / total);
|
||||||
|
});'''
|
||||||
|
if old5 not in content: print('Failed old5')
|
||||||
|
content = content.replace(old5, new5)
|
||||||
|
|
||||||
|
# 6. handleUpload linked reset
|
||||||
|
old6 = ''' setJob(null);
|
||||||
|
setOutputPrefix(uploaded.filename.replace(/\.[^.]+$/, ""));
|
||||||
|
}
|
||||||
|
setVideoFiles([]);'''
|
||||||
|
new6 = ''' setJob(null);
|
||||||
|
setOutputPrefix(uploaded.filename.replace(/\.[^.]+$/, ""));
|
||||||
|
setIsLinked(true);
|
||||||
|
}
|
||||||
|
setVideoFiles([]);'''
|
||||||
|
if old6 not in content: print('Failed old6')
|
||||||
|
content = content.replace(old6, new6)
|
||||||
|
|
||||||
|
# 7. handleSelectVideo reset
|
||||||
|
old7 = ''' setOutputPrefix(item.filename.replace(/\.[^.]+$/, ""));
|
||||||
|
setVideoFiles([]);
|
||||||
|
setPreviewUrl("");
|
||||||
|
try {'''
|
||||||
|
new7 = ''' setOutputPrefix(item.filename.replace(/\.[^.]+$/, ""));
|
||||||
|
setVideoFiles([]);
|
||||||
|
setPreviewUrl("");
|
||||||
|
setIsLinked(true);
|
||||||
|
try {'''
|
||||||
|
if old7 not in content: print('Failed old7')
|
||||||
|
content = content.replace(old7, new7)
|
||||||
|
|
||||||
|
# 8. commitSegmentDraft bypass
|
||||||
|
old8 = ''' const snapped = snapToFrame(parsed);
|
||||||
|
let boundaryIndex = -1;
|
||||||
|
let boundaryValue = snapped;'''
|
||||||
|
new8 = ''' 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;'''
|
||||||
|
if old8 not in content: print('Failed old8')
|
||||||
|
content = content.replace(old8, new8)
|
||||||
|
|
||||||
|
# 9. handleDeleteSegment bypass
|
||||||
|
old9 = ''' const handleDeleteSegment = async (row: SegmentRow) => {
|
||||||
|
setError(null);
|
||||||
|
if (row.kind === "intro") {'''
|
||||||
|
new9 = ''' const handleDeleteSegment = async (row: SegmentRow) => {
|
||||||
|
setError(null);
|
||||||
|
if (!isLinked) {
|
||||||
|
setCustomSegmentRows((prev) => prev.filter((r) => r !== row));
|
||||||
|
setPendingSegmentEditPersist(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row.kind === "intro") {'''
|
||||||
|
if old9 not in content: print('Failed old9')
|
||||||
|
content = content.replace(old9, new9)
|
||||||
|
|
||||||
|
# 10. upload progress UI
|
||||||
|
old10 = ''' <Box>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={handleUpload}
|
||||||
|
disabled={videoFiles.length === 0 || isBusy || !project}
|
||||||
|
>
|
||||||
|
{isUploading ? "Uploading..." : "Upload to Backend"}
|
||||||
|
</Button>
|
||||||
|
<Button sx={{ ml: 1 }} onClick={() => setProjectDrawerOpen(true)}>
|
||||||
|
Project settings
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 1 }}>'''
|
||||||
|
new10 = ''' <Box>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={handleUpload}
|
||||||
|
disabled={videoFiles.length === 0 || isBusy || !project}
|
||||||
|
>
|
||||||
|
{isUploading ? "Uploading..." : "Upload to Backend"}
|
||||||
|
</Button>
|
||||||
|
<Button sx={{ ml: 1 }} onClick={() => setProjectDrawerOpen(true)}>
|
||||||
|
Project settings
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
{isUploading && (
|
||||||
|
<Box sx={{ mt: 1 }}>
|
||||||
|
<LinearProgress variant="determinate" value={uploadProgress * 100} />
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
|
||||||
|
Uploading... {Math.round(uploadProgress * 100)}%
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<Box>
|
||||||
|
<Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 1 }}>'''
|
||||||
|
if old10 not in content: print('Failed old10')
|
||||||
|
content = content.replace(old10, new10)
|
||||||
|
|
||||||
|
# 11. segments table unlink button
|
||||||
|
old11 = ''' <Grid item xs={12}>
|
||||||
|
<Stack direction={{ xs: "column", sm: "row" }} spacing={2} alignItems={{ sm: "center" }}>
|
||||||
|
<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>
|
||||||
|
</Stack>
|
||||||
|
</Grid>'''
|
||||||
|
new11 = ''' <Grid item xs={12}>
|
||||||
|
<Stack direction={{ xs: "column", sm: "row" }} spacing={2} alignItems={{ sm: "center" }}>
|
||||||
|
<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>'''
|
||||||
|
if old11 not in content: print('Failed old11')
|
||||||
|
content = content.replace(old11, new11)
|
||||||
|
|
||||||
|
with open('frontend/src/App.tsx', 'w', encoding='utf-8') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
patch_file()
|
||||||
Reference in New Issue
Block a user