0fcb77b991
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Successful in 1m6s
CI Build & Deploy Pipeline / Build Staging Web Image (push) Successful in 1m49s
CI Build & Deploy Pipeline / Build Production API Image (push) Has been skipped
CI Build & Deploy Pipeline / Build Production Web Image (push) Has been skipped
CI Build & Deploy Pipeline / Build Production Worker Image (push) Has been skipped
CI Build & Deploy Pipeline / Deploy Production (push) Has been skipped
CI Build & Deploy Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Validate Code Quality And Tests (push) Successful in 3m1s
CI/CD Pipeline / Unit Tests (push) Successful in 3m20s
CI/CD Pipeline / Integration Tests (push) Successful in 1m29s
CI Build & Deploy Pipeline / Build Staging API Image (push) Successful in 7m4s
CI Build & Deploy Pipeline / Build Staging Worker Image (push) Successful in 7m54s
CI Build & Deploy Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 45s
CI Build & Deploy Pipeline / Staging E2E Tests (push) Failing after 2m50s
CI Build & Deploy Pipeline / Staging API Integration Tests (push) Successful in 3m28s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
152 lines
4.4 KiB
TypeScript
152 lines
4.4 KiB
TypeScript
/**
|
|
* 片段调速面板 — Drawer 形式
|
|
* 速度滑块(0.25x ~ 4x)+ 预设快捷按钮 + 音调修正开关
|
|
* 支持应用到当前片段 / 所有片段
|
|
*/
|
|
import React, { useCallback } from "react"
|
|
import { Drawer, Slider } from "antd"
|
|
import type { SpeedConfig } from "../types"
|
|
import { DEFAULT_SPEED } from "../types"
|
|
|
|
/* ──────────── 预设速度 ──────────── */
|
|
const SPEED_PRESETS: { rate: number; label: string }[] = [
|
|
{ rate: 0.5, label: "0.5x" },
|
|
{ rate: 1.0, label: "1x" },
|
|
{ rate: 1.5, label: "1.5x" },
|
|
{ rate: 2.0, label: "2x" },
|
|
]
|
|
|
|
/* ──────────── Props ──────────── */
|
|
interface SpeedPanelProps {
|
|
open: boolean
|
|
onClose: () => void
|
|
/** 当前片段调速配置 */
|
|
config: SpeedConfig
|
|
onChange: (config: SpeedConfig) => void
|
|
/** 应用到所有片段 */
|
|
onApplyAll?: (config: SpeedConfig) => void
|
|
}
|
|
|
|
const SpeedPanel: React.FC<SpeedPanelProps> = ({ open, onClose, config, onChange, onApplyAll }) => {
|
|
/* ── 修改速度 ── */
|
|
const handleChangeRate = useCallback(
|
|
(rate: number) => {
|
|
onChange({ ...config, rate })
|
|
},
|
|
[config, onChange],
|
|
)
|
|
|
|
/* ── 切换音调修正 ── */
|
|
const handleTogglePitch = useCallback(() => {
|
|
onChange({ ...config, pitchCorrection: !config.pitchCorrection })
|
|
}, [config, onChange])
|
|
|
|
/* ── 选择预设 ── */
|
|
const handlePreset = useCallback(
|
|
(rate: number) => {
|
|
onChange({ ...config, rate })
|
|
},
|
|
[config, onChange],
|
|
)
|
|
|
|
/* ── 应用到所有片段 ── */
|
|
const handleApplyAll = useCallback(() => {
|
|
onApplyAll?.(config)
|
|
}, [config, onApplyAll])
|
|
|
|
/* ── 重置 ── */
|
|
const handleReset = useCallback(() => {
|
|
onChange({ ...DEFAULT_SPEED })
|
|
}, [onChange])
|
|
|
|
/* ── 速度描述文字 ── */
|
|
const speedLabel =
|
|
config.rate < 1
|
|
? "慢速(慢动作)"
|
|
: config.rate === 1
|
|
? "原速"
|
|
: config.rate < 2
|
|
? "快速"
|
|
: "极速"
|
|
|
|
return (
|
|
<Drawer
|
|
title="⚡ 片段调速"
|
|
placement="right"
|
|
width={380}
|
|
open={open}
|
|
onClose={onClose}
|
|
className="speed-panel-drawer"
|
|
>
|
|
{/* ── 速度滑块 ── */}
|
|
<div className="sp-speed-section">
|
|
<div className="sp-speed-header">
|
|
<span className="sp-speed-label">播放速度</span>
|
|
<span className="sp-speed-value">{config.rate.toFixed(2)}x</span>
|
|
</div>
|
|
<Slider
|
|
min={0.25}
|
|
max={4.0}
|
|
step={0.05}
|
|
value={config.rate}
|
|
onChange={handleChangeRate}
|
|
tooltip={{ formatter: (v) => `${(v as number).toFixed(2)}x` }}
|
|
/>
|
|
<div className="sp-speed-marks">
|
|
<span>0.25x</span>
|
|
<span>1x</span>
|
|
<span>2x</span>
|
|
<span>4x</span>
|
|
</div>
|
|
<div className="sp-speed-desc">{speedLabel}</div>
|
|
</div>
|
|
|
|
{/* ── 预设快捷按钮 ── */}
|
|
<div className="sp-presets">
|
|
<div className="sp-presets-label">快捷预设</div>
|
|
<div className="sp-presets-row">
|
|
{SPEED_PRESETS.map((p) => (
|
|
<button
|
|
key={p.rate}
|
|
className={`sp-preset-btn${Math.abs(config.rate - p.rate) < 0.01 ? " active" : ""}`}
|
|
onClick={() => handlePreset(p.rate)}
|
|
>
|
|
{p.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* ── 音调修正开关 ── */}
|
|
<div className="sp-pitch-section">
|
|
<div className="sp-pitch-info">
|
|
<span className="sp-pitch-label">音调修正</span>
|
|
<span className="sp-pitch-desc">
|
|
{config.pitchCorrection ? "变速不变调(推荐)" : "变速同时变调"}
|
|
</span>
|
|
</div>
|
|
<div
|
|
className={`ep-toggle${config.pitchCorrection ? " active" : ""}`}
|
|
onClick={handleTogglePitch}
|
|
>
|
|
<div className="ep-toggle-knob" />
|
|
</div>
|
|
</div>
|
|
|
|
{/* ── 底部操作 ── */}
|
|
<div className="sp-footer">
|
|
<button className="sp-reset-btn" onClick={handleReset}>
|
|
重置原速
|
|
</button>
|
|
{onApplyAll && (
|
|
<button className="sp-apply-all-btn" onClick={handleApplyAll}>
|
|
应用到所有片段
|
|
</button>
|
|
)}
|
|
</div>
|
|
</Drawer>
|
|
)
|
|
}
|
|
|
|
export default SpeedPanel
|