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