import React, { useEffect, useMemo, useRef, useState } from "react"; import { Alert, AppBar, Box, Button, Chip, Container, CssBaseline, Drawer, FormControlLabel, Grid, IconButton, LinearProgress, Paper, Slider, Stack, Step, StepContent, StepLabel, Stepper, Switch, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TextField, InputAdornment, Tooltip, Toolbar, Typography, } from "@mui/material"; import { ThemeProvider } from "@mui/material/styles"; import { Brightness4, Brightness7, CallSplit, ContentPaste, ContentCopy, Delete, Link, LinkOff, MyLocation, Settings, SkipNext, SkipPrevious, UploadFile, } from "@mui/icons-material"; import { autoCutVideo, createProject, deleteVideo, getCurrentProject, getJob, getVideoMetadata, listMarkers, listSegmentEdits, listVideos, replaceMarkers, replaceSegmentEdits, splitAllVideos, splitVideo, updateProject, uploadVideos, } from "./api"; import { Job, Project, Video } from "./types"; import { buildTheme, ColorMode } from "./theme"; const formatTime = (value: number) => { const total = Math.max(0, value); const hours = Math.floor(total / 3600); const minutes = Math.floor((total % 3600) / 60); const seconds = total % 60; const padded = seconds.toFixed(3).padStart(6, "0"); return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${padded}`; }; const parseTimecode = (value: string) => { const trimmed = value.trim(); const match = /^(\d+):([0-5]\d):([0-5]\d)(?:\.(\d{1,3}))?$/.exec(trimmed); if (!match) { return null; } const hours = Number(match[1]); const minutes = Number(match[2]); const seconds = Number(match[3]); const millisRaw = match[4] ?? "0"; const millis = Number(millisRaw.padEnd(3, "0")); if (Number.isNaN(hours) || Number.isNaN(minutes) || Number.isNaN(seconds) || Number.isNaN(millis)) { return null; } return hours * 3600 + minutes * 60 + seconds + millis / 1000; }; const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value)); const DEFAULT_PASS1_TEMPLATE = "ffmpeg -hwaccel auto -y -ss {start} -t {duration} -i {input} -map_metadata -1 -map_chapters -1 -an -sn -b:v 2M -x265-params no-slow-firstpass=1:pass=1:open-gop=0:keyint=60:min-keyint=60:scenecut=0:vbv-maxrate=4000:vbv-bufsize=8000:stats={stats}:pools=+ -tune animation -c:v libx265 -f null {null}"; const DEFAULT_PASS2_TEMPLATE = "ffmpeg -hwaccel auto -y -ss {start} -t {duration} -i {input} -map_metadata -1 -map_chapters -1 -map 0:v:0 -map 0:a? -disposition:v:0 default -b:v 2M -x265-params pass=2:open-gop=0:keyint=60:min-keyint=60:scenecut=0:vbv-maxrate=4000:vbv-bufsize=8000:stats={stats}:pools=+ -tune animation -b:a 128k -ac:a 2 -c:v libx265 -c:a libopus -c:s subrip {output}"; export default function App() { const [mode, setMode] = useState("dark"); const theme = useMemo(() => buildTheme(mode), [mode]); const [activeStep, setActiveStep] = useState(0); const [project, setProject] = useState(null); const [projectDrawerOpen, setProjectDrawerOpen] = useState(false); const [projectForm, setProjectForm] = useState({ name: "Season", segments: 4, intro: 90, outro: 90, reencodeEnabled: false, ffmpegPass1Template: DEFAULT_PASS1_TEMPLATE, ffmpegPass2Template: DEFAULT_PASS2_TEMPLATE, }); const [video, setVideo] = useState