improved feature 11 and UI
This commit is contained in:
@ -48,9 +48,12 @@ export default function App() {
|
||||
language,
|
||||
isTranscribing,
|
||||
transcriptionStatus,
|
||||
markInTime,
|
||||
markOutTime,
|
||||
loadVideo,
|
||||
setProjectFilePath,
|
||||
setBackendUrl,
|
||||
clearMarkRange,
|
||||
setTranscription,
|
||||
setTranscriptionModel,
|
||||
setTranscribing,
|
||||
@ -333,6 +336,17 @@ export default function App() {
|
||||
};
|
||||
|
||||
const handleCut = () => {
|
||||
if (markInTime !== null && markOutTime !== null) {
|
||||
const startTime = Math.min(markInTime, markOutTime);
|
||||
const endTime = Math.max(markInTime, markOutTime);
|
||||
if (endTime - startTime >= 0.01) {
|
||||
addCutRange(startTime, endTime);
|
||||
setActivePanel('zones');
|
||||
}
|
||||
clearMarkRange();
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedWordIndices.length > 0) {
|
||||
// If words are selected, apply cut immediately
|
||||
const sorted = [...selectedWordIndices].sort((a, b) => a - b);
|
||||
@ -349,6 +363,17 @@ export default function App() {
|
||||
};
|
||||
|
||||
const handleMute = () => {
|
||||
if (markInTime !== null && markOutTime !== null) {
|
||||
const startTime = Math.min(markInTime, markOutTime);
|
||||
const endTime = Math.max(markInTime, markOutTime);
|
||||
if (endTime - startTime >= 0.01) {
|
||||
addMuteRange(startTime, endTime);
|
||||
setActivePanel('zones');
|
||||
}
|
||||
clearMarkRange();
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedWordIndices.length > 0) {
|
||||
// If words are selected, apply mute immediately
|
||||
const sorted = [...selectedWordIndices].sort((a, b) => a - b);
|
||||
@ -365,6 +390,17 @@ export default function App() {
|
||||
};
|
||||
|
||||
const handleGain = () => {
|
||||
if (markInTime !== null && markOutTime !== null) {
|
||||
const startTime = Math.min(markInTime, markOutTime);
|
||||
const endTime = Math.max(markInTime, markOutTime);
|
||||
if (endTime - startTime >= 0.01) {
|
||||
addGainRange(startTime, endTime, gainModeDb);
|
||||
setActivePanel('zones');
|
||||
}
|
||||
clearMarkRange();
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedWordIndices.length > 0) {
|
||||
const sorted = [...selectedWordIndices].sort((a, b) => a - b);
|
||||
const startTime = words[sorted[0]].start;
|
||||
@ -379,6 +415,17 @@ export default function App() {
|
||||
};
|
||||
|
||||
const handleSpeed = () => {
|
||||
if (markInTime !== null && markOutTime !== null) {
|
||||
const startTime = Math.min(markInTime, markOutTime);
|
||||
const endTime = Math.max(markInTime, markOutTime);
|
||||
if (endTime - startTime >= 0.01) {
|
||||
addSpeedRange(startTime, endTime, speedModeValue);
|
||||
setActivePanel('zones');
|
||||
}
|
||||
clearMarkRange();
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedWordIndices.length > 0) {
|
||||
const sorted = [...selectedWordIndices].sort((a, b) => a - b);
|
||||
const startTime = words[sorted[0]].start;
|
||||
|
||||
@ -278,11 +278,13 @@ export default function WaveformTimeline({
|
||||
const [showMuteZones, setShowMuteZones] = useState(true);
|
||||
const [showGainZones, setShowGainZones] = useState(true);
|
||||
const [showSpeedZones, setShowSpeedZones] = useState(true);
|
||||
const [showAdjustedTimeline, setShowAdjustedTimeline] = useState(false);
|
||||
|
||||
const sourceDuration = duration || waveformDataRef.current?.duration || 0;
|
||||
const timelineCutRanges = showAdjustedTimeline ? cutRanges : [];
|
||||
const { segments: timelineSegments, displayDuration } = useMemo(
|
||||
() => buildTimelineSegments(sourceDuration, cutRanges),
|
||||
[sourceDuration, cutRanges],
|
||||
() => buildTimelineSegments(sourceDuration, timelineCutRanges),
|
||||
[sourceDuration, timelineCutRanges],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@ -447,15 +449,16 @@ export default function WaveformTimeline({
|
||||
const x1 = (sourceToDisplayTime(range.start, timelineSegments, dur) - scroll) * pxPerSec;
|
||||
const x2 = (sourceToDisplayTime(range.end, timelineSegments, dur) - scroll) * pxPerSec;
|
||||
const isSelected = selectedZone?.type === 'cut' && selectedZone.id === range.id;
|
||||
|
||||
ctx.fillStyle = isSelected ? 'rgba(239, 68, 68, 0.5)' : 'rgba(239, 68, 68, 0.3)';
|
||||
|
||||
ctx.fillStyle = isSelected ? 'rgba(239, 68, 68, 0.62)' : 'rgba(239, 68, 68, 0.46)';
|
||||
ctx.fillRect(x1, waveTop, x2 - x1, waveH);
|
||||
|
||||
// Keep cut ranges clearly visible even when not selected.
|
||||
ctx.strokeStyle = isSelected ? '#ef4444' : 'rgba(239, 68, 68, 0.9)';
|
||||
ctx.lineWidth = isSelected ? 2.8 : 1.8;
|
||||
ctx.strokeRect(x1, waveTop, x2 - x1, waveH);
|
||||
|
||||
if (isSelected) {
|
||||
ctx.strokeStyle = '#ef4444';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeRect(x1, waveTop, x2 - x1, waveH);
|
||||
|
||||
// Draw resize handles
|
||||
ctx.fillStyle = '#ef4444';
|
||||
ctx.beginPath();
|
||||
@ -634,6 +637,7 @@ export default function WaveformTimeline({
|
||||
gainMode,
|
||||
speedMode,
|
||||
selectedZone,
|
||||
showAdjustedTimeline,
|
||||
markInTime,
|
||||
markOutTime,
|
||||
displayDuration,
|
||||
@ -1184,35 +1188,47 @@ export default function WaveformTimeline({
|
||||
{markOutTime !== null && <span className="text-[10px] text-yellow-300">O {markOutTime.toFixed(2)}s</span>}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setShowCutZones((v) => !v)}
|
||||
className={`px-1.5 py-0.5 rounded text-[10px] border ${showCutZones ? 'border-red-500/60 text-red-300 bg-red-500/10' : 'border-editor-border text-editor-text-muted'}`}
|
||||
title="Toggle cut zones"
|
||||
>
|
||||
Cut
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowMuteZones((v) => !v)}
|
||||
className={`px-1.5 py-0.5 rounded text-[10px] border ${showMuteZones ? 'border-blue-500/60 text-blue-300 bg-blue-500/10' : 'border-editor-border text-editor-text-muted'}`}
|
||||
title="Toggle mute zones"
|
||||
>
|
||||
Mute
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowGainZones((v) => !v)}
|
||||
className={`px-1.5 py-0.5 rounded text-[10px] border ${showGainZones ? 'border-amber-500/60 text-amber-300 bg-amber-500/10' : 'border-editor-border text-editor-text-muted'}`}
|
||||
title="Toggle gain zones"
|
||||
>
|
||||
Gain
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowSpeedZones((v) => !v)}
|
||||
className={`px-1.5 py-0.5 rounded text-[10px] border ${showSpeedZones ? 'border-emerald-500/60 text-emerald-300 bg-emerald-500/10' : 'border-editor-border text-editor-text-muted'}`}
|
||||
title="Toggle speed zones"
|
||||
>
|
||||
Speed
|
||||
</button>
|
||||
<span className="text-[10px] text-editor-text-muted">
|
||||
<label className="flex items-center gap-1 text-[10px] text-editor-text-muted select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showAdjustedTimeline}
|
||||
onChange={(e) => setShowAdjustedTimeline(e.target.checked)}
|
||||
className="h-3 w-3 rounded border-editor-border bg-editor-surface"
|
||||
/>
|
||||
Show adjusted timeline
|
||||
</label>
|
||||
<div className="flex items-center gap-2 pl-2 ml-1 border-l border-editor-border/80">
|
||||
<span className="text-[10px] text-editor-text-muted">Show zones</span>
|
||||
<button
|
||||
onClick={() => setShowCutZones((v) => !v)}
|
||||
className={`px-1.5 py-0.5 rounded text-[10px] border ${showCutZones ? 'border-red-500/60 text-red-300 bg-red-500/10' : 'border-editor-border text-editor-text-muted'}`}
|
||||
title="Toggle cut zones"
|
||||
>
|
||||
Cut
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowMuteZones((v) => !v)}
|
||||
className={`px-1.5 py-0.5 rounded text-[10px] border ${showMuteZones ? 'border-blue-500/60 text-blue-300 bg-blue-500/10' : 'border-editor-border text-editor-text-muted'}`}
|
||||
title="Toggle mute zones"
|
||||
>
|
||||
Mute
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowGainZones((v) => !v)}
|
||||
className={`px-1.5 py-0.5 rounded text-[10px] border ${showGainZones ? 'border-amber-500/60 text-amber-300 bg-amber-500/10' : 'border-editor-border text-editor-text-muted'}`}
|
||||
title="Toggle gain zones"
|
||||
>
|
||||
Gain
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowSpeedZones((v) => !v)}
|
||||
className={`px-1.5 py-0.5 rounded text-[10px] border ${showSpeedZones ? 'border-emerald-500/60 text-emerald-300 bg-emerald-500/10' : 'border-editor-border text-editor-text-muted'}`}
|
||||
title="Toggle speed zones"
|
||||
>
|
||||
Speed
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-[10px] text-editor-text-muted pl-2 ml-1 border-l border-editor-border/80">
|
||||
Scroll · Ctrl+Scroll to zoom
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -2,6 +2,13 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useEditorStore } from '../store/editorStore';
|
||||
import { Trash2, Scissors, Volume2, SlidersHorizontal, Gauge, Play } from 'lucide-react';
|
||||
|
||||
function formatTimelineLikeTime(secs: number): string {
|
||||
const m = Math.floor(secs / 60);
|
||||
const s = secs % 60;
|
||||
if (m > 0) return `${m}:${String(Math.floor(s)).padStart(2, '0')}.${Math.floor((s % 1) * 10)}`;
|
||||
return `${s.toFixed(1)}s`;
|
||||
}
|
||||
|
||||
export default function ZoneEditor() {
|
||||
const [viewMode, setViewMode] = useState<'all' | 'cut' | 'mute' | 'gain' | 'speed'>('all');
|
||||
const [focusedZone, setFocusedZone] = useState<{ type: 'cut' | 'mute' | 'gain' | 'speed'; id: string } | null>(null);
|
||||
@ -256,7 +263,7 @@ export default function ZoneEditor() {
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium truncate">
|
||||
{range.start.toFixed(2)}s – {range.end.toFixed(2)}s
|
||||
{formatTimelineLikeTime(range.start)} - {formatTimelineLikeTime(range.end)}
|
||||
</div>
|
||||
<div className="text-editor-text-muted text-[10px]">{range.id}</div>
|
||||
</div>
|
||||
@ -293,7 +300,7 @@ export default function ZoneEditor() {
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium truncate">
|
||||
{range.start.toFixed(2)}s – {range.end.toFixed(2)}s
|
||||
{formatTimelineLikeTime(range.start)} - {formatTimelineLikeTime(range.end)}
|
||||
</div>
|
||||
<div className="text-editor-text-muted text-[10px]">{range.id}</div>
|
||||
</div>
|
||||
@ -357,7 +364,7 @@ export default function ZoneEditor() {
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium truncate">
|
||||
{range.start.toFixed(2)}s – {range.end.toFixed(2)}s
|
||||
{formatTimelineLikeTime(range.start)} - {formatTimelineLikeTime(range.end)}
|
||||
</div>
|
||||
<div className="text-editor-text-muted text-[10px]">
|
||||
{range.gainDb > 0 ? '+' : ''}{range.gainDb.toFixed(1)} dB
|
||||
@ -407,7 +414,7 @@ export default function ZoneEditor() {
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium truncate">
|
||||
{range.start.toFixed(2)}s – {range.end.toFixed(2)}s
|
||||
{formatTimelineLikeTime(range.start)} - {formatTimelineLikeTime(range.end)}
|
||||
</div>
|
||||
<div className="text-editor-text-muted text-[10px]">
|
||||
{range.speed.toFixed(2)}x
|
||||
|
||||
Reference in New Issue
Block a user