import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type { Background, ExportJob, Layout, OutputSettings, PublicConfig, Segment, Take, TakeCrop, Transcript, Video } from '../../shared/types.ts'; import { MATTE_RESOLUTIONS, OUTPUT_HEIGHTS, backgroundOf, cropOf, exportSignature, matteBelowSetting, systemVolumeOf } from '../../shared/types.ts'; import { computeGeometry, placeCropped } from '../../shared/layout.ts'; import { cutRange, locate, moveSegment, randomId, segDuration, segmentStarts, splitAt, timelineDuration } from '../../shared/timeline.ts'; import { navigate } from '../App.tsx'; import { api } from '../api.ts'; import { Icon } from '../components/Icon.tsx'; import { CropDialog } from '../components/CropDialog.tsx'; import { ConfirmDialog, type ConfirmRequest } from '../components/Modal.tsx'; import { Stage, captureFrame } from '../components/Stage.tsx'; import { Timeline, takeHue, type PeaksData } from '../components/Timeline.tsx'; import { TranscriptPanel } from '../components/TranscriptPanel.tsx'; import { decodePeaks, fmtBytes, fmtDateTime, fmtDuration, fmtTimecode, loadPref, savePref } from '../lib/format.ts'; import { TimelinePlayer, usePlayerPlaying, usePlayerTime } from '../lib/player.ts'; type Tab = 'details' | 'transcript' | 'export'; type Marks = { in: number | null; out: number | null }; const NO_MARKS: Marks = { in: null, out: null }; const FRAME = 1 / 30; const isLeft = (c: string) => c === 'tl' || c === 'bl'; // Keep locally edited fields; take server-managed fields (takes, thumbnail, export) from `fresh`. function mergeServer(v: Video, fresh: Video): Video { const takes = JSON.stringify(v.takes) === JSON.stringify(fresh.takes) ? v.takes : fresh.takes; return { ...v, takes, thumbnail: fresh.thumbnail, export: fresh.export }; } export function VideoPage({ videoId }: { videoId: string }) { const [video, setVideo] = useState