This commit is contained in:
Daniil
2026-04-04 14:51:40 +03:00
parent 10a1d28f77
commit 0523ef3d72
191 changed files with 12065 additions and 2658 deletions
@@ -3,14 +3,7 @@
import type { ISegmentEditModalProps } from "./SegmentEditModal.d"
import type { JSX } from "react"
import { MediaPlayer, MediaProvider, useMediaState } from "@vidstack/react"
import {
DefaultVideoLayout,
defaultLayoutIcons,
} from "@vidstack/react/player/layouts/default"
import "@vidstack/react/player/styles/default/theme.css"
import "@vidstack/react/player/styles/default/layouts/video.css"
import { LoaderCircle, Scissors } from "lucide-react"
import { LoaderCircle, Pause, Play, Scissors } from "lucide-react"
import { FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from "react"
import { Button, Modal } from "@shared/ui"
@@ -23,50 +16,146 @@ import { SegmentSplitter } from "@features/project/SegmentSplitter"
import styles from "./SegmentEditModal.module.scss"
const SegmentPlayer = ({
videoUrl,
start,
end,
}: {
const SegmentPlayer: FunctionComponent<{
videoUrl: string
start: number
end: number
}) => {
const currentTime = useMediaState("currentTime")
const playing = useMediaState("playing")
const hasPausedRef = useRef(false)
const playerRef = useRef<HTMLElement | null>(null)
}> = ({ videoUrl, start, end }) => {
const videoRef = useRef<HTMLVideoElement>(null)
const trackRef = useRef<HTMLDivElement>(null)
const rafRef = useRef<number>(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(() => {
hasPausedRef.current = false
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 (!playing) return
if (currentTime >= end && !hasPausedRef.current) {
hasPausedRef.current = true
const player = playerRef.current as HTMLElement & {
pause?: () => void
}
player?.pause?.()
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)
}
}, [currentTime, end, playing])
}, [dragging, seekToPosition])
return (
<div className={styles.playerWrapper}>
<MediaProvider />
<DefaultVideoLayout
icons={defaultLayoutIcons}
slots={{
settingsMenu: null,
pipButton: null,
fullscreenButton: null,
airPlayButton: null,
googleCastButton: null,
}}
/>
<div className={styles.timeRange}>
{secondsToTimecode(start)} {secondsToTimecode(end)}
<div className={styles.videoArea}>
<video
ref={videoRef}
src={videoUrl}
crossOrigin="anonymous"
playsInline
preload="auto"
className={styles.video}
/>
<button
type="button"
className={styles.playButton}
onClick={togglePlay}
>
{playing ? <Pause size={24} /> : <Play size={24} />}
</button>
<div className={styles.timeRange}>
{secondsToTimecode(start)} {secondsToTimecode(end)}
</div>
</div>
<div className={styles.segmentControls}>
<span className={styles.segmentTime}>
{secondsToTimecode(Math.max(currentTime, start))}
</span>
<div
className={styles.segmentTrack}
ref={trackRef}
onMouseDown={handleTrackMouseDown}
>
<div
className={styles.segmentTrackFill}
style={{ width: `${progress * 100}%` }}
/>
<div
className={styles.segmentTrackThumb}
style={{ left: `${progress * 100}%` }}
/>
</div>
<span className={styles.segmentTime}>
{secondsToTimecode(end)}
</span>
</div>
</div>
)
@@ -147,18 +236,11 @@ export const SegmentEditModal: FunctionComponent<
>
<div className={styles.root} data-testid="SegmentEditModal">
{videoUrl && (
<MediaPlayer
src={videoUrl}
currentTime={segment.start}
className={styles.player}
autoPlay
>
<SegmentPlayer
videoUrl={videoUrl}
start={segment.start}
end={segment.end}
/>
</MediaPlayer>
<SegmentPlayer
videoUrl={videoUrl}
start={segment.start}
end={segment.end}
/>
)}
{splitMode ? (