new features

This commit is contained in:
Daniil
2026-02-27 23:34:17 +03:00
parent 42ce5fa0fe
commit 71b974903a
191 changed files with 11300 additions and 373 deletions
@@ -0,0 +1,220 @@
"use client"
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 { 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 = ({
videoUrl,
start,
end,
}: {
videoUrl: string
start: number
end: number
}) => {
const currentTime = useMediaState("currentTime")
const playing = useMediaState("playing")
const hasPausedRef = useRef(false)
const playerRef = useRef<HTMLElement | null>(null)
useEffect(() => {
hasPausedRef.current = false
}, [start, end])
useEffect(() => {
if (!playing) return
if (currentTime >= end && !hasPausedRef.current) {
hasPausedRef.current = true
const player = playerRef.current as HTMLElement & {
pause?: () => void
}
player?.pause?.()
}
}, [currentTime, end, playing])
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>
</div>
)
}
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<HTMLTextAreaElement>) => {
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
handleSave()
}
},
[handleSave],
)
return (
<Modal
open={open}
onOpenChange={onOpenChange}
title="Редактировать субтитр"
>
<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>
)}
{splitMode ? (
<SegmentSplitter
segment={editorSegment}
onSplit={handleSplit}
onCancel={() => setSplitMode(false)}
/>
) : (
<>
<textarea
className={styles.textArea}
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={handleKeyDown}
rows={3}
placeholder="Текст субтитра..."
autoFocus
/>
<div className={styles.actions}>
{canSplit && (
<Button
type="button"
variant="outline"
onClick={() => setSplitMode(true)}
className={styles.splitAction}
>
<Scissors size={14} />
Разделить
</Button>
)}
<div className={styles.actionsSpacer} />
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
disabled={saving}
>
Отмена
</Button>
<Button
type="button"
variant="primary"
onClick={handleSave}
disabled={saving}
>
{saving ? (
<LoaderCircle size={16} className={styles.spinner} />
) : null}
Сохранить
</Button>
</div>
</>
)}
</div>
</Modal>
)
}