"use client" import type { ISegmentEditModalProps } from "./SegmentEditModal.d" import type { JSX } from "react" import { LoaderCircle, Pause, Play, Scissors } from "lucide-react" import { FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from "react" import { Button, Modal } from "@shared/ui" import { type EditorSegment, secondsToTimecode, splitSegmentAtMarkers, } from "@shared/lib/transcriptionDocument" import { SegmentSplitter } from "@features/project/SegmentSplitter" import styles from "./SegmentEditModal.module.scss" const SegmentPlayer: FunctionComponent<{ videoUrl: string start: number end: number }> = ({ videoUrl, start, end }) => { const videoRef = useRef(null) const trackRef = useRef(null) const rafRef = useRef(0) const [currentTime, setCurrentTime] = useState(start) const [playing, setPlaying] = useState(false) const [dragging, setDragging] = useState(false) const duration = end - start const progress = duration > 0 ? Math.min(Math.max((currentTime - start) / duration, 0), 1) : 0 /* Time tracking via rAF — only runs while playing or dragging */ useEffect(() => { if (!playing && !dragging) return const video = videoRef.current if (!video) return const tick = () => { setCurrentTime(video.currentTime) if (video.currentTime >= end && !video.paused) { video.pause() setPlaying(false) } rafRef.current = requestAnimationFrame(tick) } rafRef.current = requestAnimationFrame(tick) return () => cancelAnimationFrame(rafRef.current) }, [playing, dragging, end]) /* Set initial time once video is ready */ useEffect(() => { const video = videoRef.current if (!video) return const onLoaded = () => { video.currentTime = start } video.addEventListener("loadedmetadata", onLoaded) if (video.readyState >= 1) onLoaded() return () => video.removeEventListener("loadedmetadata", onLoaded) }, [start]) const togglePlay = useCallback(() => { const video = videoRef.current if (!video) return if (video.paused) { if (video.currentTime >= end) video.currentTime = start video.play() setPlaying(true) } else { video.pause() setPlaying(false) } }, [start, end]) const seekToPosition = useCallback( (clientX: number) => { const track = trackRef.current const video = videoRef.current if (!track || !video || duration <= 0) return const rect = track.getBoundingClientRect() const fraction = Math.min( Math.max((clientX - rect.left) / rect.width, 0), 1, ) video.currentTime = start + fraction * duration }, [start, duration], ) const handleTrackMouseDown = useCallback( (e: React.MouseEvent) => { e.preventDefault() setDragging(true) seekToPosition(e.clientX) }, [seekToPosition], ) useEffect(() => { if (!dragging) return const handleMouseMove = (e: MouseEvent) => seekToPosition(e.clientX) const handleMouseUp = () => setDragging(false) window.addEventListener("mousemove", handleMouseMove) window.addEventListener("mouseup", handleMouseUp) return () => { window.removeEventListener("mousemove", handleMouseMove) window.removeEventListener("mouseup", handleMouseUp) } }, [dragging, seekToPosition]) return (
{secondsToTimecode(Math.max(currentTime, start))}
{secondsToTimecode(end)}
) } export const SegmentEditModal: FunctionComponent< ISegmentEditModalProps > = ({ open, onOpenChange, videoUrl, segment, onSave, onSplit }): JSX.Element => { const [text, setText] = useState(segment.text) const [saving, setSaving] = useState(false) const [splitMode, setSplitMode] = useState(false) const canSplit = !!onSplit && !!segment.words && segment.words.length >= 2 useEffect(() => { if (open) { setText(segment.text) setSplitMode(false) } }, [open, segment.text]) const editorSegment: EditorSegment = useMemo( () => ({ startTime: secondsToTimecode(segment.start), endTime: secondsToTimecode(segment.end), text: segment.text, words: segment.words, }), [segment], ) const handleSave = useCallback(async () => { setSaving(true) try { await onSave(text) onOpenChange(false) } finally { setSaving(false) } }, [text, onSave, onOpenChange]) const handleSplit = useCallback( async (newSegments: EditorSegment[]) => { if (!onSplit) return setSaving(true) try { await onSplit( newSegments.map((s) => ({ start: s.words?.[0]?.start ?? segment.start, end: s.words?.[s.words.length - 1]?.end ?? segment.end, text: s.text, words: s.words, })), ) onOpenChange(false) } finally { setSaving(false) } }, [onSplit, onOpenChange, segment], ) const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { e.preventDefault() handleSave() } }, [handleSave], ) return (
{videoUrl && ( )} {splitMode ? ( setSplitMode(false)} /> ) : ( <>